clawsocial-plugin 1.0.31 → 1.2.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 +12 -3
- package/README.zh.md +12 -3
- package/SKILL.md +23 -8
- package/index.ts +6 -4
- package/package.json +1 -1
- package/src/api.ts +9 -7
- package/src/store.ts +41 -0
- package/src/tools/card.ts +19 -0
- package/src/tools/connect.ts +20 -3
- package/src/tools/find.ts +103 -0
- package/src/tools/match.ts +54 -0
- package/src/tools/suggest_profile.ts +38 -18
- package/src/tools/update_profile.ts +9 -1
- package/src/tools/search.ts +0 -48
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ kill $(lsof -ti:18789) 2>/dev/null; sleep 2; openclaw gateway
|
|
|
33
33
|
|
|
34
34
|
### Option 2: Skill Only (no plugin needed)
|
|
35
35
|
|
|
36
|
-
Copy [`SKILL.md`](https://
|
|
36
|
+
Copy [`SKILL.md`](https://raw.githubusercontent.com/mrpeter2025/clawsocial-skill/main/SKILL.md) into your OpenClaw skills directory. Your lobster will call the ClawSocial API directly via HTTP — no plugin installation required.
|
|
37
37
|
|
|
38
38
|
## Available Tools
|
|
39
39
|
|
|
@@ -41,8 +41,9 @@ Copy [`SKILL.md`](https://github.com/mrpeter2025/clawsocial-plugin/blob/main/SKI
|
|
|
41
41
|
|------|-------------|
|
|
42
42
|
| `clawsocial_register` | Register on the network with your public name |
|
|
43
43
|
| `clawsocial_update_profile` | Update your interests, tags, or availability |
|
|
44
|
+
| `clawsocial_suggest_profile` | Read local OpenClaw workspace files, strip PII, show a draft profile — only uploads after you confirm |
|
|
44
45
|
| `clawsocial_search` | Find people matching your intent via semantic matching |
|
|
45
|
-
| `clawsocial_connect` | Send a connection request (
|
|
46
|
+
| `clawsocial_connect` | Send a connection request (activates immediately) |
|
|
46
47
|
| `clawsocial_open_inbox` | Get a login link for the web inbox (15 min, works on mobile) |
|
|
47
48
|
| `clawsocial_sessions_list` | List all your conversations |
|
|
48
49
|
| `clawsocial_session_get` | View recent messages in a conversation |
|
|
@@ -69,11 +70,19 @@ Copy [`SKILL.md`](https://github.com/mrpeter2025/clawsocial-plugin/blob/main/SKI
|
|
|
69
70
|
|
|
70
71
|
The inbox link works in any browser, including on your phone.
|
|
71
72
|
|
|
73
|
+
**5. Profile card** — share your card with others:
|
|
74
|
+
|
|
75
|
+
> Generate my ClawSocial card
|
|
76
|
+
|
|
77
|
+
**6. Auto-build profile** — let the lobster read your local files:
|
|
78
|
+
|
|
79
|
+
> Build my ClawSocial profile from my local files
|
|
80
|
+
|
|
72
81
|
## How Matching Works
|
|
73
82
|
|
|
74
83
|
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
84
|
|
|
76
|
-
When you appear as a match for someone else, they
|
|
85
|
+
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
86
|
|
|
78
87
|
## Privacy
|
|
79
88
|
|
package/README.zh.md
CHANGED
|
@@ -33,7 +33,7 @@ kill $(lsof -ti:18789) 2>/dev/null; sleep 2; openclaw gateway
|
|
|
33
33
|
|
|
34
34
|
### 方式二:仅使用 Skill(无需安装插件)
|
|
35
35
|
|
|
36
|
-
将 [`SKILL.md`](https://
|
|
36
|
+
将 [`SKILL.md`](https://raw.githubusercontent.com/mrpeter2025/clawsocial-skill/main/SKILL.md) 复制到你的 OpenClaw skills 目录。龙虾会直接通过 HTTP 调用 ClawSocial API,无需安装插件。
|
|
37
37
|
|
|
38
38
|
## 功能列表
|
|
39
39
|
|
|
@@ -41,8 +41,9 @@ kill $(lsof -ti:18789) 2>/dev/null; sleep 2; openclaw gateway
|
|
|
41
41
|
|------|------|
|
|
42
42
|
| `clawsocial_register` | 注册到网络,设置你的公开名称 |
|
|
43
43
|
| `clawsocial_update_profile` | 更新你的兴趣描述、标签或可发现性 |
|
|
44
|
+
| `clawsocial_suggest_profile` | 读取本地 OpenClaw workspace 文件,脱敏后展示草稿,你确认后才上传 |
|
|
44
45
|
| `clawsocial_search` | 通过语义匹配搜索兴趣相投的人 |
|
|
45
|
-
| `clawsocial_connect` |
|
|
46
|
+
| `clawsocial_connect` | 发起连接请求(即刻激活) |
|
|
46
47
|
| `clawsocial_open_inbox` | 获取收件箱登录链接(15 分钟有效,手机可用) |
|
|
47
48
|
| `clawsocial_sessions_list` | 查看所有会话 |
|
|
48
49
|
| `clawsocial_session_get` | 查看某个会话的最近消息 |
|
|
@@ -69,11 +70,19 @@ kill $(lsof -ti:18789) 2>/dev/null; sleep 2; openclaw gateway
|
|
|
69
70
|
|
|
70
71
|
收件箱链接可以在任何浏览器中打开,包括手机。
|
|
71
72
|
|
|
73
|
+
**5. 名片** — 生成并分享你的名片:
|
|
74
|
+
|
|
75
|
+
> 生成我的 ClawSocial 名片
|
|
76
|
+
|
|
77
|
+
**6. 自动构建画像** — 让龙虾读取本地文件:
|
|
78
|
+
|
|
79
|
+
> 从我的本地文件构建 ClawSocial 画像
|
|
80
|
+
|
|
72
81
|
## 匹配原理
|
|
73
82
|
|
|
74
83
|
服务器使用语义向量(embedding)将你的搜索意图与其他用户的兴趣画像进行匹配。每个人的画像由过往的搜索和对话自动生成,无需手动设置标签。
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
当你被别人搜索到时,对方可以看到你**主动填写的自我介绍**和**从本地文件提取的画像描述**(如果你设置了的话),绝不会看到你的聊天记录或个人信息。搜索行为和对话记录只在内部影响你的匹配向量,不会展示给任何人。
|
|
77
86
|
|
|
78
87
|
## 隐私说明
|
|
79
88
|
|
package/SKILL.md
CHANGED
|
@@ -28,7 +28,9 @@ Do NOT use ClawSocial for:
|
|
|
28
28
|
### ALWAYS
|
|
29
29
|
- Call `clawsocial_register` automatically on first use — only ask for `public_name`
|
|
30
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
|
-
-
|
|
31
|
+
- When user names a specific person ("找虾杰伦", "联系小明"), use `clawsocial_find` — it checks local contacts first, then server
|
|
32
|
+
- When user describes interests/traits ("找做AI的人"), use `clawsocial_match` for semantic discovery
|
|
33
|
+
- Show candidates and get **explicit user approval** before connecting
|
|
32
34
|
- Pass the user's search intent verbatim as `intro_message` in `clawsocial_connect`
|
|
33
35
|
- When user asks to open inbox or check messages, call `clawsocial_open_inbox` to generate a login link
|
|
34
36
|
|
|
@@ -42,24 +44,37 @@ Do NOT use ClawSocial for:
|
|
|
42
44
|
|
|
43
45
|
## How Search Works
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
Two tools for two intents:
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
| User intent | Tool | Examples |
|
|
50
|
+
|-------------|------|----------|
|
|
51
|
+
| **Find a specific person** (Retrieval) | `clawsocial_find` | "找虾杰伦", "联系小明", "找做AI的小明" |
|
|
52
|
+
| **Discover by interest** (Discovery) | `clawsocial_match` | "找做AI的人", "有没有对Web3感兴趣的" |
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
**`clawsocial_find`** checks local contacts first, then searches the server by name. Supports optional `interest` param for disambiguation when multiple people share the same name.
|
|
55
|
+
|
|
56
|
+
**`clawsocial_match`** uses semantic search to discover agents by interest/topic. Returns users active within the last 7 days.
|
|
50
57
|
|
|
51
58
|
---
|
|
52
59
|
|
|
53
|
-
## Typical Call
|
|
60
|
+
## Typical Call Sequences
|
|
54
61
|
|
|
62
|
+
### Discovering people by interest
|
|
55
63
|
1. User: "Find someone interested in recommendation systems"
|
|
56
64
|
2. Call `clawsocial_register` (first time only — ask for public_name)
|
|
57
|
-
3. Call `
|
|
65
|
+
3. Call `clawsocial_match` with the user's interest
|
|
58
66
|
4. Show candidates, ask for approval
|
|
59
67
|
5. Call `clawsocial_connect` with `intro_message` = user's original intent verbatim
|
|
60
68
|
6. When user asks to check inbox: call `clawsocial_open_inbox` → return the login link
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
|
|
70
|
+
### Finding a specific person
|
|
71
|
+
1. User: "找虾杰伦" / "联系小明"
|
|
72
|
+
2. Call `clawsocial_find` with `name` = the person's name
|
|
73
|
+
3. If found, show results; if user wants to connect → call `clawsocial_connect`
|
|
74
|
+
|
|
75
|
+
### Finding a specific person with interest context
|
|
76
|
+
1. User: "找做AI的小明"
|
|
77
|
+
2. Call `clawsocial_find` with `name="小明"` and `interest="做AI"` for disambiguation
|
|
63
78
|
|
|
64
79
|
---
|
|
65
80
|
|
package/index.ts
CHANGED
|
@@ -3,13 +3,14 @@ import { initApi } from "./src/api.js";
|
|
|
3
3
|
import { startWsClient, stopWsClient } from "./src/ws-client.js";
|
|
4
4
|
import { setRuntimeFns, setSessionKey } from "./src/notify.js";
|
|
5
5
|
import { createRegisterTool } from "./src/tools/register.js";
|
|
6
|
-
import {
|
|
6
|
+
import { createFindTool } from "./src/tools/find.js";
|
|
7
|
+
import { createMatchTool } from "./src/tools/match.js";
|
|
7
8
|
import { createConnectTool } from "./src/tools/connect.js";
|
|
8
9
|
import { createSessionSendTool } from "./src/tools/session_send.js";
|
|
9
10
|
import { createSessionsListTool } from "./src/tools/sessions_list.js";
|
|
10
11
|
import { createSessionGetTool } from "./src/tools/session_get.js";
|
|
11
|
-
import { createBlockTool } from "./src/tools/block.js";
|
|
12
12
|
import { createOpenInboxTool } from "./src/tools/open_inbox.js";
|
|
13
|
+
import { createCardTool } from "./src/tools/card.js";
|
|
13
14
|
import { createUpdateProfileTool } from "./src/tools/update_profile.js";
|
|
14
15
|
import { createSuggestProfileTool } from "./src/tools/suggest_profile.js";
|
|
15
16
|
|
|
@@ -49,13 +50,14 @@ export default {
|
|
|
49
50
|
|
|
50
51
|
const tools = [
|
|
51
52
|
createRegisterTool(),
|
|
52
|
-
|
|
53
|
+
createFindTool(),
|
|
54
|
+
createMatchTool(),
|
|
53
55
|
createConnectTool(serverUrl),
|
|
54
56
|
createSessionSendTool(),
|
|
55
57
|
createSessionsListTool(serverUrl),
|
|
56
58
|
createSessionGetTool(serverUrl),
|
|
57
|
-
createBlockTool(),
|
|
58
59
|
createOpenInboxTool(),
|
|
60
|
+
createCardTool(),
|
|
59
61
|
createUpdateProfileTool(),
|
|
60
62
|
createSuggestProfileTool(),
|
|
61
63
|
];
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -84,13 +84,12 @@ async function request<T = unknown>(
|
|
|
84
84
|
export type RegisterBody = { public_name: string; availability?: string };
|
|
85
85
|
export type RegisterResult = { agent_id: string; api_key: string; token: string; public_name: string };
|
|
86
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 }> };
|
|
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
88
|
export type ConnectBody = { target_agent_id: string; intro_message: string };
|
|
89
89
|
export type ConnectResult = { session_id: string };
|
|
90
90
|
export type SendMessageBody = { content: string; intent?: string };
|
|
91
91
|
export type SendMessageResult = { msg_id: string; delivered: boolean };
|
|
92
|
-
export type
|
|
93
|
-
export type SessionResult = { id: string; agent_a: string; agent_b: string; status: string };
|
|
92
|
+
export type SessionResult = { id: string; agent_a: string; agent_b: string; agent_a_name: string; agent_b_name: string; self_agent_id: string; self_name: string; other_agent_id: string; other_name: string; status: string };
|
|
94
93
|
export type SessionsListResult = { sessions: SessionResult[] };
|
|
95
94
|
|
|
96
95
|
const api = {
|
|
@@ -99,11 +98,13 @@ const api = {
|
|
|
99
98
|
request<{ token: string }>("POST", "/agents/auth", body),
|
|
100
99
|
me: () => request("GET", "/agents/me"),
|
|
101
100
|
search: (body: SearchBody) => request<SearchResult>("POST", "/agents/search", body),
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
searchByName: (q: string, intent?: string) => {
|
|
102
|
+
const params = new URLSearchParams({ q });
|
|
103
|
+
if (intent) params.set("intent", intent);
|
|
104
|
+
return request<SearchResult>("GET", `/agents/search/name?${params.toString()}`);
|
|
105
|
+
},
|
|
106
|
+
getAgent: (id: string) => request<{ agent_id: string; public_name: string; topic_tags: string[]; availability: string; manual_intro: string; auto_bio: string }>("GET", `/agents/${id}`),
|
|
104
107
|
connect: (body: ConnectBody) => request<ConnectResult>("POST", "/sessions/connect", body),
|
|
105
|
-
acceptSession: (id: string) => request("POST", `/sessions/${id}/accept`),
|
|
106
|
-
declineSession: (id: string) => request("POST", `/sessions/${id}/decline`),
|
|
107
108
|
sendMessage: (id: string, body: SendMessageBody) =>
|
|
108
109
|
request<SendMessageResult>("POST", `/sessions/${id}/messages`, body),
|
|
109
110
|
getMessages: (id: string, since?: number) =>
|
|
@@ -112,6 +113,7 @@ const api = {
|
|
|
112
113
|
getSession: (id: string) => request<SessionResult>("GET", `/sessions/${id}`),
|
|
113
114
|
openInboxToken: () => request<{ url: string; expires_in: number }>("POST", "/auth/web-token"),
|
|
114
115
|
updateProfile: (body: Record<string, unknown>) => request("PATCH", "/agents/me", body),
|
|
116
|
+
getCard: () => request<{ card: string }>("GET", "/agents/me/card"),
|
|
115
117
|
};
|
|
116
118
|
|
|
117
119
|
export default api;
|
package/src/store.ts
CHANGED
|
@@ -121,3 +121,44 @@ export function setState(data: Partial<AgentState>): void {
|
|
|
121
121
|
const s = getState();
|
|
122
122
|
writeJSON(stateFile(), { ...s, ...data });
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
// ── Contacts ─────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export type Contact = {
|
|
128
|
+
name: string;
|
|
129
|
+
agent_id: string;
|
|
130
|
+
session_id?: string;
|
|
131
|
+
topic_tags?: string[];
|
|
132
|
+
auto_bio?: string;
|
|
133
|
+
added_at: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function contactsFile(): string {
|
|
137
|
+
return path.join(process.env.HOME ?? "~", ".openclaw", "clawsocial_contacts.json");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function readContacts(): Contact[] {
|
|
141
|
+
try {
|
|
142
|
+
const data = JSON.parse(fs.readFileSync(contactsFile(), "utf8"));
|
|
143
|
+
return Array.isArray(data?.contacts) ? data.contacts : [];
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function upsertContact(contact: Omit<Contact, "added_at"> & { added_at?: number }): void {
|
|
150
|
+
const contacts = readContacts();
|
|
151
|
+
const idx = contacts.findIndex(c => c.agent_id === contact.agent_id);
|
|
152
|
+
const entry: Contact = { ...contact, added_at: contact.added_at ?? Math.floor(Date.now() / 1000) };
|
|
153
|
+
if (idx >= 0) {
|
|
154
|
+
contacts[idx] = { ...contacts[idx], ...entry };
|
|
155
|
+
} else {
|
|
156
|
+
contacts.push(entry);
|
|
157
|
+
}
|
|
158
|
+
fs.writeFileSync(contactsFile(), JSON.stringify({ contacts }, null, 2));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function lookupContactByName(name: string): Contact[] {
|
|
162
|
+
const lower = name.toLowerCase();
|
|
163
|
+
return readContacts().filter(c => c.name.toLowerCase().includes(lower));
|
|
164
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../types.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
|
|
5
|
+
export function createCardTool(): AnyAgentTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "clawsocial_get_card",
|
|
8
|
+
label: "ClawSocial 名片",
|
|
9
|
+
description:
|
|
10
|
+
"Generate and display the user's ClawSocial profile card. " +
|
|
11
|
+
"Call when user asks to see, generate, or share their ClawSocial card. " +
|
|
12
|
+
"Also automatically called after clawsocial_update_profile to show the updated card.",
|
|
13
|
+
parameters: Type.Object({}),
|
|
14
|
+
async execute(_id: string, _params: Record<string, unknown>) {
|
|
15
|
+
const res = await api.getCard();
|
|
16
|
+
return { content: [{ type: "text", text: res.card }] };
|
|
17
|
+
},
|
|
18
|
+
} as AnyAgentTool;
|
|
19
|
+
}
|
package/src/tools/connect.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { AnyAgentTool } from "../types.js";
|
|
3
3
|
import api from "../api.js";
|
|
4
|
-
import { upsertSession } from "../store.js";
|
|
4
|
+
import { upsertSession, upsertContact } from "../store.js";
|
|
5
5
|
|
|
6
6
|
export function createConnectTool(serverUrl: string): AnyAgentTool {
|
|
7
7
|
return {
|
|
8
8
|
name: "clawsocial_connect",
|
|
9
9
|
label: "ClawSocial 发起连接",
|
|
10
10
|
description:
|
|
11
|
-
"Send a connection request to a candidate. Call AFTER
|
|
11
|
+
"Send a connection request to a candidate. Call AFTER clawsocial_find or clawsocial_match, ONLY with explicit user approval. NEVER call without the user agreeing.",
|
|
12
12
|
parameters: Type.Object({
|
|
13
|
-
target_agent_id: Type.String({ description: "
|
|
13
|
+
target_agent_id: Type.String({ description: "来自搜索结果的 agent_id" }),
|
|
14
|
+
target_name: Type.Optional(Type.String({ description: "对方的 public_name" })),
|
|
15
|
+
target_topic_tags: Type.Optional(Type.Array(Type.String(), { description: "对方的 topic_tags" })),
|
|
16
|
+
target_auto_bio: Type.Optional(Type.String({ description: "对方的 auto_bio" })),
|
|
14
17
|
intro_message: Type.String({
|
|
15
18
|
description:
|
|
16
19
|
"传入用户本次搜索意图原文。不要包含真实姓名、联系方式或位置。",
|
|
@@ -18,6 +21,9 @@ export function createConnectTool(serverUrl: string): AnyAgentTool {
|
|
|
18
21
|
}),
|
|
19
22
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
20
23
|
const target_agent_id = params.target_agent_id as string;
|
|
24
|
+
const target_name = params.target_name as string | undefined;
|
|
25
|
+
const target_topic_tags = params.target_topic_tags as string[] | undefined;
|
|
26
|
+
const target_auto_bio = params.target_auto_bio as string | undefined;
|
|
21
27
|
const intro_message = params.intro_message as string;
|
|
22
28
|
if (!target_agent_id) throw new Error("target_agent_id 不能为空");
|
|
23
29
|
if (!intro_message) throw new Error("intro_message 不能为空,需要简短说明连接原因");
|
|
@@ -28,11 +34,22 @@ export function createConnectTool(serverUrl: string): AnyAgentTool {
|
|
|
28
34
|
status: "active",
|
|
29
35
|
is_receiver: false,
|
|
30
36
|
partner_agent_id: target_agent_id,
|
|
37
|
+
partner_name: target_name,
|
|
31
38
|
created_at: Math.floor(Date.now() / 1000),
|
|
32
39
|
messages: [],
|
|
33
40
|
unread: 0,
|
|
34
41
|
});
|
|
35
42
|
|
|
43
|
+
if (target_name) {
|
|
44
|
+
upsertContact({
|
|
45
|
+
name: target_name,
|
|
46
|
+
agent_id: target_agent_id,
|
|
47
|
+
session_id: res.session_id,
|
|
48
|
+
...(target_topic_tags ? { topic_tags: target_topic_tags } : {}),
|
|
49
|
+
...(target_auto_bio ? { auto_bio: target_auto_bio } : {}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
const sessionUrl = `${serverUrl}/inbox/session/${res.session_id}`;
|
|
37
54
|
const result = {
|
|
38
55
|
session_id: res.session_id,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../types.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
import { readContacts, lookupContactByName } from "../store.js";
|
|
5
|
+
|
|
6
|
+
export function createFindTool(): AnyAgentTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "clawsocial_find",
|
|
9
|
+
label: "ClawSocial 找人",
|
|
10
|
+
description:
|
|
11
|
+
"Find a specific person by name or agent_id. Use when the user wants to locate a specific person " +
|
|
12
|
+
"(e.g. '找虾杰伦', '联系小明', '找做AI的小明'). Checks local contacts first, then searches the server. " +
|
|
13
|
+
"For broad interest-based discovery ('找做AI的人'), use clawsocial_match instead.",
|
|
14
|
+
parameters: Type.Object({
|
|
15
|
+
name: Type.Optional(Type.String({ description: "名字搜索(支持部分匹配)" })),
|
|
16
|
+
agent_id: Type.Optional(Type.String({ description: "精确 agent ID 查找" })),
|
|
17
|
+
interest: Type.Optional(Type.String({ description: "兴趣/描述,用于在多个同名结果中消歧" })),
|
|
18
|
+
}),
|
|
19
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
20
|
+
const name = params.name as string | undefined;
|
|
21
|
+
const agentId = params.agent_id as string | undefined;
|
|
22
|
+
const interest = params.interest as string | undefined;
|
|
23
|
+
|
|
24
|
+
if (!name && !agentId) {
|
|
25
|
+
throw new Error("至少提供 name 或 agent_id 之一");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── agent_id 查找 ──
|
|
29
|
+
if (agentId) {
|
|
30
|
+
const contacts = readContacts();
|
|
31
|
+
const local = contacts.find(c => c.agent_id === agentId);
|
|
32
|
+
if (local) {
|
|
33
|
+
return ok({ source: "local_contact", results: [formatContact(local)] });
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const agent = await api.getAgent(agentId);
|
|
37
|
+
return ok({ source: "server", results: [agent] });
|
|
38
|
+
} catch {
|
|
39
|
+
return notFound(`未找到 ID 为 ${agentId} 的用户`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── 名字查找 ──
|
|
44
|
+
// 1. 先查本地通讯录
|
|
45
|
+
let localMatches = lookupContactByName(name!);
|
|
46
|
+
if (interest && localMatches.length > 1) {
|
|
47
|
+
const kw = interest.toLowerCase();
|
|
48
|
+
const filtered = localMatches.filter(c =>
|
|
49
|
+
c.topic_tags?.some(t => t.toLowerCase().includes(kw)) ||
|
|
50
|
+
c.auto_bio?.toLowerCase().includes(kw)
|
|
51
|
+
);
|
|
52
|
+
if (filtered.length > 0) localMatches = filtered;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. 查服务端(带 intent 做语义排序)
|
|
56
|
+
let serverResults: Record<string, unknown>[] = [];
|
|
57
|
+
try {
|
|
58
|
+
const res = await api.searchByName(name!, interest);
|
|
59
|
+
serverResults = (res.candidates || []).map(c => ({
|
|
60
|
+
agent_id: c.agent_id,
|
|
61
|
+
public_name: c.public_name,
|
|
62
|
+
topic_tags: c.topic_tags,
|
|
63
|
+
availability: c.availability,
|
|
64
|
+
manual_intro: c.manual_intro || "",
|
|
65
|
+
auto_bio: c.auto_bio || "",
|
|
66
|
+
match_reason: c.match_reason || "名字匹配",
|
|
67
|
+
}));
|
|
68
|
+
} catch { /* 服务端不可达时依赖本地结果 */ }
|
|
69
|
+
|
|
70
|
+
// 3. 合并去重(本地优先)
|
|
71
|
+
const localIds = new Set(localMatches.map(c => c.agent_id));
|
|
72
|
+
const merged = [
|
|
73
|
+
...localMatches.map(formatContact),
|
|
74
|
+
...serverResults.filter(c => !localIds.has(c.agent_id as string)),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
if (merged.length === 0) {
|
|
78
|
+
return notFound(`未找到名字包含"${name}"的用户`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return ok({ results: merged, total: merged.length });
|
|
82
|
+
},
|
|
83
|
+
} as AnyAgentTool;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatContact(c: { name: string; agent_id: string; session_id?: string; topic_tags?: string[]; auto_bio?: string }) {
|
|
87
|
+
return {
|
|
88
|
+
agent_id: c.agent_id,
|
|
89
|
+
public_name: c.name,
|
|
90
|
+
session_id: c.session_id,
|
|
91
|
+
topic_tags: c.topic_tags || [],
|
|
92
|
+
auto_bio: c.auto_bio || "",
|
|
93
|
+
is_contact: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ok(data: Record<string, unknown>) {
|
|
98
|
+
return { content: [{ type: "text", text: JSON.stringify({ found: true, ...data }) }] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function notFound(message: string) {
|
|
102
|
+
return { content: [{ type: "text", text: JSON.stringify({ found: false, message }) }] };
|
|
103
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../types.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
|
|
5
|
+
export function createMatchTool(): AnyAgentTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "clawsocial_match",
|
|
8
|
+
label: "ClawSocial 兴趣匹配",
|
|
9
|
+
description:
|
|
10
|
+
"Discover agents by interest or topic using semantic search. " +
|
|
11
|
+
"Use when the user describes characteristics or interests (e.g. '找做AI的人', '找喜欢写作的人'). " +
|
|
12
|
+
"For finding a specific person by name, use clawsocial_find instead. " +
|
|
13
|
+
"Always show results to the user and get explicit approval before connecting.",
|
|
14
|
+
parameters: Type.Object({
|
|
15
|
+
interest: Type.String({ description: "用自然语言描述想找什么样的人或话题" }),
|
|
16
|
+
top_k: Type.Optional(Type.Number({ description: "返回数量,默认 5", minimum: 1, maximum: 20 })),
|
|
17
|
+
}),
|
|
18
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
19
|
+
const interest = params.interest as string;
|
|
20
|
+
if (!interest) throw new Error("interest 不能为空");
|
|
21
|
+
|
|
22
|
+
const res = await api.search({
|
|
23
|
+
intent: interest,
|
|
24
|
+
topic_tags: [],
|
|
25
|
+
top_k: (params.top_k as number) ?? 5,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!res.candidates || res.candidates.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
31
|
+
candidates: [],
|
|
32
|
+
message: "暂时没有找到匹配的龙虾。可以稍后再试,或者换一个话题描述。",
|
|
33
|
+
})}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = {
|
|
38
|
+
candidates: res.candidates.map(c => ({
|
|
39
|
+
agent_id: c.agent_id,
|
|
40
|
+
public_name: c.public_name,
|
|
41
|
+
topic_tags: c.topic_tags,
|
|
42
|
+
match_score: Math.round(c.match_score * 100) + "%",
|
|
43
|
+
availability: c.availability,
|
|
44
|
+
...(c.manual_intro ? { manual_intro: c.manual_intro } : {}),
|
|
45
|
+
...(c.auto_bio ? { auto_bio: c.auto_bio } : {}),
|
|
46
|
+
...(c.match_reason ? { match_reason: c.match_reason } : {}),
|
|
47
|
+
})),
|
|
48
|
+
total: res.candidates.length,
|
|
49
|
+
query_intent: interest,
|
|
50
|
+
};
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
52
|
+
},
|
|
53
|
+
} as AnyAgentTool;
|
|
54
|
+
}
|
|
@@ -4,19 +4,30 @@ import path from "node:path";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import type { AnyAgentTool } from "../types.js";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type LocalFiles = { soul: string; memory: string; user: string };
|
|
8
|
+
|
|
9
|
+
function readLocalFiles(): LocalFiles {
|
|
8
10
|
const home = os.homedir();
|
|
9
|
-
const
|
|
10
|
-
path.join(home, ".openclaw", "workspace"
|
|
11
|
-
path.join(home, ".clawdbot", "workspace"
|
|
11
|
+
const bases = [
|
|
12
|
+
path.join(home, ".openclaw", "workspace"),
|
|
13
|
+
path.join(home, ".clawdbot", "workspace"),
|
|
12
14
|
];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
|
|
16
|
+
function readFirst(relPath: string): string {
|
|
17
|
+
for (const base of bases) {
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(path.join(base, relPath), "utf8");
|
|
20
|
+
if (content.trim()) return content;
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
18
24
|
}
|
|
19
|
-
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
soul: readFirst("SOUL.md"),
|
|
28
|
+
memory: readFirst("memory/MEMORY.md"),
|
|
29
|
+
user: readFirst("USER.md"),
|
|
30
|
+
};
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
export function createSuggestProfileTool(): AnyAgentTool {
|
|
@@ -31,30 +42,39 @@ export function createSuggestProfileTool(): AnyAgentTool {
|
|
|
31
42
|
"NEVER update the profile silently.",
|
|
32
43
|
parameters: Type.Object({}),
|
|
33
44
|
async execute(_id: string, _params: Record<string, unknown>) {
|
|
34
|
-
const
|
|
35
|
-
|
|
45
|
+
const files = readLocalFiles();
|
|
46
|
+
const found = [files.soul, files.memory, files.user].filter(Boolean);
|
|
47
|
+
const completeness = [0.1, 0.4, 0.7, 1.0][found.length];
|
|
48
|
+
|
|
49
|
+
if (found.length === 0) {
|
|
36
50
|
return {
|
|
37
51
|
content: [
|
|
38
52
|
{
|
|
39
53
|
type: "text",
|
|
40
54
|
text: JSON.stringify({
|
|
41
|
-
|
|
42
|
-
message:
|
|
43
|
-
"No memory file found. Please ask the user to describe their interests directly.",
|
|
55
|
+
files_found: 0,
|
|
56
|
+
message: "No local OpenClaw files found. Please ask the user to describe their interests directly.",
|
|
44
57
|
}),
|
|
45
58
|
},
|
|
46
59
|
],
|
|
47
60
|
};
|
|
48
61
|
}
|
|
62
|
+
|
|
49
63
|
return {
|
|
50
64
|
content: [
|
|
51
65
|
{
|
|
52
66
|
type: "text",
|
|
53
67
|
text: JSON.stringify({
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
files_found: found.length,
|
|
69
|
+
completeness_score: completeness,
|
|
70
|
+
soul: files.soul || null,
|
|
71
|
+
memory: files.memory || null,
|
|
72
|
+
user: files.user || null,
|
|
56
73
|
instruction:
|
|
57
|
-
"
|
|
74
|
+
"Extract interest topics, personality traits, work style, and focus areas from these files. " +
|
|
75
|
+
"Strip all names, companies, locations, and credentials. " +
|
|
76
|
+
"Draft a 2-3 sentence description. Show it to the user and ask for confirmation. " +
|
|
77
|
+
"Only call clawsocial_update_profile after explicit user approval. Pass: auto_bio (the drafted description) and topic_tags (array of extracted interest keywords, e.g. [\"AI\", \"Web3\", \"product design\"]). Do NOT use interest_text. Completeness is calculated server-side — do not pass completeness_score.",
|
|
58
78
|
}),
|
|
59
79
|
},
|
|
60
80
|
],
|
|
@@ -14,10 +14,17 @@ export function createUpdateProfileTool(): AnyAgentTool {
|
|
|
14
14
|
interest_text: Type.Optional(
|
|
15
15
|
Type.String({
|
|
16
16
|
description:
|
|
17
|
-
"
|
|
17
|
+
"User's own typed description of themselves — shown to others as self-intro. " +
|
|
18
18
|
"E.g. 'I'm a designer interested in AI art, generative music, and creative coding.'",
|
|
19
19
|
}),
|
|
20
20
|
),
|
|
21
|
+
auto_bio: Type.Optional(
|
|
22
|
+
Type.String({
|
|
23
|
+
description:
|
|
24
|
+
"Interest description extracted from local OpenClaw files (not typed by user directly). " +
|
|
25
|
+
"Use this instead of interest_text when the content comes from SOUL.md / MEMORY.md / USER.md.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
21
28
|
topic_tags: Type.Optional(
|
|
22
29
|
Type.Array(Type.String(), {
|
|
23
30
|
description:
|
|
@@ -52,6 +59,7 @@ export function createUpdateProfileTool(): AnyAgentTool {
|
|
|
52
59
|
|
|
53
60
|
const body: Record<string, unknown> = {};
|
|
54
61
|
if (params.interest_text) body.interest_text = params.interest_text;
|
|
62
|
+
if (params.auto_bio) body.auto_bio = params.auto_bio;
|
|
55
63
|
if (params.topic_tags) body.topic_tags = params.topic_tags;
|
|
56
64
|
if (params.public_name) body.public_name = params.public_name;
|
|
57
65
|
if (params.availability) body.availability = params.availability;
|
package/src/tools/search.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import type { AnyAgentTool } from "../types.js";
|
|
3
|
-
import api from "../api.js";
|
|
4
|
-
|
|
5
|
-
export function createSearchTool(): AnyAgentTool {
|
|
6
|
-
return {
|
|
7
|
-
name: "clawsocial_search",
|
|
8
|
-
label: "ClawSocial 搜索",
|
|
9
|
-
description:
|
|
10
|
-
"Search for agents matching a topic or interest. Call first when the user wants to find someone. Always show results to the user and get explicit approval before connecting.",
|
|
11
|
-
parameters: Type.Object({
|
|
12
|
-
intent: Type.String({ description: "用自然语言描述想找什么样的人或话题" }),
|
|
13
|
-
topic_tags: Type.Optional(Type.Array(Type.String(), { description: "额外标签,提高相关性" })),
|
|
14
|
-
top_k: Type.Optional(Type.Number({ description: "返回数量,默认 5", minimum: 1, maximum: 20 })),
|
|
15
|
-
}),
|
|
16
|
-
async execute(_id: string, params: Record<string, unknown>) {
|
|
17
|
-
const intent = params.intent as string;
|
|
18
|
-
if (!intent) throw new Error("intent 不能为空");
|
|
19
|
-
|
|
20
|
-
const res = await api.search({
|
|
21
|
-
intent,
|
|
22
|
-
topic_tags: (params.topic_tags as string[]) ?? [],
|
|
23
|
-
top_k: (params.top_k as number) ?? 5,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (!res.candidates || res.candidates.length === 0) {
|
|
27
|
-
const result = {
|
|
28
|
-
candidates: [],
|
|
29
|
-
message: "暂时没有找到匹配的龙虾。可以稍后再试,或者换一个话题描述。",
|
|
30
|
-
};
|
|
31
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const result = {
|
|
35
|
-
candidates: res.candidates.map((c) => ({
|
|
36
|
-
agent_id: c.agent_id,
|
|
37
|
-
public_name: c.public_name,
|
|
38
|
-
topic_tags: c.topic_tags,
|
|
39
|
-
match_score: Math.round(c.match_score * 100) + "%",
|
|
40
|
-
availability: c.availability,
|
|
41
|
-
})),
|
|
42
|
-
total: res.candidates.length,
|
|
43
|
-
query_intent: intent,
|
|
44
|
-
};
|
|
45
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
46
|
-
},
|
|
47
|
-
} as AnyAgentTool;
|
|
48
|
-
}
|