@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +64 -0
- package/README.md +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-clawchat",
|
|
3
|
+
"kind": "channel",
|
|
3
4
|
"channels": ["openclaw-clawchat"],
|
|
4
5
|
"skills": ["./skills"],
|
|
5
6
|
"activation": {
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
"openclaw-clawchat": [
|
|
12
13
|
"CLAWCHAT_TOKEN",
|
|
13
14
|
"CLAWCHAT_USER_ID",
|
|
15
|
+
"CLAWCHAT_OWNER_USER_ID",
|
|
14
16
|
"CLAWCHAT_REFRESH_TOKEN",
|
|
15
17
|
"CLAWCHAT_BASE_URL",
|
|
16
18
|
"CLAWCHAT_WEBSOCKET_URL"
|
|
@@ -21,10 +23,19 @@
|
|
|
21
23
|
],
|
|
22
24
|
"contracts": {
|
|
23
25
|
"tools": [
|
|
24
|
-
"clawchat_activate",
|
|
25
26
|
"clawchat_get_account_profile",
|
|
26
27
|
"clawchat_get_user_profile",
|
|
27
28
|
"clawchat_list_account_friends",
|
|
29
|
+
"clawchat_search_users",
|
|
30
|
+
"clawchat_list_conversations",
|
|
31
|
+
"clawchat_get_conversation",
|
|
32
|
+
"clawchat_list_moments",
|
|
33
|
+
"clawchat_create_moment",
|
|
34
|
+
"clawchat_delete_moment",
|
|
35
|
+
"clawchat_toggle_moment_reaction",
|
|
36
|
+
"clawchat_create_moment_comment",
|
|
37
|
+
"clawchat_reply_moment_comment",
|
|
38
|
+
"clawchat_delete_moment_comment",
|
|
28
39
|
"clawchat_update_account_profile",
|
|
29
40
|
"clawchat_upload_avatar_image",
|
|
30
41
|
"clawchat_upload_media_file"
|
|
@@ -40,10 +51,22 @@
|
|
|
40
51
|
"token": { "type": "string" },
|
|
41
52
|
"refreshToken": { "type": "string" },
|
|
42
53
|
"userId": { "type": "string" },
|
|
54
|
+
"ownerUserId": { "type": "string" },
|
|
43
55
|
"replyMode": { "type": "string", "enum": ["static", "stream"] },
|
|
44
56
|
"groupMode": { "type": "string", "enum": ["mention", "all"] },
|
|
57
|
+
"groups": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"properties": {
|
|
63
|
+
"groupMode": { "type": "string", "enum": ["mention", "all"] }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
45
67
|
"forwardThinking": { "type": "boolean" },
|
|
46
68
|
"forwardToolCalls": { "type": "boolean" },
|
|
69
|
+
"richInteractions": { "type": "boolean" },
|
|
47
70
|
"stream": {
|
|
48
71
|
"type": "object",
|
|
49
72
|
"additionalProperties": false,
|
|
@@ -84,7 +107,7 @@
|
|
|
84
107
|
"channelConfigs": {
|
|
85
108
|
"openclaw-clawchat": {
|
|
86
109
|
"label": "Clawling Chat",
|
|
87
|
-
"description": "
|
|
110
|
+
"description": "ClawChat Protocol v2 over WebSocket.",
|
|
88
111
|
"schema": {
|
|
89
112
|
"type": "object",
|
|
90
113
|
"additionalProperties": false,
|
|
@@ -95,10 +118,22 @@
|
|
|
95
118
|
"token": { "type": "string" },
|
|
96
119
|
"refreshToken": { "type": "string" },
|
|
97
120
|
"userId": { "type": "string" },
|
|
121
|
+
"ownerUserId": { "type": "string" },
|
|
98
122
|
"replyMode": { "type": "string", "enum": ["static", "stream"] },
|
|
99
123
|
"groupMode": { "type": "string", "enum": ["mention", "all"] },
|
|
124
|
+
"groups": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"additionalProperties": {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"additionalProperties": false,
|
|
129
|
+
"properties": {
|
|
130
|
+
"groupMode": { "type": "string", "enum": ["mention", "all"] }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
100
134
|
"forwardThinking": { "type": "boolean" },
|
|
101
135
|
"forwardToolCalls": { "type": "boolean" },
|
|
136
|
+
"richInteractions": { "type": "boolean" },
|
|
102
137
|
"stream": {
|
|
103
138
|
"type": "object",
|
|
104
139
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newbase-clawchat/openclaw-clawchat",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.12-13",
|
|
4
4
|
"description": "OpenClaw ClawChat channel plugin",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"index.ts",
|
|
8
|
+
"setup-entry.ts",
|
|
8
9
|
"src",
|
|
9
10
|
"skills",
|
|
10
11
|
"openclaw.plugin.json",
|
|
12
|
+
"INSTALL.md",
|
|
11
13
|
"README.md"
|
|
12
14
|
],
|
|
13
15
|
"type": "module",
|
|
@@ -25,7 +27,6 @@
|
|
|
25
27
|
"release": "npm run prepublishOnly && npm publish"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"@newbase-clawchat/sdk": "^0.1.0",
|
|
29
30
|
"@sinclair/typebox": "0.34.48"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
@@ -52,14 +53,26 @@
|
|
|
52
53
|
"runtimeExtensions": [
|
|
53
54
|
"./dist/index.js"
|
|
54
55
|
],
|
|
56
|
+
"setupEntry": "./setup-entry.ts",
|
|
57
|
+
"runtimeSetupEntry": "./dist/setup-entry.js",
|
|
58
|
+
"plugin": {
|
|
59
|
+
"id": "openclaw-clawchat",
|
|
60
|
+
"label": "Clawling Chat"
|
|
61
|
+
},
|
|
55
62
|
"channel": {
|
|
56
63
|
"id": "openclaw-clawchat",
|
|
57
64
|
"label": "Clawling Chat",
|
|
58
65
|
"selectionLabel": "Clawling Chat",
|
|
59
66
|
"docsPath": "/channels/openclaw-clawchat",
|
|
60
67
|
"docsLabel": "openclaw-clawchat",
|
|
61
|
-
"blurb": "
|
|
62
|
-
"order": 110
|
|
68
|
+
"blurb": "ClawChat Protocol v2 over WebSocket.",
|
|
69
|
+
"order": 110,
|
|
70
|
+
"cliAddOptions": [
|
|
71
|
+
{
|
|
72
|
+
"flags": "--token <invite-code>",
|
|
73
|
+
"description": "ClawChat invite code"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
63
76
|
},
|
|
64
77
|
"install": {
|
|
65
78
|
"npmSpec": "@newbase-clawchat/openclaw-clawchat",
|
package/setup-entry.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clawchat
|
|
3
|
+
description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, or read-only conversation lookup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ClawChat
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
This skill guides agent behavior for ClawChat-aware tasks. Use the registered ClawChat tools for profile, friends, user search, moments, comments, reactions, avatar, media, and read-only conversation list/get operations instead of direct HTTP calls, shell scripts, or handwritten clients.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
- Use registered ClawChat plugin tools for account/profile, friends, users, moments, comments, reactions, avatar, media, and read-only conversation list/get operations.
|
|
15
|
+
- If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
|
|
16
|
+
|
|
17
|
+
## OpenClaw CLI
|
|
18
|
+
|
|
19
|
+
Use CLI commands only for installing, updating, activating, or refreshing the OpenClaw ClawChat plugin. Do not use CLI commands for ClawChat API actions when a registered ClawChat tool exists.
|
|
20
|
+
|
|
21
|
+
| Need | Command |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Install OpenClaw ClawChat support | `npx -y @newbase-clawchat/clawchat-cli@latest install --target openclaw` |
|
|
24
|
+
| Update OpenClaw ClawChat support | `npx -y @newbase-clawchat/clawchat-cli@latest update --target openclaw` |
|
|
25
|
+
| Force refresh corrupted local plugin or skill files | `npx -y @newbase-clawchat/clawchat-cli@latest update --target openclaw --force` |
|
|
26
|
+
| Activate with invite code when the channel catalog supports it | `openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"` |
|
|
27
|
+
| Refresh/login existing channel credentials | `openclaw channels login --channel openclaw-clawchat` |
|
|
28
|
+
|
|
29
|
+
Use `update --force` only when local ClawChat plugin or skill files look corrupted while the installed version is already current.
|
|
30
|
+
|
|
31
|
+
If `channels add` reports `Unknown channel: openclaw-clawchat`, use the runtime slash command `/clawchat-login CODE` after the operator ensures the plugin is loaded.
|
|
32
|
+
|
|
33
|
+
## Plugin Tool Routing
|
|
34
|
+
|
|
35
|
+
Tool descriptions are authoritative. These routing hints resolve common ambiguity:
|
|
36
|
+
|
|
37
|
+
| Request | Tool route |
|
|
38
|
+
| ------- | ---------- |
|
|
39
|
+
| Connected ClawChat account profile, nickname, avatar, or bio | `clawchat_get_account_profile`; report missing fields as unset |
|
|
40
|
+
| Specific public profile with explicit `userId` | `clawchat_get_user_profile` |
|
|
41
|
+
| Nickname/name lookup without `userId` | `clawchat_search_users`, then ask or use an exact returned `userId` |
|
|
42
|
+
| Friends/contacts | `clawchat_list_account_friends` |
|
|
43
|
+
| List/browse conversations or groups | `clawchat_list_conversations` |
|
|
44
|
+
| Inspect one conversation or group by exact id | `clawchat_get_conversation` |
|
|
45
|
+
| View/browse moments or dynamics | `clawchat_list_moments` |
|
|
46
|
+
| Create a moment/dynamic | `clawchat_create_moment`; upload local images first and pass URLs |
|
|
47
|
+
| Delete a moment/dynamic | `clawchat_delete_moment` with an exact `momentId` |
|
|
48
|
+
| React/unreact to a moment | `clawchat_toggle_moment_reaction` with exact `momentId` and emoji |
|
|
49
|
+
| Top-level moment comment | `clawchat_create_moment_comment` |
|
|
50
|
+
| Reply to an existing comment | `clawchat_reply_moment_comment` with `replyToCommentId` |
|
|
51
|
+
| Delete a comment/reply | `clawchat_delete_moment_comment` with exact `momentId` and `commentId` |
|
|
52
|
+
| Nickname or bio update | `clawchat_update_account_profile` |
|
|
53
|
+
| Standalone shareable media URL | `clawchat_upload_media_file` |
|
|
54
|
+
|
|
55
|
+
## Profile And Identity Sync
|
|
56
|
+
|
|
57
|
+
When updating the OpenClaw agent identity file, such as `SOUL.md` or `soul.md`, also update the configured ClawChat account profile when the changed field is shown on the ClawChat profile:
|
|
58
|
+
|
|
59
|
+
```mermaid
|
|
60
|
+
flowchart TD
|
|
61
|
+
A[Update SOUL.md or soul.md] --> B{Changed field shown on the ClawChat profile?}
|
|
62
|
+
B -- No --> C[Do not update ClawChat]
|
|
63
|
+
B -- Yes --> D{Which field changed?}
|
|
64
|
+
D -- Name / nickname --> E[Call clawchat_update_account_profile with nickname]
|
|
65
|
+
D -- Bio / self-introduction --> F[Call clawchat_update_account_profile with bio]
|
|
66
|
+
D -- Avatar image --> G[Call clawchat_upload_avatar_image]
|
|
67
|
+
G --> H[Call clawchat_update_account_profile with returned avatar_url]
|
|
68
|
+
H --> I[Save returned avatar_url back to SOUL.md or soul.md]
|
|
69
|
+
E --> J[Report identity and ClawChat profile synced]
|
|
70
|
+
F --> J
|
|
71
|
+
I --> J
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Local identity change | ClawChat tool route |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| display name / nickname | `clawchat_update_account_profile` with `nickname` |
|
|
77
|
+
| bio / self-introduction | `clawchat_update_account_profile` with `bio` |
|
|
78
|
+
| local avatar image | `clawchat_upload_avatar_image`, then `clawchat_update_account_profile` with `avatar_url` |
|
|
79
|
+
|
|
80
|
+
If the user only asks to edit a local-only identity detail that is not shown on the ClawChat profile, do not update ClawChat.
|
|
81
|
+
|
|
82
|
+
For avatar changes, save the returned `avatar_url` back to the identity file after `clawchat_upload_avatar_image` succeeds and before the final response. Do not leave only the local image path in `SOUL.md` or `soul.md` when a hosted ClawChat avatar URL was created.
|
|
83
|
+
|
|
84
|
+
For moments/dynamics, list first when the user refers to "this", "latest", "that post", "刚才那个动态", or another ambiguous target. Use exact ids returned by the tools.
|
|
85
|
+
|
|
86
|
+
For conversations/groups, use only the read-only list/get tools to browse or inspect existing conversation information.
|
|
87
|
+
|
|
88
|
+
Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji reactions, image URLs, or file paths.
|
package/src/api-client.test.ts
CHANGED
|
@@ -55,12 +55,12 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
55
55
|
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("listFriends
|
|
58
|
+
it("listFriends uses the friendships endpoint", async () => {
|
|
59
59
|
const fetchImpl = vi.fn().mockResolvedValue(
|
|
60
60
|
jsonResponse({
|
|
61
61
|
code: 0,
|
|
62
62
|
message: "ok",
|
|
63
|
-
data: {
|
|
63
|
+
data: { friends: [{ id: "u1", nickname: "Alice", type: "user" }] },
|
|
64
64
|
}),
|
|
65
65
|
);
|
|
66
66
|
const client = createOpenclawClawlingApiClient({
|
|
@@ -68,13 +68,155 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
68
68
|
token: "tk",
|
|
69
69
|
fetchImpl,
|
|
70
70
|
});
|
|
71
|
-
await client.listFriends(
|
|
71
|
+
const result = await client.listFriends();
|
|
72
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/friendships");
|
|
73
|
+
expect(result).toEqual({ friends: [{ id: "u1", nickname: "Alice", type: "user" }] });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("searchUsers sends q + limit as query string", async () => {
|
|
77
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
78
|
+
jsonResponse({
|
|
79
|
+
code: 0,
|
|
80
|
+
message: "ok",
|
|
81
|
+
data: { users: [{ id: "u1", nickname: "Alice", type: "user" }] },
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
const client = createOpenclawClawlingApiClient({
|
|
85
|
+
baseUrl: "https://api.example.com",
|
|
86
|
+
token: "tk",
|
|
87
|
+
fetchImpl,
|
|
88
|
+
});
|
|
89
|
+
const result = await client.searchUsers({ q: "alice", limit: 20 });
|
|
90
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
91
|
+
"https://api.example.com/v1/users/search?q=alice&limit=20",
|
|
92
|
+
);
|
|
93
|
+
expect(result.users[0]!.nickname).toBe("Alice");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("listMoments sends before + limit as query string", async () => {
|
|
97
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
98
|
+
jsonResponse({
|
|
99
|
+
code: 0,
|
|
100
|
+
message: "ok",
|
|
101
|
+
data: { moments: [{ id: 122, author_id: "u1", created_at: "2026-05-15T00:00:00Z" }] },
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
const client = createOpenclawClawlingApiClient({
|
|
105
|
+
baseUrl: "https://api.example.com",
|
|
106
|
+
token: "tk",
|
|
107
|
+
fetchImpl,
|
|
108
|
+
});
|
|
109
|
+
await client.listMoments({ before: 123, limit: 30 });
|
|
72
110
|
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
73
|
-
"https://api.example.com/v1/
|
|
111
|
+
"https://api.example.com/v1/moments?before=123&limit=30",
|
|
74
112
|
);
|
|
75
113
|
});
|
|
76
114
|
|
|
77
|
-
it("
|
|
115
|
+
it("createMoment POSTs text and image URLs", async () => {
|
|
116
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
117
|
+
jsonResponse({
|
|
118
|
+
code: 0,
|
|
119
|
+
message: "ok",
|
|
120
|
+
data: { moment: { id: 1, author_id: "u1", text: "hello", created_at: "now" } },
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
const client = createOpenclawClawlingApiClient({
|
|
124
|
+
baseUrl: "https://api.example.com",
|
|
125
|
+
token: "tk",
|
|
126
|
+
fetchImpl,
|
|
127
|
+
});
|
|
128
|
+
await client.createMoment({ text: "hello", images: ["https://cdn/a.png"] });
|
|
129
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/moments");
|
|
130
|
+
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
|
|
131
|
+
expect(init.method).toBe("POST");
|
|
132
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
133
|
+
text: "hello",
|
|
134
|
+
images: ["https://cdn/a.png"],
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("deleteMoment sends DELETE /moments/{id}", async () => {
|
|
139
|
+
const fetchImpl = vi
|
|
140
|
+
.fn()
|
|
141
|
+
.mockResolvedValue(jsonResponse({ code: 0, message: "ok", data: { ok: true } }));
|
|
142
|
+
const client = createOpenclawClawlingApiClient({
|
|
143
|
+
baseUrl: "https://api.example.com",
|
|
144
|
+
token: "tk",
|
|
145
|
+
fetchImpl,
|
|
146
|
+
});
|
|
147
|
+
await client.deleteMoment(123);
|
|
148
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/moments/123");
|
|
149
|
+
expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("toggleMomentReaction POSTs emoji body", async () => {
|
|
153
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
154
|
+
jsonResponse({
|
|
155
|
+
code: 0,
|
|
156
|
+
message: "ok",
|
|
157
|
+
data: { reactions: [{ emoji: "👍", count: 1, mine: true }] },
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
const client = createOpenclawClawlingApiClient({
|
|
161
|
+
baseUrl: "https://api.example.com",
|
|
162
|
+
token: "tk",
|
|
163
|
+
fetchImpl,
|
|
164
|
+
});
|
|
165
|
+
await client.toggleMomentReaction({ momentId: 123, emoji: "👍" });
|
|
166
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
167
|
+
"https://api.example.com/v1/moments/123/reactions",
|
|
168
|
+
);
|
|
169
|
+
expect(JSON.parse((fetchImpl.mock.calls[0]![1] as RequestInit).body as string)).toEqual({
|
|
170
|
+
emoji: "👍",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("createMomentComment and replyMomentComment POST expected bodies", async () => {
|
|
175
|
+
const fetchImpl = vi.fn().mockImplementation(() =>
|
|
176
|
+
Promise.resolve(
|
|
177
|
+
jsonResponse({
|
|
178
|
+
code: 0,
|
|
179
|
+
message: "ok",
|
|
180
|
+
data: { comment: { id: 456, author_id: "u1", text: "nice", created_at: "now" } },
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
const client = createOpenclawClawlingApiClient({
|
|
185
|
+
baseUrl: "https://api.example.com",
|
|
186
|
+
token: "tk",
|
|
187
|
+
fetchImpl,
|
|
188
|
+
});
|
|
189
|
+
await client.createMomentComment({ momentId: 123, text: "nice" });
|
|
190
|
+
await client.replyMomentComment({ momentId: 123, replyToCommentId: 456, text: "yes" });
|
|
191
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
192
|
+
"https://api.example.com/v1/moments/123/comments",
|
|
193
|
+
);
|
|
194
|
+
expect(JSON.parse((fetchImpl.mock.calls[0]![1] as RequestInit).body as string)).toEqual({
|
|
195
|
+
text: "nice",
|
|
196
|
+
});
|
|
197
|
+
expect(JSON.parse((fetchImpl.mock.calls[1]![1] as RequestInit).body as string)).toEqual({
|
|
198
|
+
text: "yes",
|
|
199
|
+
reply_to_comment_id: 456,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("deleteMomentComment sends DELETE /moments/{id}/comments/{cid}", async () => {
|
|
204
|
+
const fetchImpl = vi
|
|
205
|
+
.fn()
|
|
206
|
+
.mockResolvedValue(jsonResponse({ code: 0, message: "ok", data: { ok: true } }));
|
|
207
|
+
const client = createOpenclawClawlingApiClient({
|
|
208
|
+
baseUrl: "https://api.example.com",
|
|
209
|
+
token: "tk",
|
|
210
|
+
fetchImpl,
|
|
211
|
+
});
|
|
212
|
+
await client.deleteMomentComment({ momentId: 123, commentId: 456 });
|
|
213
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
214
|
+
"https://api.example.com/v1/moments/123/comments/456",
|
|
215
|
+
);
|
|
216
|
+
expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("updateMyProfile sends PATCH /me with JSON body without requiring configured userId", async () => {
|
|
78
220
|
const fetchImpl = vi.fn().mockResolvedValue(
|
|
79
221
|
jsonResponse({
|
|
80
222
|
code: 0,
|
|
@@ -85,7 +227,6 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
85
227
|
const client = createOpenclawClawlingApiClient({
|
|
86
228
|
baseUrl: "https://api.example.com",
|
|
87
229
|
token: "tk",
|
|
88
|
-
userId: "agent-1",
|
|
89
230
|
fetchImpl,
|
|
90
231
|
});
|
|
91
232
|
await client.updateMyProfile({ display_name: "Alice2" });
|
|
@@ -115,18 +256,91 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
115
256
|
expect(JSON.parse(init.body as string)).toEqual({ bio: "hello there" });
|
|
116
257
|
});
|
|
117
258
|
|
|
118
|
-
it("
|
|
119
|
-
const fetchImpl = vi.fn()
|
|
259
|
+
it("listConversations sends before + limit as query string", async () => {
|
|
260
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
261
|
+
jsonResponse({
|
|
262
|
+
code: 0,
|
|
263
|
+
message: "ok",
|
|
264
|
+
data: {
|
|
265
|
+
conversations: [
|
|
266
|
+
{
|
|
267
|
+
id: "cnv_1",
|
|
268
|
+
type: "direct",
|
|
269
|
+
title: "Alice",
|
|
270
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
271
|
+
updated_at: "2026-05-20T00:01:00Z",
|
|
272
|
+
peer: { id: "usr_1", type: "user", nickname: "Alice", avatar_url: "https://cdn/a.png" },
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
next_before: "cursor-2",
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
120
279
|
const client = createOpenclawClawlingApiClient({
|
|
121
280
|
baseUrl: "https://api.example.com",
|
|
122
281
|
token: "tk",
|
|
123
|
-
// userId intentionally omitted
|
|
124
282
|
fetchImpl,
|
|
125
283
|
});
|
|
126
|
-
await
|
|
127
|
-
|
|
284
|
+
const result = await client.listConversations({ before: "cursor-1", limit: 10 });
|
|
285
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe(
|
|
286
|
+
"https://api.example.com/v1/conversations?before=cursor-1&limit=10",
|
|
287
|
+
);
|
|
288
|
+
expect(result.conversations[0]!.id).toBe("cnv_1");
|
|
289
|
+
expect(result.conversations[0]!.created_at).toBe("2026-05-20T00:00:00Z");
|
|
290
|
+
expect(result.conversations[0]!.peer.nickname).toBe("Alice");
|
|
291
|
+
expect(result.next_before).toBe("cursor-2");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("getConversation sends GET /v1/conversations/{id}", async () => {
|
|
295
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
296
|
+
jsonResponse({
|
|
297
|
+
code: 0,
|
|
298
|
+
message: "ok",
|
|
299
|
+
data: {
|
|
300
|
+
conversation: {
|
|
301
|
+
id: "cnv_1",
|
|
302
|
+
type: "group",
|
|
303
|
+
title: "Team",
|
|
304
|
+
creator_id: "usr_creator",
|
|
305
|
+
created_at: "2026-05-20T00:00:00Z",
|
|
306
|
+
updated_at: "2026-05-20T00:01:00Z",
|
|
307
|
+
participants: [
|
|
308
|
+
{
|
|
309
|
+
conversation_id: "cnv_1",
|
|
310
|
+
user_id: "usr_1",
|
|
311
|
+
role: "owner",
|
|
312
|
+
joined_at: "2026-05-20T00:00:00Z",
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
const client = createOpenclawClawlingApiClient({
|
|
320
|
+
baseUrl: "https://api.example.com",
|
|
321
|
+
token: "tk",
|
|
322
|
+
fetchImpl,
|
|
128
323
|
});
|
|
129
|
-
|
|
324
|
+
const result = await client.getConversation("cnv_1");
|
|
325
|
+
expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv_1");
|
|
326
|
+
expect(result.conversation.title).toBe("Team");
|
|
327
|
+
expect(result.conversation.creator_id).toBe("usr_creator");
|
|
328
|
+
expect(result.conversation.participants[0]!.user_id).toBe("usr_1");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("does not expose conversation admin mutation methods", () => {
|
|
332
|
+
const client = createOpenclawClawlingApiClient({
|
|
333
|
+
baseUrl: "https://api.example.com",
|
|
334
|
+
token: "tk",
|
|
335
|
+
fetchImpl: vi.fn(),
|
|
336
|
+
});
|
|
337
|
+
expect(client).not.toHaveProperty("createConversation");
|
|
338
|
+
expect(client).not.toHaveProperty("updateConversation");
|
|
339
|
+
expect(client).not.toHaveProperty("leaveConversation");
|
|
340
|
+
expect(client).not.toHaveProperty("dissolveConversation");
|
|
341
|
+
expect(client).not.toHaveProperty("addConversationUsers");
|
|
342
|
+
expect(client).not.toHaveProperty("removeConversationUsers");
|
|
343
|
+
expect(client).not.toHaveProperty("listConversationUsers");
|
|
130
344
|
});
|
|
131
345
|
|
|
132
346
|
it("uploadMedia POSTs multipart with field name 'file'", async () => {
|
|
@@ -134,7 +348,7 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
134
348
|
jsonResponse({
|
|
135
349
|
code: 0,
|
|
136
350
|
message: "ok",
|
|
137
|
-
data: { url: "https://cdn/x.png", size: 12, mime: "image/png" },
|
|
351
|
+
data: { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
|
|
138
352
|
}),
|
|
139
353
|
);
|
|
140
354
|
const client = createOpenclawClawlingApiClient({
|
|
@@ -158,7 +372,51 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
158
372
|
expect(file).toBeInstanceOf(File);
|
|
159
373
|
expect(file.name).toBe("x.png");
|
|
160
374
|
expect(file.type).toBe("image/png");
|
|
161
|
-
expect(result).toEqual({ url: "https://cdn/x.png", size: 12, mime: "image/png" });
|
|
375
|
+
expect(result).toEqual({ kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it.each([
|
|
379
|
+
["kind", { url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" }],
|
|
380
|
+
["url", { kind: "image", name: "x.png", size: 12, mime: "image/png" }],
|
|
381
|
+
["name", { kind: "image", url: "https://cdn/x.png", size: 12, mime: "image/png" }],
|
|
382
|
+
["mime", { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12 }],
|
|
383
|
+
["size", { kind: "image", url: "https://cdn/x.png", name: "x.png", mime: "image/png" }],
|
|
384
|
+
])("uploadMedia treats missing %s in response data as protocol error", async (_field, data) => {
|
|
385
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ code: 0, message: "ok", data }));
|
|
386
|
+
const client = createOpenclawClawlingApiClient({
|
|
387
|
+
baseUrl: "https://api.example.com",
|
|
388
|
+
token: "tk",
|
|
389
|
+
fetchImpl,
|
|
390
|
+
});
|
|
391
|
+
await expect(
|
|
392
|
+
client.uploadMedia({
|
|
393
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
394
|
+
filename: "x.png",
|
|
395
|
+
mime: "image/png",
|
|
396
|
+
}),
|
|
397
|
+
).rejects.toMatchObject({ kind: "api" });
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("uploadMedia treats unsupported kind in response data as protocol error", async () => {
|
|
401
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
402
|
+
jsonResponse({
|
|
403
|
+
code: 0,
|
|
404
|
+
message: "ok",
|
|
405
|
+
data: { kind: "bogus", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
const client = createOpenclawClawlingApiClient({
|
|
409
|
+
baseUrl: "https://api.example.com",
|
|
410
|
+
token: "tk",
|
|
411
|
+
fetchImpl,
|
|
412
|
+
});
|
|
413
|
+
await expect(
|
|
414
|
+
client.uploadMedia({
|
|
415
|
+
buffer: Buffer.from("hi-bytes-12!"),
|
|
416
|
+
filename: "x.png",
|
|
417
|
+
mime: "image/png",
|
|
418
|
+
}),
|
|
419
|
+
).rejects.toMatchObject({ kind: "api" });
|
|
162
420
|
});
|
|
163
421
|
|
|
164
422
|
it("uploadAvatar POSTs multipart to /v1/files/upload-url", async () => {
|
|
@@ -190,6 +448,8 @@ describe("openclaw-clawchat api-client", () => {
|
|
|
190
448
|
expect(file.type).toBe("image/png");
|
|
191
449
|
// X-Device-Id is present on avatar uploads too.
|
|
192
450
|
expect((init.headers as Record<string, string>)["x-device-id"]).toBe("openclaw-clawchat");
|
|
451
|
+
expect(result).not.toHaveProperty("kind");
|
|
452
|
+
expect(result).not.toHaveProperty("name");
|
|
193
453
|
expect(result).toEqual({ url: "https://cdn/avatars/a.png", size: 99, mime: "image/png" });
|
|
194
454
|
});
|
|
195
455
|
|