@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 +23 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +66 -0
- package/skills/popo-card/SKILL.md +169 -0
- package/skills/popo-group/SKILL.md +115 -0
- package/skills/popo-msg/SKILL.md +105 -0
- package/src/accounts.ts +52 -0
- package/src/auth.ts +151 -0
- package/src/bot.ts +365 -0
- package/src/channel.ts +203 -0
- package/src/client.ts +111 -0
- package/src/config-schema.ts +79 -0
- package/src/crypto.ts +69 -0
- package/src/media.ts +299 -0
- package/src/monitor.ts +241 -0
- package/src/outbound.ts +40 -0
- package/src/policy.ts +93 -0
- package/src/probe.ts +29 -0
- package/src/reply-dispatcher.ts +118 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +169 -0
- package/src/targets.ts +68 -0
- package/src/types.ts +48 -0
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;
|
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`)
|
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/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
|
+
}
|