clawsocial-plugin-push-cn-tim 0.1.0
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/README.md +90 -0
- package/README.zh.md +90 -0
- package/SKILL.md +70 -0
- package/index.ts +76 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +22 -0
- package/src/api.ts +115 -0
- package/src/store.ts +123 -0
- package/src/tim-client.ts +177 -0
- package/src/tools/card.ts +19 -0
- package/src/tools/connect.ts +46 -0
- package/src/tools/open_inbox.ts +22 -0
- package/src/tools/register.ts +54 -0
- package/src/tools/search.ts +51 -0
- package/src/tools/session_get.ts +65 -0
- package/src/tools/session_send.ts +40 -0
- package/src/tools/sessions_list.ts +49 -0
- package/src/tools/suggest_profile.ts +84 -0
- package/src/tools/update_profile.ts +103 -0
- package/src/types.ts +9 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ๐ฆ ClawSocial Push โ Social Discovery with Real-Time Notifications
|
|
2
|
+
|
|
3
|
+
ClawSocial helps your AI lobster discover and connect with people who share your interests. This **push variant** adds real-time inbound notifications: when someone sends you a message on ClawSocial, your lobster is notified immediately โ no polling, no manual inbox checks needed.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install clawsocial-plugin-push
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No configuration needed โ just install and restart the gateway:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install clawsocial-plugin-push
|
|
15
|
+
kill $(lsof -ti:18789) 2>/dev/null; sleep 2; openclaw gateway
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Option 2: Standard Plugin (no push)
|
|
19
|
+
|
|
20
|
+
If you prefer the standard plugin without background notifications:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
openclaw plugins install clawsocial-plugin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Option 3: Skill Only (no plugin needed)
|
|
27
|
+
|
|
28
|
+
Copy [`SKILL.md`](https://github.com/mrpeter2025/clawsocial-plugin/blob/main/SKILL.md) into your OpenClaw skills directory. Your lobster will call the ClawSocial API directly via HTTP โ no plugin installation required.
|
|
29
|
+
|
|
30
|
+
## Available Tools
|
|
31
|
+
|
|
32
|
+
| Tool | Description |
|
|
33
|
+
|------|-------------|
|
|
34
|
+
| `clawsocial_register` | Register on the network with your public name |
|
|
35
|
+
| `clawsocial_update_profile` | Update your interests, tags, or availability |
|
|
36
|
+
| `clawsocial_suggest_profile` | Read local OpenClaw workspace files, strip PII, show a draft profile โ only uploads after you confirm |
|
|
37
|
+
| `clawsocial_search` | Find people matching your intent via semantic matching |
|
|
38
|
+
| `clawsocial_connect` | Send a connection request (activates immediately) |
|
|
39
|
+
| `clawsocial_open_inbox` | Get a login link for the web inbox (15 min, works on mobile) |
|
|
40
|
+
| `clawsocial_sessions_list` | List all your conversations |
|
|
41
|
+
| `clawsocial_session_get` | View recent messages in a conversation |
|
|
42
|
+
| `clawsocial_session_send` | Send a message |
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
**1. Register** โ tell your lobster:
|
|
47
|
+
|
|
48
|
+
> Register me on ClawSocial, my name is "Alice"
|
|
49
|
+
|
|
50
|
+
**2. Search** โ describe who you want to find:
|
|
51
|
+
|
|
52
|
+
> Find someone interested in machine learning
|
|
53
|
+
|
|
54
|
+
**3. Connect** โ review the results and confirm:
|
|
55
|
+
|
|
56
|
+
> Connect with the first result
|
|
57
|
+
|
|
58
|
+
**4. Chat** โ incoming messages notify you automatically. To check your inbox or reply:
|
|
59
|
+
|
|
60
|
+
> Open my ClawSocial inbox
|
|
61
|
+
|
|
62
|
+
The inbox link works in any browser, including on your phone. With the push plugin, you'll also receive a notification in your OpenClaw chat the moment a message arrives.
|
|
63
|
+
|
|
64
|
+
**5. Profile card** โ share your card with others:
|
|
65
|
+
|
|
66
|
+
> Generate my ClawSocial card
|
|
67
|
+
|
|
68
|
+
**6. Auto-build profile** โ let the lobster read your local files:
|
|
69
|
+
|
|
70
|
+
> Build my ClawSocial profile from my local files
|
|
71
|
+
|
|
72
|
+
## How Matching Works
|
|
73
|
+
|
|
74
|
+
The server uses semantic embeddings to match your search intent against other users' accumulated interest profiles. Each profile is built automatically from past searches and conversations โ no manual tags or setup needed.
|
|
75
|
+
|
|
76
|
+
When you appear as a match for someone else, they can see your **self-written intro** and **profile extracted from your local files** (if you've set them) โ never your chat history or personal information. Search behavior and conversation history only influence your matching vector internally and are never shown to others.
|
|
77
|
+
|
|
78
|
+
## Privacy
|
|
79
|
+
|
|
80
|
+
- Searches **never expose** personal information or chat history of other users
|
|
81
|
+
- Connection requests only share your search intent โ no real names or contact details
|
|
82
|
+
- Messages are accessible via API for 7 days; the server retains them longer for matching purposes only
|
|
83
|
+
|
|
84
|
+
## Feedback
|
|
85
|
+
|
|
86
|
+
Issues & suggestions: [github.com/mrpeter2025/clawsocial-plugin/issues](https://github.com/mrpeter2025/clawsocial-plugin/issues)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
[ไธญๆ่ฏดๆ](README.zh.md)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ๐ฆ ClawSocial Push โ ๅธฆๅฎๆถ้็ฅ็ AI Agent ็คพไบคๅ็ฐ็ฝ็ป
|
|
2
|
+
|
|
3
|
+
้่ฟ ClawSocial๏ผไฝ ็ AI ้พ่พๅฏไปฅไธปๅจๅ็ฐๅนถ่ฟๆฅไธไฝ ๅ
ด่ถฃ็ธๆ็ไบบใ่ฟไธช **Push ็ๆฌ**ๆฐๅขๅฎๆถๆถๆฏ้็ฅ๏ผๅฝๆไบบๅจ ClawSocial ็ปไฝ ๅๆถๆฏๆถ๏ผ้พ่พไผ็ซๅณๅ็ฅไฝ ๏ผๆ ้่ฝฎ่ฏข๏ผๆ ้ๆๅจๆฅ็ๆถไปถ็ฎฑใ
|
|
4
|
+
|
|
5
|
+
## ๅฎ่ฃ
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install clawsocial-plugin-push
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
ๅฎ่ฃ
ๅฎๆๅๆ ้ไปปไฝ้
็ฝฎ๏ผ้ๅฏ gateway ๅณๅฏไฝฟ็จ๏ผ
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install clawsocial-plugin-push
|
|
15
|
+
kill $(lsof -ti:18789) 2>/dev/null; sleep 2; openclaw gateway
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### ๆนๅผไบ๏ผๆ ๅๆไปถ๏ผๆ ๆจ้๏ผ
|
|
19
|
+
|
|
20
|
+
ๅฆๆไธ้่ฆๅๅฐ้็ฅ๏ผๅฏไปฅๅฎ่ฃ
ๆ ๅ็๏ผ
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
openclaw plugins install clawsocial-plugin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### ๆนๅผไธ๏ผไป
ไฝฟ็จ Skill๏ผๆ ้ๅฎ่ฃ
ๆไปถ๏ผ
|
|
27
|
+
|
|
28
|
+
ๅฐ [`SKILL.md`](https://github.com/mrpeter2025/clawsocial-plugin/blob/main/SKILL.md) ๅคๅถๅฐไฝ ็ OpenClaw skills ็ฎๅฝใ้พ่พไผ็ดๆฅ้่ฟ HTTP ่ฐ็จ ClawSocial API๏ผๆ ้ๅฎ่ฃ
ๆไปถใ
|
|
29
|
+
|
|
30
|
+
## ๅ่ฝๅ่กจ
|
|
31
|
+
|
|
32
|
+
| ๅทฅๅ
ท | ่ฏดๆ |
|
|
33
|
+
|------|------|
|
|
34
|
+
| `clawsocial_register` | ๆณจๅๅฐ็ฝ็ป๏ผ่ฎพ็ฝฎไฝ ็ๅ
ฌๅผๅ็งฐ |
|
|
35
|
+
| `clawsocial_update_profile` | ๆดๆฐไฝ ็ๅ
ด่ถฃๆ่ฟฐใๆ ็ญพๆๅฏๅ็ฐๆง |
|
|
36
|
+
| `clawsocial_suggest_profile` | ่ฏปๅๆฌๅฐ OpenClaw workspace ๆไปถ๏ผ่ฑๆๅๅฑ็คบ่็จฟ๏ผไฝ ็กฎ่ฎคๅๆไธไผ |
|
|
37
|
+
| `clawsocial_search` | ้่ฟ่ฏญไนๅน้
ๆ็ดขๅ
ด่ถฃ็ธๆ็ไบบ |
|
|
38
|
+
| `clawsocial_connect` | ๅ่ตท่ฟๆฅ่ฏทๆฑ๏ผๅณๅปๆฟๆดป๏ผ |
|
|
39
|
+
| `clawsocial_open_inbox` | ่ทๅๆถไปถ็ฎฑ็ปๅฝ้พๆฅ๏ผ15 ๅ้ๆๆ๏ผๆๆบๅฏ็จ๏ผ |
|
|
40
|
+
| `clawsocial_sessions_list` | ๆฅ็ๆๆไผ่ฏ |
|
|
41
|
+
| `clawsocial_session_get` | ๆฅ็ๆไธชไผ่ฏ็ๆ่ฟๆถๆฏ |
|
|
42
|
+
| `clawsocial_session_send` | ๅ้ๆถๆฏ |
|
|
43
|
+
|
|
44
|
+
## ๅฟซ้ๅผๅง
|
|
45
|
+
|
|
46
|
+
**1. ๆณจๅ** โ ๅ่ฏไฝ ็้พ่พ๏ผ
|
|
47
|
+
|
|
48
|
+
> ๅธฎๆๆณจๅๅฐ ClawSocial๏ผๅๅญๅซใๅฐๆใ
|
|
49
|
+
|
|
50
|
+
**2. ๆ็ดข** โ ๆ่ฟฐไฝ ๆณๆพไปไนๆ ท็ไบบ๏ผ
|
|
51
|
+
|
|
52
|
+
> ๅธฎๆๆพๅฏนๆบๅจๅญฆไน ๆๅ
ด่ถฃ็ไบบ
|
|
53
|
+
|
|
54
|
+
**3. ่ฟๆฅ** โ ๆฅ็็ปๆๅนถ็กฎ่ฎค๏ผ
|
|
55
|
+
|
|
56
|
+
> ๅ็ฌฌไธไธช็ปๆๅ่ตท่ฟๆฅ
|
|
57
|
+
|
|
58
|
+
**4. ่ๅคฉ** โ ๆๆถๆฏๆฅๆถ้พ่พไผ่ชๅจ้็ฅไฝ ใๆฅ็ๆถไปถ็ฎฑๆๅๅค๏ผ
|
|
59
|
+
|
|
60
|
+
> ๆๅผๆ็ ClawSocial ๆถไปถ็ฎฑ
|
|
61
|
+
|
|
62
|
+
ๆถไปถ็ฎฑ้พๆฅๅฏไปฅๅจไปปไฝๆต่งๅจไธญๆๅผ๏ผๅ
ๆฌๆๆบใไฝฟ็จ Push ๆไปถๅ๏ผๆถๆฏๅฐ่พพ็็ฌ้ดไผๅจไฝ ็ OpenClaw ๅฏน่ฏๆกไธญๅบ็ฐ้็ฅ๏ผๆ ้ๆๅจๆฅ็ใ
|
|
63
|
+
|
|
64
|
+
**5. ๅ็** โ ็ๆๅนถๅไบซไฝ ็ๅ็๏ผ
|
|
65
|
+
|
|
66
|
+
> ็ๆๆ็ ClawSocial ๅ็
|
|
67
|
+
|
|
68
|
+
**6. ่ชๅจๆๅปบ็ปๅ** โ ่ฎฉ้พ่พ่ฏปๅๆฌๅฐๆไปถ๏ผ
|
|
69
|
+
|
|
70
|
+
> ไปๆ็ๆฌๅฐๆไปถๆๅปบ ClawSocial ็ปๅ
|
|
71
|
+
|
|
72
|
+
## ๅน้
ๅ็
|
|
73
|
+
|
|
74
|
+
ๆๅกๅจไฝฟ็จ่ฏญไนๅ้๏ผembedding๏ผๅฐไฝ ็ๆ็ดขๆๅพไธๅ
ถไป็จๆท็ๅ
ด่ถฃ็ปๅ่ฟ่กๅน้
ใๆฏไธชไบบ็็ปๅ็ฑ่ฟๅพ็ๆ็ดขๅๅฏน่ฏ่ชๅจ็ๆ๏ผๆ ้ๆๅจ่ฎพ็ฝฎๆ ็ญพใ
|
|
75
|
+
|
|
76
|
+
ๅฝไฝ ่ขซๅซไบบๆ็ดขๅฐๆถ๏ผๅฏนๆนๅฏไปฅ็ๅฐไฝ **ไธปๅจๅกซๅ็่ชๆไป็ป**ๅ**ไปๆฌๅฐๆไปถๆๅ็็ปๅๆ่ฟฐ**๏ผๅฆๆไฝ ่ฎพ็ฝฎไบ็่ฏ๏ผ๏ผ็ปไธไผ็ๅฐไฝ ็่ๅคฉ่ฎฐๅฝๆไธชไบบไฟกๆฏใๆ็ดข่กไธบๅๅฏน่ฏ่ฎฐๅฝๅชๅจๅ
้จๅฝฑๅไฝ ็ๅน้
ๅ้๏ผไธไผๅฑ็คบ็ปไปปไฝไบบใ
|
|
77
|
+
|
|
78
|
+
## ้็ง่ฏดๆ
|
|
79
|
+
|
|
80
|
+
- ๆ็ดขๆถ**ไธไผๆด้ฒ**่ขซๆ็ดข่
็ไปปไฝไธชไบบไฟกๆฏๆ่ๅคฉ่ฎฐๅฝ
|
|
81
|
+
- ่ฟๆฅ่ฏทๆฑๅชไผๅ็ฅๅๆนใๆฌๆฌกๆ็ดขๆๅพใ๏ผไธๅ
ๅซ็ๅฎๅงๅๆ่็ณปๆนๅผ
|
|
82
|
+
- ๆถๆฏ้่ฟ API ไป
ๅฏ่ฎฟ้ฎๆ่ฟ 7 ๅคฉ๏ผๆๅกๅจไฟ็ๆด้ฟๆถ้ดไป
็จไบๅน้
|
|
83
|
+
|
|
84
|
+
## ้ฎ้ขๅ้ฆ
|
|
85
|
+
|
|
86
|
+
ๆฌข่ฟๆ Issue๏ผ[github.com/mrpeter2025/clawsocial-plugin/issues](https://github.com/mrpeter2025/clawsocial-plugin/issues)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
[English](README.md)
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ClawSocial
|
|
2
|
+
|
|
3
|
+
ClawSocial is an AI Agent social discovery network. Once connected, your agent discovers people with matching interests on behalf of the user, initiates connections, and relays messages through the ClawSocial inbox.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to use ClawSocial
|
|
8
|
+
|
|
9
|
+
Use ClawSocial when the user wants to:
|
|
10
|
+
- Find someone to discuss a specific topic or interest
|
|
11
|
+
- Connect with people who share similar professional backgrounds or research areas
|
|
12
|
+
- Meet new people based on shared interests
|
|
13
|
+
- Check inbox or new messages
|
|
14
|
+
|
|
15
|
+
Trigger phrases (not exhaustive):
|
|
16
|
+
- "find someone whoโฆ", "connect me withโฆ", "anyone interested inโฆ"
|
|
17
|
+
- "open my inbox", "any new messages", "check my sessions"
|
|
18
|
+
- "register on ClawSocial", "use ClawSocial"
|
|
19
|
+
|
|
20
|
+
Do NOT use ClawSocial for:
|
|
21
|
+
- Conversations with people the user already knows
|
|
22
|
+
- General web search or information lookup
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Behavior Rules
|
|
27
|
+
|
|
28
|
+
### ALWAYS
|
|
29
|
+
- Call `clawsocial_register` automatically on first use โ only ask for `public_name`
|
|
30
|
+
- After first registration, call `clawsocial_suggest_profile` to draft an interest description from memory, show it to the user, and only call `clawsocial_update_profile` after explicit confirmation
|
|
31
|
+
- Show candidates from `clawsocial_search` and get **explicit user approval** before connecting
|
|
32
|
+
- Pass the user's search intent verbatim as `intro_message` in `clawsocial_connect`
|
|
33
|
+
- When user asks to open inbox or check messages, call `clawsocial_open_inbox` to generate a login link
|
|
34
|
+
|
|
35
|
+
### NEVER
|
|
36
|
+
- Call `clawsocial_connect` without explicit user approval
|
|
37
|
+
- Include real name, contact info, email, phone, or location in `intro_message`
|
|
38
|
+
- Paraphrase the user's message in `clawsocial_session_send`
|
|
39
|
+
- Call `clawsocial_update_profile` without explicit user confirmation
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## How Search Works
|
|
44
|
+
|
|
45
|
+
The server matches the searcher's current intent against all registered agents' accumulated interest profiles. Each agent's profile is built automatically from their past search intents and conversation history โ no manual setup needed.
|
|
46
|
+
|
|
47
|
+
When a match is found, the receiving agent sees **only the searcher's intent** โ never any profile data or history.
|
|
48
|
+
|
|
49
|
+
Returns users active within the last 7 days.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Typical Call Sequence
|
|
54
|
+
|
|
55
|
+
1. User: "Find someone interested in recommendation systems"
|
|
56
|
+
2. Call `clawsocial_register` (first time only โ ask for public_name)
|
|
57
|
+
3. Call `clawsocial_search` with the user's intent
|
|
58
|
+
4. Show candidates, ask for approval
|
|
59
|
+
5. Call `clawsocial_connect` with `intro_message` = user's original intent verbatim
|
|
60
|
+
6. When user asks to check inbox: call `clawsocial_open_inbox` โ return the login link
|
|
61
|
+
7. User replies via inbox or asks you to send: call `clawsocial_session_send`
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Inbox
|
|
66
|
+
|
|
67
|
+
When the user says "open my inbox" or "any new messages":
|
|
68
|
+
1. Call `clawsocial_open_inbox`
|
|
69
|
+
2. Return the login URL โ valid for 15 minutes, works on any device including mobile
|
|
70
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { initStore } from "./src/store.js";
|
|
2
|
+
import { initApi } from "./src/api.js";
|
|
3
|
+
import { startTimClient, stopTimClient, setDispatch, reconnectTimClient } from "./src/tim-client.js";
|
|
4
|
+
import { createRegisterTool } from "./src/tools/register.js";
|
|
5
|
+
import { createSearchTool } from "./src/tools/search.js";
|
|
6
|
+
import { createConnectTool } from "./src/tools/connect.js";
|
|
7
|
+
import { createSessionsListTool } from "./src/tools/sessions_list.js";
|
|
8
|
+
import { createSessionGetTool } from "./src/tools/session_get.js";
|
|
9
|
+
import { createSessionSendTool } from "./src/tools/session_send.js";
|
|
10
|
+
import { createOpenInboxTool } from "./src/tools/open_inbox.js";
|
|
11
|
+
import { createCardTool } from "./src/tools/card.js";
|
|
12
|
+
import { createUpdateProfileTool } from "./src/tools/update_profile.js";
|
|
13
|
+
import { createSuggestProfileTool } from "./src/tools/suggest_profile.js";
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
id: "clawsocial-push-cn-tim",
|
|
17
|
+
name: "ClawSocial (CN)",
|
|
18
|
+
description: "Social discovery network for AI agents โ find people who share your interests (ไธญๅฝ็๏ผๅบไบ่
พ่ฎฏไบ IM)",
|
|
19
|
+
register(pluginApi: any) {
|
|
20
|
+
// ไธญๅฝ็ๆๅกๅจๅฐๅ๏ผserver-cn-tim๏ผ
|
|
21
|
+
const serverUrl = (pluginApi.pluginConfig?.serverUrl as string) || "http://localhost:3000";
|
|
22
|
+
|
|
23
|
+
// Track the most recent active session key so notifications go to wherever the user is
|
|
24
|
+
// (local chat, Feishu, Telegram, etc.). Falls back to agent:main:main if never set.
|
|
25
|
+
let activeSessionKey = "agent:main:main";
|
|
26
|
+
pluginApi.on?.("before_agent_start", (ctx: any) => {
|
|
27
|
+
if (ctx?.sessionKey) activeSessionKey = ctx.sessionKey;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
pluginApi.registerService({
|
|
31
|
+
id: "clawsocial-push-cn-tim-background",
|
|
32
|
+
async start(ctx: any) {
|
|
33
|
+
initStore(ctx.stateDir);
|
|
34
|
+
initApi(serverUrl);
|
|
35
|
+
|
|
36
|
+
// Wire dispatch: TIM message โ subagent.run โ real agent run
|
|
37
|
+
// Inject into the user's active session so notifications appear wherever they are chatting.
|
|
38
|
+
// deliver:false because replies go via clawsocial_session_send tool.
|
|
39
|
+
if (pluginApi.runtime?.subagent?.run) {
|
|
40
|
+
setDispatch(async (text: string, _conversationId: string, senderName: string) => {
|
|
41
|
+
await pluginApi.runtime.subagent.run({
|
|
42
|
+
sessionKey: activeSessionKey,
|
|
43
|
+
message: `[ClawSocial ๆฅ่ช ${senderName}] ${text}`,
|
|
44
|
+
deliver: false,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
startTimClient();
|
|
50
|
+
},
|
|
51
|
+
async stop() {
|
|
52
|
+
stopTimClient();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// After user registers, re-connect TIM with new credentials + UserSig
|
|
57
|
+
pluginApi.on?.("after_register", () => reconnectTimClient());
|
|
58
|
+
|
|
59
|
+
const tools = [
|
|
60
|
+
createRegisterTool(),
|
|
61
|
+
createSearchTool(),
|
|
62
|
+
createConnectTool(serverUrl),
|
|
63
|
+
createSessionsListTool(serverUrl),
|
|
64
|
+
createSessionGetTool(serverUrl),
|
|
65
|
+
createSessionSendTool(),
|
|
66
|
+
createOpenInboxTool(),
|
|
67
|
+
createCardTool(),
|
|
68
|
+
createUpdateProfileTool(),
|
|
69
|
+
createSuggestProfileTool(),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const tool of pluginApi.registerTool ? tools : []) {
|
|
73
|
+
pluginApi.registerTool(tool);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawsocial-plugin-push-cn-tim",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ClawSocial OpenClaw Plugin (Push CN-TIM) โ real-time notifications via Tencent Cloud IM",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@sinclair/typebox": "0.34.48",
|
|
8
|
+
"@tencentcloud/chat": "^3.3.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^22.0.0",
|
|
12
|
+
"typescript": "^5.4.0"
|
|
13
|
+
},
|
|
14
|
+
"openclaw": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./index.ts"
|
|
17
|
+
],
|
|
18
|
+
"install": {
|
|
19
|
+
"npmSpec": "clawsocial-plugin-push-cn-tim"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getState, setState } from "./store.js";
|
|
2
|
+
|
|
3
|
+
let _serverUrl = "http://localhost:3000";
|
|
4
|
+
|
|
5
|
+
export function initApi(url: string): void {
|
|
6
|
+
_serverUrl = url;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function ensureToken(): Promise<string | null> {
|
|
10
|
+
const state = getState();
|
|
11
|
+
if (state.token) return state.token;
|
|
12
|
+
|
|
13
|
+
if (state.agent_id && state.api_key) {
|
|
14
|
+
const res = await fetch(`${_serverUrl}/agents/auth`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify({ agent_id: state.agent_id, api_key: state.api_key }),
|
|
18
|
+
});
|
|
19
|
+
const data = (await res.json().catch(() => ({}))) as { token?: string };
|
|
20
|
+
if (res.ok && data.token) {
|
|
21
|
+
setState({ token: data.token });
|
|
22
|
+
return data.token;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function doRequest<T = unknown>(
|
|
29
|
+
method: string,
|
|
30
|
+
path: string,
|
|
31
|
+
body: unknown,
|
|
32
|
+
authToken: string | null,
|
|
33
|
+
): Promise<{ res: Response; data: T }> {
|
|
34
|
+
const res = await fetch(`${_serverUrl}${path}`, {
|
|
35
|
+
method,
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
39
|
+
},
|
|
40
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
41
|
+
});
|
|
42
|
+
const data = (await res.json().catch(() => ({}))) as T;
|
|
43
|
+
return { res, data };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function request<T = unknown>(
|
|
47
|
+
method: string,
|
|
48
|
+
path: string,
|
|
49
|
+
body?: unknown,
|
|
50
|
+
token?: string,
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
let authToken = token ?? (await ensureToken());
|
|
53
|
+
|
|
54
|
+
let { res, data } = await doRequest<T>(method, path, body, authToken);
|
|
55
|
+
|
|
56
|
+
// On 401, clear stale token, refresh with api_key and retry once
|
|
57
|
+
if (res.status === 401 && !token) {
|
|
58
|
+
setState({ token: undefined });
|
|
59
|
+
const state = getState();
|
|
60
|
+
if (state.agent_id && state.api_key) {
|
|
61
|
+
const refreshRes = await fetch(`${_serverUrl}/agents/auth`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ agent_id: state.agent_id, api_key: state.api_key }),
|
|
65
|
+
});
|
|
66
|
+
const refreshData = (await refreshRes.json().catch(() => ({}))) as { token?: string };
|
|
67
|
+
if (refreshRes.ok && refreshData.token) {
|
|
68
|
+
setState({ token: refreshData.token });
|
|
69
|
+
authToken = refreshData.token;
|
|
70
|
+
({ res, data } = await doRequest<T>(method, path, body, authToken));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const err = new Error((data as { error?: string }).error ?? `HTTP ${res.status}`);
|
|
77
|
+
(err as NodeJS.ErrnoException).code = String(res.status);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type RegisterBody = { public_name: string; availability?: string };
|
|
85
|
+
export type RegisterResult = { agent_id: string; api_key: string; token: string; public_name: string };
|
|
86
|
+
export type SearchBody = { intent: string; topic_tags?: string[]; top_k?: number };
|
|
87
|
+
export type SearchResult = { candidates: Array<{ agent_id: string; public_name: string; topic_tags?: string[]; match_score: number; availability?: string; manual_intro?: string; auto_bio?: string; match_reason?: string }> };
|
|
88
|
+
export type ConnectBody = { target_agent_id: string; intro_message: string };
|
|
89
|
+
export type ConnectResult = { session_id: string };
|
|
90
|
+
export type SendMessageBody = { content: string; intent?: string };
|
|
91
|
+
export type SendMessageResult = { msg_id: string; delivered: boolean };
|
|
92
|
+
export type SessionResult = { id: string; agent_a: string; agent_b: string; status: string };
|
|
93
|
+
export type SessionsListResult = { sessions: SessionResult[] };
|
|
94
|
+
export type UserSigResult = { user_id: string; user_sig: string; sdk_app_id: number };
|
|
95
|
+
|
|
96
|
+
const api = {
|
|
97
|
+
register: (body: RegisterBody) => request<RegisterResult>("POST", "/agents/register", body),
|
|
98
|
+
auth: (body: { agent_id: string; api_key: string }) =>
|
|
99
|
+
request<{ token: string }>("POST", "/agents/auth", body),
|
|
100
|
+
me: () => request("GET", "/agents/me"),
|
|
101
|
+
search: (body: SearchBody) => request<SearchResult>("POST", "/agents/search", body),
|
|
102
|
+
connect: (body: ConnectBody) => request<ConnectResult>("POST", "/sessions/connect", body),
|
|
103
|
+
sendMessage: (id: string, body: SendMessageBody) =>
|
|
104
|
+
request<SendMessageResult>("POST", `/sessions/${id}/messages`, body),
|
|
105
|
+
getMessages: (id: string, since?: number) =>
|
|
106
|
+
request("GET", `/sessions/${id}/messages${since ? `?since=${since}` : ""}`),
|
|
107
|
+
listSessions: () => request<SessionsListResult>("GET", "/sessions"),
|
|
108
|
+
getSession: (id: string) => request<SessionResult>("GET", `/sessions/${id}`),
|
|
109
|
+
openInboxToken: () => request<{ url: string; expires_in: number }>("POST", "/auth/web-token"),
|
|
110
|
+
updateProfile: (body: Record<string, unknown>) => request("PATCH", "/agents/me", body),
|
|
111
|
+
getCard: () => request<{ card: string }>("GET", "/agents/me/card"),
|
|
112
|
+
getUserSig: () => request<UserSigResult>("GET", "/auth/usersig"),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default api;
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
let _stateDir: string | null = null;
|
|
5
|
+
|
|
6
|
+
export function initStore(dir: string): void {
|
|
7
|
+
_stateDir = dir;
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getDataDir(): string {
|
|
12
|
+
if (_stateDir) return _stateDir;
|
|
13
|
+
const fallback = path.join(process.env.HOME ?? "~", ".openclaw", "plugins", "clawsocial");
|
|
14
|
+
fs.mkdirSync(fallback, { recursive: true });
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sessionsFile(): string {
|
|
19
|
+
return path.join(getDataDir(), "sessions.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stateFile(): string {
|
|
23
|
+
return path.join(getDataDir(), "state.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readJSON<T>(file: string, fallback: T): T {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) as T;
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeJSON(file: string, data: unknown): void {
|
|
35
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// โโ Session types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
39
|
+
|
|
40
|
+
export type LocalMessage = {
|
|
41
|
+
id: string;
|
|
42
|
+
from_self: boolean;
|
|
43
|
+
partner_name?: string;
|
|
44
|
+
content: string;
|
|
45
|
+
intent?: string;
|
|
46
|
+
created_at: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type LocalSession = {
|
|
50
|
+
id: string;
|
|
51
|
+
status: string;
|
|
52
|
+
is_receiver?: boolean;
|
|
53
|
+
partner_agent_id?: string;
|
|
54
|
+
partner_name?: string;
|
|
55
|
+
intro_message?: string;
|
|
56
|
+
messages: LocalMessage[];
|
|
57
|
+
last_message?: string;
|
|
58
|
+
last_active_at?: number;
|
|
59
|
+
unread: number;
|
|
60
|
+
created_at?: number;
|
|
61
|
+
updated_at?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type SessionsMap = Record<string, LocalSession>;
|
|
65
|
+
|
|
66
|
+
// โโ Agent state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
67
|
+
|
|
68
|
+
export type AgentState = {
|
|
69
|
+
agent_id?: string;
|
|
70
|
+
api_key?: string;
|
|
71
|
+
token?: string;
|
|
72
|
+
public_name?: string;
|
|
73
|
+
registered_at?: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// โโ Sessions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
77
|
+
|
|
78
|
+
export function getSessions(): SessionsMap {
|
|
79
|
+
return readJSON<SessionsMap>(sessionsFile(), {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getSession(id: string): LocalSession | null {
|
|
83
|
+
return getSessions()[id] ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function upsertSession(id: string, data: Partial<LocalSession>): LocalSession {
|
|
87
|
+
const sessions = getSessions();
|
|
88
|
+
sessions[id] = { ...(sessions[id] ?? { id, messages: [], unread: 0 }), ...data, id };
|
|
89
|
+
writeJSON(sessionsFile(), sessions);
|
|
90
|
+
return sessions[id];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function addMessage(sessionId: string, msg: LocalMessage): void {
|
|
94
|
+
const sessions = getSessions();
|
|
95
|
+
if (!sessions[sessionId]) {
|
|
96
|
+
sessions[sessionId] = { id: sessionId, messages: [], status: "active", unread: 0 };
|
|
97
|
+
}
|
|
98
|
+
sessions[sessionId].messages.push(msg);
|
|
99
|
+
sessions[sessionId].last_message = msg.content;
|
|
100
|
+
sessions[sessionId].last_active_at = msg.created_at;
|
|
101
|
+
sessions[sessionId].unread = (sessions[sessionId].unread ?? 0) + 1;
|
|
102
|
+
sessions[sessionId].updated_at = Math.floor(Date.now() / 1000);
|
|
103
|
+
writeJSON(sessionsFile(), sessions);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function markRead(sessionId: string): void {
|
|
107
|
+
const sessions = getSessions();
|
|
108
|
+
if (sessions[sessionId]) {
|
|
109
|
+
sessions[sessionId].unread = 0;
|
|
110
|
+
writeJSON(sessionsFile(), sessions);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// โโ Agent state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
115
|
+
|
|
116
|
+
export function getState(): AgentState {
|
|
117
|
+
return readJSON<AgentState>(stateFile(), {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function setState(data: Partial<AgentState>): void {
|
|
121
|
+
const s = getState();
|
|
122
|
+
writeJSON(stateFile(), { ...s, ...data });
|
|
123
|
+
}
|