@zlr_236/popo 0.0.1
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/index.ts +53 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +66 -0
- package/skills/popo-admin/SKILL.md +195 -0
- package/skills/popo-card/SKILL.md +571 -0
- package/skills/popo-group/SKILL.md +292 -0
- package/skills/popo-msg/SKILL.md +360 -0
- package/skills/popo-team/SKILL.md +296 -0
- package/src/accounts.ts +52 -0
- package/src/auth.ts +151 -0
- package/src/bot.ts +394 -0
- package/src/channel.ts +839 -0
- package/src/client.ts +118 -0
- package/src/config-schema.ts +79 -0
- package/src/crypto.ts +67 -0
- package/src/media.ts +623 -0
- package/src/monitor.ts +236 -0
- package/src/outbound.ts +133 -0
- package/src/policy.ts +93 -0
- package/src/probe.ts +29 -0
- package/src/reply-dispatcher.ts +141 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +430 -0
- package/src/targets.ts +68 -0
- package/src/team.ts +506 -0
- package/src/types.ts +48 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: popo-team
|
|
3
|
+
description: |
|
|
4
|
+
POPO team (group) management operations. Activate when user mentions creating groups, managing team members, or group settings.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# POPO Team Tool
|
|
8
|
+
|
|
9
|
+
Manage POPO teams (groups) and their settings.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
POPO team management supports:
|
|
14
|
+
- Create new teams with members
|
|
15
|
+
- Invite users/robots to teams
|
|
16
|
+
- Disband (delete) teams
|
|
17
|
+
- Query team members and information
|
|
18
|
+
- Update team settings and announcements
|
|
19
|
+
- Manage team permissions
|
|
20
|
+
|
|
21
|
+
## Actions
|
|
22
|
+
|
|
23
|
+
### Create Team
|
|
24
|
+
|
|
25
|
+
Create a new team (group) with initial members.
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"action": "channel-create",
|
|
30
|
+
"uid": "owner@corp.netease.com",
|
|
31
|
+
"name": "Project Team",
|
|
32
|
+
"uidList": ["member1@corp.netease.com", "member2@corp.netease.com"],
|
|
33
|
+
"photoUrl": "https://example.com/team-avatar.png"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Parameters:**
|
|
38
|
+
- `uid` (required): Team owner email
|
|
39
|
+
- `name` (optional): Team name
|
|
40
|
+
- `uidList` (required): Array of member emails
|
|
41
|
+
- `photoUrl` (optional): Team avatar URL
|
|
42
|
+
|
|
43
|
+
**Response:**
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"ok": true,
|
|
47
|
+
"tid": "1697154",
|
|
48
|
+
"passNum": 4,
|
|
49
|
+
"failList": {}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Invite to Team
|
|
54
|
+
|
|
55
|
+
Invite users or robots to join an existing team.
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"action": "addParticipant",
|
|
60
|
+
"target": "1234567",
|
|
61
|
+
"inviteList": ["newuser@corp.netease.com"],
|
|
62
|
+
"type": "1",
|
|
63
|
+
"text": "Join our project team"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Parameters:**
|
|
68
|
+
- `target` (required): Team ID
|
|
69
|
+
- `inviteList` (required): Array of emails to invite
|
|
70
|
+
- `type` (optional): "1" for normal user (default), "2" for robot
|
|
71
|
+
- `text` (optional): Application reason
|
|
72
|
+
|
|
73
|
+
**Response:**
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"ok": true,
|
|
77
|
+
"type": 0,
|
|
78
|
+
"failList": {}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Drop Team
|
|
83
|
+
|
|
84
|
+
Disband (delete) a team.
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"action": "channel-delete",
|
|
89
|
+
"channelId": "1234567"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Parameters:**
|
|
94
|
+
- `channelId` (required): Team ID to disband
|
|
95
|
+
|
|
96
|
+
### Get Team Members
|
|
97
|
+
|
|
98
|
+
Query member list of a team.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"action": "member-info",
|
|
103
|
+
"tid": "1234567",
|
|
104
|
+
"page": 1,
|
|
105
|
+
"limit": 20
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
- `tid` (required): Team ID
|
|
111
|
+
- `page` (optional): Page number, default 1
|
|
112
|
+
- `limit` (optional): Page size, default 20
|
|
113
|
+
|
|
114
|
+
**Response:**
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"ok": true,
|
|
118
|
+
"records": [
|
|
119
|
+
{
|
|
120
|
+
"uid": "user@corp.netease.com",
|
|
121
|
+
"name": "张三",
|
|
122
|
+
"nickName": "张三 nick",
|
|
123
|
+
"teamNickName": "群昵称"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"total": 25,
|
|
127
|
+
"page": 1,
|
|
128
|
+
"limit": 20
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Get Team Info
|
|
133
|
+
|
|
134
|
+
Query basic information of a team.
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"action": "channel-info",
|
|
139
|
+
"channelId": "1234567",
|
|
140
|
+
"scopes": ["board"]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
- `channelId` (required): Team ID
|
|
146
|
+
- `scopes` (optional): Array of scopes to query, e.g., `["board"]` for announcement
|
|
147
|
+
|
|
148
|
+
**Response:**
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"ok": true,
|
|
152
|
+
"info": {
|
|
153
|
+
"name": "Project Team",
|
|
154
|
+
"desc": "Team description",
|
|
155
|
+
"board": "Team announcement"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Update Team Info
|
|
161
|
+
|
|
162
|
+
Update team name and announcement.
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"action": "channel-edit",
|
|
167
|
+
"channelId": "1234567",
|
|
168
|
+
"name": "New Team Name",
|
|
169
|
+
"board": "Updated announcement",
|
|
170
|
+
"type": 2
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Parameters:**
|
|
175
|
+
- `channelId` (required): Team ID
|
|
176
|
+
- `name` (required): New team name
|
|
177
|
+
- `board` (required): New announcement
|
|
178
|
+
- `type` (optional): Notification type (1=notification only, 2=card announcement)
|
|
179
|
+
|
|
180
|
+
### Update Team Management Settings
|
|
181
|
+
|
|
182
|
+
Update team permission settings.
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"action": "update-team-mgmt",
|
|
187
|
+
"tid": "1234567",
|
|
188
|
+
"type": 8,
|
|
189
|
+
"value": 1
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Setting Types:**
|
|
194
|
+
|
|
195
|
+
| Type | Description | Value |
|
|
196
|
+
|------|-------------|-------|
|
|
197
|
+
| 1 | Only owner/admin can edit group info | 1=on, 2=off |
|
|
198
|
+
| 2 | Only owner/admin can add members | 1=on, 2=off |
|
|
199
|
+
| 3 | Allow apply to join | 1=on, 2=off |
|
|
200
|
+
| 4 | Members can view chat history | 1=on, 2=off |
|
|
201
|
+
| 5 | New member join notification | 1=all, 2=owner/admin only, 3=none |
|
|
202
|
+
| 6 | Member leave notification | 1=all, 2=owner/admin only, 3=none |
|
|
203
|
+
| 7 | Allow modify nickname in group | 1=on, 2=off |
|
|
204
|
+
| 8 | Allow external accounts to join | 1=on, 2=off |
|
|
205
|
+
|
|
206
|
+
## Examples
|
|
207
|
+
|
|
208
|
+
### Create a project team
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"action": "channel-create",
|
|
213
|
+
"uid": "pm@corp.netease.com",
|
|
214
|
+
"name": "2024 Q1 Project",
|
|
215
|
+
"uidList": [
|
|
216
|
+
"dev1@corp.netease.com",
|
|
217
|
+
"dev2@corp.netease.com",
|
|
218
|
+
"designer@corp.netease.com"
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Invite new member
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"action": "addParticipant",
|
|
228
|
+
"target": "1234567",
|
|
229
|
+
"inviteList": ["newhire@corp.netease.com"],
|
|
230
|
+
"text": "Welcome to the team!"
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Update team announcement
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"action": "channel-edit",
|
|
239
|
+
"channelId": "1234567",
|
|
240
|
+
"name": "Project Team",
|
|
241
|
+
"board": "Meeting tomorrow at 10am",
|
|
242
|
+
"type": 2
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Enable external access
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"action": "update-team-mgmt",
|
|
251
|
+
"tid": "1234567",
|
|
252
|
+
"type": 8,
|
|
253
|
+
"value": 1
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Get team members
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"action": "member-info",
|
|
262
|
+
"tid": "8888888",
|
|
263
|
+
"page": 1,
|
|
264
|
+
"limit": 50
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Configuration
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
channels:
|
|
272
|
+
popo:
|
|
273
|
+
enabled: true
|
|
274
|
+
appKey: "your_app_key"
|
|
275
|
+
appSecret: "your_app_secret"
|
|
276
|
+
token: "your_token"
|
|
277
|
+
aesKey: "your_aes_key"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Notes
|
|
281
|
+
|
|
282
|
+
- Team IDs (tid) are numeric strings, typically 7 digits
|
|
283
|
+
- Only team owner or admin can update team info and settings
|
|
284
|
+
- Member emails use domains: `@corp.netease.com` or `@mesg.corp.netease.com`
|
|
285
|
+
- When disbanding a team, all members will be removed
|
|
286
|
+
- Team creation requires appropriate bot permissions
|
|
287
|
+
|
|
288
|
+
## API Reference
|
|
289
|
+
|
|
290
|
+
- Create team: `POST /open-apis/robots/v1/team`
|
|
291
|
+
- Invite to team: `POST /open-apis/robots/v1/team/{tid}/invite`
|
|
292
|
+
- Drop team: `DELETE /open-apis/robots/v1/team/{tid}/drop`
|
|
293
|
+
- Get members: `GET /open-apis/robots/v1/team/{tid}/members`
|
|
294
|
+
- Get info: `GET /open-apis/robots/v1/team/{tid}/info`
|
|
295
|
+
- Update info: `PUT /open-apis/robots/v1/team/{tid}/info`
|
|
296
|
+
- Update settings: `POST /open-apis/robots/v1/team/{tid}/management`
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { PopoConfig, ResolvedPopoAccount } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function resolvePopoCredentials(cfg?: PopoConfig): {
|
|
6
|
+
appKey: string;
|
|
7
|
+
appSecret: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
aesKey?: string;
|
|
10
|
+
server: string;
|
|
11
|
+
} | null {
|
|
12
|
+
const appKey = cfg?.appKey?.trim();
|
|
13
|
+
const appSecret = cfg?.appSecret?.trim();
|
|
14
|
+
if (!appKey || !appSecret) return null;
|
|
15
|
+
return {
|
|
16
|
+
appKey,
|
|
17
|
+
appSecret,
|
|
18
|
+
token: cfg?.token?.trim() || undefined,
|
|
19
|
+
aesKey: cfg?.aesKey?.trim() || undefined,
|
|
20
|
+
server: cfg?.server ?? "https://open.popo.netease.com",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePopoAccount(params: {
|
|
25
|
+
cfg: ClawdbotConfig;
|
|
26
|
+
accountId?: string | null;
|
|
27
|
+
}): ResolvedPopoAccount {
|
|
28
|
+
const popoCfg = params.cfg.channels?.popo as PopoConfig | undefined;
|
|
29
|
+
const enabled = popoCfg?.enabled !== false;
|
|
30
|
+
const creds = resolvePopoCredentials(popoCfg);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
accountId: params.accountId?.trim() || DEFAULT_ACCOUNT_ID,
|
|
34
|
+
enabled,
|
|
35
|
+
configured: Boolean(creds),
|
|
36
|
+
appKey: creds?.appKey,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listPopoAccountIds(_cfg: ClawdbotConfig): string[] {
|
|
41
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveDefaultPopoAccountId(_cfg: ClawdbotConfig): string {
|
|
45
|
+
return DEFAULT_ACCOUNT_ID;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function listEnabledPopoAccounts(cfg: ClawdbotConfig): ResolvedPopoAccount[] {
|
|
49
|
+
return listPopoAccountIds(cfg)
|
|
50
|
+
.map((accountId) => resolvePopoAccount({ cfg, accountId }))
|
|
51
|
+
.filter((account) => account.enabled && account.configured);
|
|
52
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { PopoConfig, PopoToken } from "./types.js";
|
|
2
|
+
import { resolvePopoCredentials } from "./accounts.js";
|
|
3
|
+
|
|
4
|
+
// Token cache
|
|
5
|
+
let cachedToken: PopoToken | null = null;
|
|
6
|
+
let cachedAppKey: string | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get a valid access token, refreshing if necessary.
|
|
10
|
+
*/
|
|
11
|
+
export async function getAccessToken(cfg: PopoConfig): Promise<string> {
|
|
12
|
+
const creds = resolvePopoCredentials(cfg);
|
|
13
|
+
if (!creds) {
|
|
14
|
+
throw new Error("POPO credentials not configured (appKey, appSecret required)");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
|
|
19
|
+
// Check if we have a valid cached token
|
|
20
|
+
if (
|
|
21
|
+
cachedToken &&
|
|
22
|
+
cachedAppKey === creds.appKey &&
|
|
23
|
+
cachedToken.accessExpiredAt > now + 60000 // 1 minute buffer
|
|
24
|
+
) {
|
|
25
|
+
return cachedToken.accessToken;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if we can use refresh token
|
|
29
|
+
if (
|
|
30
|
+
cachedToken &&
|
|
31
|
+
cachedAppKey === creds.appKey &&
|
|
32
|
+
cachedToken.refreshExpiredAt > now + 60000
|
|
33
|
+
) {
|
|
34
|
+
try {
|
|
35
|
+
cachedToken = await refreshAccessToken(cfg, cachedToken.refreshToken);
|
|
36
|
+
return cachedToken.accessToken;
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall through to get a new token
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get a new token
|
|
43
|
+
cachedToken = await fetchNewToken(cfg);
|
|
44
|
+
cachedAppKey = creds.appKey;
|
|
45
|
+
return cachedToken.accessToken;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch a new token using appKey and appSecret.
|
|
50
|
+
*/
|
|
51
|
+
async function fetchNewToken(cfg: PopoConfig): Promise<PopoToken> {
|
|
52
|
+
const creds = resolvePopoCredentials(cfg);
|
|
53
|
+
if (!creds) {
|
|
54
|
+
throw new Error("POPO credentials not configured");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await fetch(`${creds.server}/open-apis/robots/v1/token`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
appKey: creds.appKey,
|
|
64
|
+
appSecret: creds.appSecret,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`POPO token request failed: ${response.status} ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = (await response.json()) as {
|
|
73
|
+
errcode?: number;
|
|
74
|
+
errmsg?: string;
|
|
75
|
+
data?: {
|
|
76
|
+
accessToken: string;
|
|
77
|
+
accessExpiredAt: number;
|
|
78
|
+
refreshToken: string;
|
|
79
|
+
refreshExpiredAt: number;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (data.errcode !== 0 || !data.data) {
|
|
84
|
+
throw new Error(`POPO token request failed: ${data.errmsg || "unknown error"}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
accessToken: data.data.accessToken,
|
|
89
|
+
accessExpiredAt: data.data.accessExpiredAt,
|
|
90
|
+
refreshToken: data.data.refreshToken,
|
|
91
|
+
refreshExpiredAt: data.data.refreshExpiredAt,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Refresh an access token using a refresh token.
|
|
97
|
+
*/
|
|
98
|
+
export async function refreshAccessToken(
|
|
99
|
+
cfg: PopoConfig,
|
|
100
|
+
refreshToken: string
|
|
101
|
+
): Promise<PopoToken> {
|
|
102
|
+
const creds = resolvePopoCredentials(cfg);
|
|
103
|
+
if (!creds) {
|
|
104
|
+
throw new Error("POPO credentials not configured");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const response = await fetch(`${creds.server}/open-apis/robots/v1/token/refresh`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
appKey: creds.appKey,
|
|
114
|
+
refreshToken,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new Error(`POPO token refresh failed: ${response.status} ${response.statusText}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = (await response.json()) as {
|
|
123
|
+
errcode?: number;
|
|
124
|
+
errmsg?: string;
|
|
125
|
+
data?: {
|
|
126
|
+
accessToken: string;
|
|
127
|
+
accessExpiredAt: number;
|
|
128
|
+
refreshToken: string;
|
|
129
|
+
refreshExpiredAt: number;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (data.errcode !== 0 || !data.data) {
|
|
134
|
+
throw new Error(`POPO token refresh failed: ${data.errmsg || "unknown error"}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
accessToken: data.data.accessToken,
|
|
139
|
+
accessExpiredAt: data.data.accessExpiredAt,
|
|
140
|
+
refreshToken: data.data.refreshToken,
|
|
141
|
+
refreshExpiredAt: data.data.refreshExpiredAt,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clear the token cache.
|
|
147
|
+
*/
|
|
148
|
+
export function clearTokenCache() {
|
|
149
|
+
cachedToken = null;
|
|
150
|
+
cachedAppKey = null;
|
|
151
|
+
}
|