@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.
@@ -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`
@@ -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
+ }