@shenhh/popo 0.1.8

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 ADDED
@@ -0,0 +1,23 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import { popoPlugin } from "./src/channel.js";
4
+ import { setPopoRuntime } from "./src/runtime.js";
5
+
6
+ export { monitorPopoProvider } from "./src/monitor.js";
7
+ export { sendMessagePopo, sendRichTextPopo, sendCardPopo } from "./src/send.js";
8
+ export { uploadImagePopo, uploadFilePopo, sendImagePopo, sendFilePopo, sendMediaPopo } from "./src/media.js";
9
+ export { probePopo } from "./src/probe.js";
10
+ export { popoPlugin } from "./src/channel.js";
11
+
12
+ const plugin = {
13
+ id: "popo",
14
+ name: "POPO",
15
+ description: "POPO channel plugin",
16
+ configSchema: emptyPluginConfigSchema(),
17
+ register(api: OpenClawPluginApi) {
18
+ setPopoRuntime(api.runtime);
19
+ api.registerChannel({ plugin: popoPlugin });
20
+ },
21
+ };
22
+
23
+ export default plugin;
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "popo",
3
+ "channels": ["popo"],
4
+ "skills": ["./skills"],
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {}
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@shenhh/popo",
3
+ "version": "0.1.8",
4
+ "type": "module",
5
+ "description": "OpenClaw POPO channel plugin",
6
+ "license": "MIT",
7
+ "files": [
8
+ "index.ts",
9
+ "src",
10
+ "skills",
11
+ "openclaw.plugin.json"
12
+ ],
13
+ "author": {
14
+ "name": "Hengheng Shen",
15
+ "email": "1048157315@qq.com"
16
+ },
17
+ "publishConfig": {
18
+ "cache": "~/.npm",
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/m1heng/clawdbot-feishu.git"
24
+ },
25
+ "keywords": [
26
+ "openclaw",
27
+ "popo",
28
+ "netease",
29
+ "chatbot",
30
+ "ai",
31
+ "claude"
32
+ ],
33
+ "openclaw": {
34
+ "extensions": [
35
+ "./index.ts"
36
+ ],
37
+ "channel": {
38
+ "id": "popo",
39
+ "label": "POPO",
40
+ "selectionLabel": "POPO (网易)",
41
+ "docsPath": "/channels/popo",
42
+ "docsLabel": "popo",
43
+ "blurb": "POPO enterprise messaging.",
44
+ "aliases": [],
45
+ "order": 80
46
+ },
47
+ "install": {
48
+ "npmSpec": "@shenhh/clawdbot-popo-plugin",
49
+ "localPath": ".",
50
+ "defaultChoice": "npm"
51
+ }
52
+ },
53
+ "dependencies": {
54
+ "@sinclair/typebox": "^0.34.48",
55
+ "zod": "^4.3.6"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.0.10",
59
+ "openclaw": "2026.1.29",
60
+ "tsx": "^4.21.0",
61
+ "typescript": "^5.7.0"
62
+ },
63
+ "peerDependencies": {
64
+ "openclaw": ">=2026.1.29"
65
+ }
66
+ }
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: popo-card
3
+ description: |
4
+ POPO interactive card message operations. Activate when user mentions card messages, interactive cards, or template messages.
5
+ ---
6
+
7
+ # POPO Card Tool
8
+
9
+ Send interactive card messages in POPO.
10
+
11
+ ## Overview
12
+
13
+ POPO cards are interactive message templates that support:
14
+ - Structured layouts
15
+ - Variables for dynamic content
16
+ - Interactive buttons and actions
17
+ - Rich formatting
18
+
19
+ ## Actions
20
+
21
+ ### Send Card
22
+
23
+ ```json
24
+ {
25
+ "action": "send",
26
+ "to": "user@example.com",
27
+ "templateUuid": "tpl_xxx",
28
+ "instanceUuid": "inst_xxx",
29
+ "variables": {
30
+ "title": "Order Notification",
31
+ "content": "Your order has been shipped",
32
+ "orderId": "12345"
33
+ }
34
+ }
35
+ ```
36
+
37
+ To group:
38
+ ```json
39
+ {
40
+ "action": "send",
41
+ "to": "group:123456",
42
+ "templateUuid": "tpl_xxx",
43
+ "instanceUuid": "inst_xxx",
44
+ "variables": {
45
+ "title": "Meeting Reminder",
46
+ "time": "2024-01-15 10:00"
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Update Card
52
+
53
+ ```json
54
+ {
55
+ "action": "update",
56
+ "cardId": "card_xxx",
57
+ "variables": {
58
+ "status": "Completed",
59
+ "updatedAt": "2024-01-15 15:30"
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## Card Template Structure
65
+
66
+ POPO card templates are created in the POPO developer console. Key concepts:
67
+
68
+ ### Template UUID
69
+ Unique identifier for the card template design, created in POPO console.
70
+
71
+ ### Instance UUID
72
+ Unique identifier for each card instance. Generate a new UUID for each card sent to track updates.
73
+
74
+ ### Variables
75
+ Key-value pairs that fill in the template placeholders:
76
+ ```json
77
+ {
78
+ "variables": {
79
+ "{{title}}": "Value for title",
80
+ "{{body}}": "Value for body content",
81
+ "{{buttonText}}": "Click Me"
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## Examples
87
+
88
+ ### Notification Card
89
+
90
+ ```json
91
+ {
92
+ "action": "send",
93
+ "to": "user@example.com",
94
+ "templateUuid": "notification_tpl_001",
95
+ "instanceUuid": "ntf_20240115_001",
96
+ "variables": {
97
+ "type": "系统通知",
98
+ "title": "密码即将过期",
99
+ "content": "您的密码将于3天后过期,请及时修改",
100
+ "actionUrl": "https://account.company.com/password"
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### Approval Card
106
+
107
+ ```json
108
+ {
109
+ "action": "send",
110
+ "to": "approver@example.com",
111
+ "templateUuid": "approval_tpl_001",
112
+ "instanceUuid": "apr_20240115_001",
113
+ "variables": {
114
+ "applicant": "张三",
115
+ "type": "请假申请",
116
+ "duration": "2024-01-20 至 2024-01-22",
117
+ "reason": "个人事务"
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Status Update
123
+
124
+ After approval action:
125
+ ```json
126
+ {
127
+ "action": "update",
128
+ "cardId": "apr_20240115_001",
129
+ "variables": {
130
+ "status": "已批准",
131
+ "approver": "李四",
132
+ "approvedAt": "2024-01-15 16:00"
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Configuration
138
+
139
+ ```yaml
140
+ channels:
141
+ popo:
142
+ appKey: "your_app_key"
143
+ appSecret: "your_app_secret"
144
+ ```
145
+
146
+ ## Notes
147
+
148
+ - Card templates must be created and approved in POPO developer console first
149
+ - `instanceUuid` should be unique per card for tracking and updates
150
+ - Card updates only work within the card's validity period
151
+ - Interactive button callbacks are handled via webhook events
152
+
153
+ ## Callback Handling
154
+
155
+ When users click card buttons, POPO sends `ACTION` events to your webhook:
156
+
157
+ ```json
158
+ {
159
+ "eventType": "ACTION",
160
+ "eventData": {
161
+ "actionId": "approve_btn",
162
+ "userId": "user@example.com",
163
+ "cardId": "apr_20240115_001",
164
+ "data": { "action": "approve" }
165
+ }
166
+ }
167
+ ```
168
+
169
+ Handle these in your bot logic to process user interactions.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: popo-group
3
+ description: |
4
+ POPO group management operations. Activate when user mentions POPO groups, group members, or group management.
5
+ ---
6
+
7
+ # POPO Group Tool
8
+
9
+ Manage POPO groups and group members.
10
+
11
+ ## Actions
12
+
13
+ ### List Groups
14
+
15
+ ```json
16
+ { "action": "list" }
17
+ ```
18
+
19
+ Returns all groups the bot is a member of.
20
+
21
+ ### Get Group Info
22
+
23
+ ```json
24
+ { "action": "info", "groupId": "123456" }
25
+ ```
26
+
27
+ Returns: group name, member count, owner, etc.
28
+
29
+ ### List Group Members
30
+
31
+ ```json
32
+ { "action": "members", "groupId": "123456" }
33
+ ```
34
+
35
+ Returns list of members with email and role.
36
+
37
+ ### Create Group
38
+
39
+ ```json
40
+ { "action": "create", "name": "Project Team", "members": ["user1@example.com", "user2@example.com"] }
41
+ ```
42
+
43
+ With description:
44
+ ```json
45
+ { "action": "create", "name": "Project Team", "description": "Project discussion group", "members": ["user1@example.com"] }
46
+ ```
47
+
48
+ ### Add Members
49
+
50
+ ```json
51
+ { "action": "add_member", "groupId": "123456", "members": ["newuser@example.com"] }
52
+ ```
53
+
54
+ ### Remove Members
55
+
56
+ ```json
57
+ { "action": "remove_member", "groupId": "123456", "members": ["user@example.com"] }
58
+ ```
59
+
60
+ ### Update Group Info
61
+
62
+ ```json
63
+ { "action": "update", "groupId": "123456", "name": "New Group Name" }
64
+ ```
65
+
66
+ Update description:
67
+ ```json
68
+ { "action": "update", "groupId": "123456", "description": "Updated description" }
69
+ ```
70
+
71
+ ### Leave Group
72
+
73
+ ```json
74
+ { "action": "leave", "groupId": "123456" }
75
+ ```
76
+
77
+ Bot leaves the group.
78
+
79
+ ## Examples
80
+
81
+ Create a project group:
82
+ ```json
83
+ {
84
+ "action": "create",
85
+ "name": "2024 Q1 项目组",
86
+ "description": "Q1季度项目讨论群",
87
+ "members": ["pm@company.com", "dev@company.com", "test@company.com"]
88
+ }
89
+ ```
90
+
91
+ Add new team member:
92
+ ```json
93
+ { "action": "add_member", "groupId": "888888", "members": ["newjoin@company.com"] }
94
+ ```
95
+
96
+ ## Permissions
97
+
98
+ - Bot must be group admin to add/remove members
99
+ - Only group owner can delete the group
100
+ - Bot can only access groups it has been added to
101
+
102
+ ## Configuration
103
+
104
+ ```yaml
105
+ channels:
106
+ popo:
107
+ appKey: "your_app_key"
108
+ appSecret: "your_app_secret"
109
+ ```
110
+
111
+ ## Notes
112
+
113
+ - Group IDs are numeric strings in POPO
114
+ - Member identifiers are email addresses
115
+ - Creating groups requires appropriate bot permissions from POPO admin
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: popo-msg
3
+ description: |
4
+ POPO message sending operations. Activate when user wants to send messages via POPO to users or groups.
5
+ ---
6
+
7
+ # POPO Message Tool
8
+
9
+ Send messages to POPO users or groups.
10
+
11
+ ## Target Formats
12
+
13
+ - **User (P2P)**: `user:email@example.com` or just `email@example.com`
14
+ - **Group**: `group:123456` or just `123456` (numeric group ID)
15
+
16
+ ## Actions
17
+
18
+ ### Send Text Message
19
+
20
+ ```json
21
+ { "action": "send", "to": "user@example.com", "text": "Hello!" }
22
+ ```
23
+
24
+ To group:
25
+ ```json
26
+ { "action": "send", "to": "group:123456", "text": "Hello everyone!" }
27
+ ```
28
+
29
+ ### Send with @mention
30
+
31
+ ```json
32
+ { "action": "send", "to": "group:123456", "text": "Please review", "at": ["user1@example.com", "user2@example.com"] }
33
+ ```
34
+
35
+ @all in group:
36
+ ```json
37
+ { "action": "send", "to": "group:123456", "text": "Important announcement", "atAll": true }
38
+ ```
39
+
40
+ ### Send Rich Text
41
+
42
+ ```json
43
+ {
44
+ "action": "send_rich",
45
+ "to": "user@example.com",
46
+ "content": [
47
+ { "tag": "text", "text": "Hello " },
48
+ { "tag": "a", "text": "click here", "href": "https://example.com" }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ Supported tags:
54
+ - `text` - Plain text
55
+ - `a` - Hyperlink (text + href)
56
+ - `at` - @mention (userId)
57
+
58
+ ### Send Image
59
+
60
+ ```json
61
+ { "action": "send_image", "to": "user@example.com", "url": "https://example.com/image.png" }
62
+ ```
63
+
64
+ From local file:
65
+ ```json
66
+ { "action": "send_image", "to": "user@example.com", "path": "/path/to/image.png" }
67
+ ```
68
+
69
+ ### Send File
70
+
71
+ ```json
72
+ { "action": "send_file", "to": "user@example.com", "url": "https://example.com/document.pdf" }
73
+ ```
74
+
75
+ From local file:
76
+ ```json
77
+ { "action": "send_file", "to": "user@example.com", "path": "/path/to/document.pdf" }
78
+ ```
79
+
80
+ ## Examples
81
+
82
+ Simple text to user:
83
+ ```json
84
+ { "action": "send", "to": "alice@company.com", "text": "会议提醒:明天上午10点开项目评审会" }
85
+ ```
86
+
87
+ Announcement to group with @all:
88
+ ```json
89
+ { "action": "send", "to": "group:888888", "text": "全员通知:系统将于今晚23:00-24:00进行维护", "atAll": true }
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ ```yaml
95
+ channels:
96
+ popo:
97
+ appKey: "your_app_key"
98
+ appSecret: "your_app_secret"
99
+ ```
100
+
101
+ ## Notes
102
+
103
+ - POPO uses email as user identifier for P2P messages
104
+ - Group messages require the bot to be added to the group first
105
+ - File size limit: 20MB (configurable via `mediaMaxMb`)
@@ -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/open-apis/robots/v1",
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}/auth/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
+ code?: number;
74
+ message?: string;
75
+ result?: {
76
+ accessToken: string;
77
+ accessExpiredAt: number;
78
+ refreshToken: string;
79
+ refreshExpiredAt: number;
80
+ };
81
+ };
82
+
83
+ if (data.code !== 200 || !data.result) {
84
+ throw new Error(`POPO token request failed: ${data.message || "unknown error"}`);
85
+ }
86
+
87
+ return {
88
+ accessToken: data.result.accessToken,
89
+ accessExpiredAt: data.result.accessExpiredAt,
90
+ refreshToken: data.result.refreshToken,
91
+ refreshExpiredAt: data.result.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}/auth/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
+ code?: number;
124
+ message?: string;
125
+ result?: {
126
+ accessToken: string;
127
+ accessExpiredAt: number;
128
+ refreshToken: string;
129
+ refreshExpiredAt: number;
130
+ };
131
+ };
132
+
133
+ if (data.code !== 200 || !data.result) {
134
+ throw new Error(`POPO token refresh failed: ${data.message || "unknown error"}`);
135
+ }
136
+
137
+ return {
138
+ accessToken: data.result.accessToken,
139
+ accessExpiredAt: data.result.accessExpiredAt,
140
+ refreshToken: data.result.refreshToken,
141
+ refreshExpiredAt: data.result.refreshExpiredAt,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Clear the token cache.
147
+ */
148
+ export function clearTokenCache() {
149
+ cachedToken = null;
150
+ cachedAppKey = null;
151
+ }