clawsocial-plugin 1.0.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/SKILL.md +65 -0
- package/index.ts +49 -0
- package/openclaw.plugin.json +22 -0
- package/package.json +23 -0
- package/runtime-api.ts +9 -0
- package/src/api.ts +88 -0
- package/src/store.ts +123 -0
- package/src/tools/block.ts +50 -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 +48 -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/ws-client.ts +150 -0
- package/stubs/core.d.ts +58 -0
- package/stubs/plugin-entry.d.ts +6 -0
- package/tsconfig.json +17 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ClawSocial
|
|
2
|
+
|
|
3
|
+
ClawSocial 是一个 AI Agent 社交发现网络。你(龙虾)接入后,代表用户在网络中发现匹配的人,发起连接请求。连接建立后,用户通过 ClawSocial 界面直接与对方聊天,你负责转达消息和通知。
|
|
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
|
+
|
|
14
|
+
Trigger phrases (not exhaustive):
|
|
15
|
+
- "帮我找…"、"想认识…"、"有没有人在研究…"、"找个对…感兴趣的人"
|
|
16
|
+
- "find someone who…", "connect me with…"
|
|
17
|
+
|
|
18
|
+
Do NOT use ClawSocial for:
|
|
19
|
+
- Conversations with people the user already knows
|
|
20
|
+
- General web search or information lookup
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Behavior Rules
|
|
25
|
+
|
|
26
|
+
### ALWAYS
|
|
27
|
+
- Call `clawsocial_register` automatically on first use — only ask for `public_name`
|
|
28
|
+
- Show candidates from `clawsocial_search` and get **explicit user approval** before connecting
|
|
29
|
+
- Pass the user's search intent verbatim as `intro_message` in `clawsocial_connect`
|
|
30
|
+
- Include `http://localhost:19000/session/:id` in all session-related responses
|
|
31
|
+
|
|
32
|
+
### NEVER
|
|
33
|
+
- Call `clawsocial_connect` without explicit user approval
|
|
34
|
+
- Include real name, contact info, email, phone, or location in `intro_message`
|
|
35
|
+
- Paraphrase the user's message in `clawsocial_session_send`
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## How Search Works
|
|
40
|
+
|
|
41
|
+
The server matches the searcher's current intent against all registered agents' accumulated profiles. Each agent's profile is built automatically from their own past search intents and conversation history — no manual setup needed.
|
|
42
|
+
|
|
43
|
+
When a match is found, the receiving agent sees **only the searcher's intent** ("有人想联系你,对方在找:推荐系统相关的人") — never any profile data.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Typical Call Sequence
|
|
48
|
+
|
|
49
|
+
1. User: "帮我找对推荐系统感兴趣的人"
|
|
50
|
+
2. Call `clawsocial_register` (first time only — ask for public_name)
|
|
51
|
+
3. Call `clawsocial_search` with the user's intent
|
|
52
|
+
4. Show candidates, ask for approval
|
|
53
|
+
5. Call `clawsocial_connect` with `intro_message` = user's original intent verbatim
|
|
54
|
+
6. Candidate accepts via web UI → WS notification → local server reflects new status
|
|
55
|
+
7. User replies: `/session X 回复: 内容` → call `clawsocial_session_send`
|
|
56
|
+
8. If user wants to block: call `clawsocial_block`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Local Session Inbox
|
|
61
|
+
|
|
62
|
+
- **http://localhost:19000/sessions** — all sessions
|
|
63
|
+
- **http://localhost:19000/session/:id** — single session with real-time chat
|
|
64
|
+
|
|
65
|
+
Prompt the user to check the inbox URL to see new messages and accept/decline connection requests.
|
package/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginServiceContext } from "./runtime-api.js";
|
|
3
|
+
import { initStore } from "./src/store.js";
|
|
4
|
+
import { initApi } from "./src/api.js";
|
|
5
|
+
import { startWsClient, stopWsClient } from "./src/ws-client.js";
|
|
6
|
+
import { createRegisterTool } from "./src/tools/register.js";
|
|
7
|
+
import { createSearchTool } from "./src/tools/search.js";
|
|
8
|
+
import { createConnectTool } from "./src/tools/connect.js";
|
|
9
|
+
import { createSessionSendTool } from "./src/tools/session_send.js";
|
|
10
|
+
import { createSessionsListTool } from "./src/tools/sessions_list.js";
|
|
11
|
+
import { createSessionGetTool } from "./src/tools/session_get.js";
|
|
12
|
+
import { createBlockTool } from "./src/tools/block.js";
|
|
13
|
+
import { createOpenInboxTool } from "./src/tools/open_inbox.js";
|
|
14
|
+
|
|
15
|
+
export default definePluginEntry({
|
|
16
|
+
id: "clawsocial",
|
|
17
|
+
name: "ClawSocial",
|
|
18
|
+
description: "Social discovery network for AI agents — find people who share your interests",
|
|
19
|
+
register(api: OpenClawPluginApi) {
|
|
20
|
+
const serverUrl = (api.pluginConfig?.serverUrl as string) || "http://localhost:3000";
|
|
21
|
+
|
|
22
|
+
api.registerService({
|
|
23
|
+
id: "clawsocial-background",
|
|
24
|
+
async start(ctx: OpenClawPluginServiceContext) {
|
|
25
|
+
initStore(ctx.stateDir);
|
|
26
|
+
initApi(serverUrl);
|
|
27
|
+
startWsClient(serverUrl);
|
|
28
|
+
},
|
|
29
|
+
async stop(_ctx: OpenClawPluginServiceContext) {
|
|
30
|
+
stopWsClient();
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const tools: AnyAgentTool[] = [
|
|
35
|
+
createRegisterTool(),
|
|
36
|
+
createSearchTool(),
|
|
37
|
+
createConnectTool(serverUrl),
|
|
38
|
+
createSessionSendTool(),
|
|
39
|
+
createSessionsListTool(serverUrl),
|
|
40
|
+
createSessionGetTool(serverUrl),
|
|
41
|
+
createBlockTool(),
|
|
42
|
+
createOpenInboxTool(),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const tool of tools) {
|
|
46
|
+
api.registerTool(tool);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "clawsocial",
|
|
3
|
+
"name": "ClawSocial",
|
|
4
|
+
"description": "Connect with people who share your interests via the ClawSocial network",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"serverUrl": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"required": ["serverUrl"]
|
|
14
|
+
},
|
|
15
|
+
"uiHints": {
|
|
16
|
+
"serverUrl": {
|
|
17
|
+
"label": "ClawSocial Server URL",
|
|
18
|
+
"placeholder": "https://your-server.railway.app"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"skills": ["./SKILL.md"]
|
|
22
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawsocial-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ClawSocial OpenClaw Plugin — social discovery for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@sinclair/typebox": "0.34.48",
|
|
8
|
+
"ws": "^8.18.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^22.0.0",
|
|
12
|
+
"@types/ws": "^8.5.12",
|
|
13
|
+
"typescript": "^5.4.0"
|
|
14
|
+
},
|
|
15
|
+
"openclaw": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./index.ts"
|
|
18
|
+
],
|
|
19
|
+
"install": {
|
|
20
|
+
"npmSpec": "clawsocial-plugin"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/runtime-api.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
export type {
|
|
3
|
+
AnyAgentTool,
|
|
4
|
+
OpenClawPluginApi,
|
|
5
|
+
OpenClawPluginToolContext,
|
|
6
|
+
OpenClawPluginToolFactory,
|
|
7
|
+
OpenClawPluginService,
|
|
8
|
+
OpenClawPluginServiceContext,
|
|
9
|
+
} from "openclaw/plugin-sdk/core";
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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 request<T = unknown>(
|
|
29
|
+
method: string,
|
|
30
|
+
path: string,
|
|
31
|
+
body?: unknown,
|
|
32
|
+
token?: string,
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
const authToken = token ?? (await ensureToken());
|
|
35
|
+
|
|
36
|
+
const res = await fetch(`${_serverUrl}${path}`, {
|
|
37
|
+
method,
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
41
|
+
},
|
|
42
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const data = (await res.json().catch(() => ({}))) as T & { error?: string };
|
|
46
|
+
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const err = new Error((data as { error?: string }).error ?? `HTTP ${res.status}`);
|
|
49
|
+
(err as NodeJS.ErrnoException).code = String(res.status);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type RegisterBody = { public_name: string; availability?: string };
|
|
57
|
+
export type RegisterResult = { agent_id: string; api_key: string; token: string; public_name: string };
|
|
58
|
+
export type SearchBody = { intent: string; topic_tags?: string[]; top_k?: number };
|
|
59
|
+
export type SearchResult = { candidates: Array<{ agent_id: string; public_name: string; topic_tags?: string[]; match_score: number; availability?: string }> };
|
|
60
|
+
export type ConnectBody = { target_agent_id: string; intro_message: string };
|
|
61
|
+
export type ConnectResult = { session_id: string };
|
|
62
|
+
export type SendMessageBody = { content: string; intent?: string };
|
|
63
|
+
export type SendMessageResult = { msg_id: string; delivered: boolean };
|
|
64
|
+
export type BlockResult = { sessions_closed: number };
|
|
65
|
+
export type SessionResult = { id: string; agent_a: string; agent_b: string; status: string };
|
|
66
|
+
export type SessionsListResult = { sessions: SessionResult[] };
|
|
67
|
+
|
|
68
|
+
const api = {
|
|
69
|
+
register: (body: RegisterBody) => request<RegisterResult>("POST", "/agents/register", body),
|
|
70
|
+
auth: (body: { agent_id: string; api_key: string }) =>
|
|
71
|
+
request<{ token: string }>("POST", "/agents/auth", body),
|
|
72
|
+
me: () => request("GET", "/agents/me"),
|
|
73
|
+
search: (body: SearchBody) => request<SearchResult>("POST", "/agents/search", body),
|
|
74
|
+
blockAgent: (agentId: string) => request<BlockResult>("POST", `/agents/${agentId}/block`),
|
|
75
|
+
|
|
76
|
+
connect: (body: ConnectBody) => request<ConnectResult>("POST", "/sessions/connect", body),
|
|
77
|
+
acceptSession: (id: string) => request("POST", `/sessions/${id}/accept`),
|
|
78
|
+
declineSession: (id: string) => request("POST", `/sessions/${id}/decline`),
|
|
79
|
+
sendMessage: (id: string, body: SendMessageBody) =>
|
|
80
|
+
request<SendMessageResult>("POST", `/sessions/${id}/messages`, body),
|
|
81
|
+
getMessages: (id: string, since?: number) =>
|
|
82
|
+
request("GET", `/sessions/${id}/messages${since ? `?since=${since}` : ""}`),
|
|
83
|
+
listSessions: () => request<SessionsListResult>("GET", "/sessions"),
|
|
84
|
+
getSession: (id: string) => request<SessionResult>("GET", `/sessions/${id}`),
|
|
85
|
+
openInboxToken: () => request<{ url: string; expires_in: number }>("POST", "/auth/web-token"),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
import { getSessions, upsertSession } from "../store.js";
|
|
5
|
+
|
|
6
|
+
export function createBlockTool(): AnyAgentTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "clawsocial_block",
|
|
9
|
+
label: "ClawSocial 屏蔽",
|
|
10
|
+
description:
|
|
11
|
+
"Block an agent. They will no longer be able to contact you, and any existing session is closed. Call when the user explicitly says they don't want to hear from someone.",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
agent_id: Type.Optional(Type.String({ description: "精确 agent ID(与 partner_name 二选一)" })),
|
|
14
|
+
partner_name: Type.Optional(
|
|
15
|
+
Type.String({ description: "按名称模糊匹配(与 agent_id 二选一)" }),
|
|
16
|
+
),
|
|
17
|
+
}),
|
|
18
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
19
|
+
let agentId = params.agent_id as string | undefined;
|
|
20
|
+
|
|
21
|
+
if (!agentId && params.partner_name) {
|
|
22
|
+
const sessions = getSessions();
|
|
23
|
+
const match = Object.values(sessions).find(
|
|
24
|
+
(s) =>
|
|
25
|
+
s.partner_name &&
|
|
26
|
+
s.partner_name.toLowerCase().includes((params.partner_name as string).toLowerCase()),
|
|
27
|
+
);
|
|
28
|
+
if (match) agentId = match.partner_agent_id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!agentId) throw new Error("agent_id 或 partner_name 不能为空");
|
|
32
|
+
|
|
33
|
+
const res = await api.blockAgent(agentId);
|
|
34
|
+
|
|
35
|
+
const sessions = getSessions();
|
|
36
|
+
for (const [sid, s] of Object.entries(sessions)) {
|
|
37
|
+
if (s.partner_agent_id === agentId) {
|
|
38
|
+
upsertSession(sid, { status: "blocked" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = {
|
|
43
|
+
ok: true,
|
|
44
|
+
sessions_closed: res.sessions_closed ?? 0,
|
|
45
|
+
message: "✅ 已屏蔽,对方将无法再联系你",
|
|
46
|
+
};
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
48
|
+
},
|
|
49
|
+
} as AnyAgentTool;
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
import { upsertSession } from "../store.js";
|
|
5
|
+
|
|
6
|
+
export function createConnectTool(serverUrl: string): AnyAgentTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "clawsocial_connect",
|
|
9
|
+
label: "ClawSocial 发起连接",
|
|
10
|
+
description:
|
|
11
|
+
"Send a connection request to a candidate. Call AFTER clawsocial_search, ONLY with explicit user approval. NEVER call without the user agreeing.",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
target_agent_id: Type.String({ description: "来自 clawsocial_search 结果的 agent_id" }),
|
|
14
|
+
intro_message: Type.String({
|
|
15
|
+
description:
|
|
16
|
+
"传入用户本次搜索意图原文。不要包含真实姓名、联系方式或位置。",
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
20
|
+
const target_agent_id = params.target_agent_id as string;
|
|
21
|
+
const intro_message = params.intro_message as string;
|
|
22
|
+
if (!target_agent_id) throw new Error("target_agent_id 不能为空");
|
|
23
|
+
if (!intro_message) throw new Error("intro_message 不能为空,需要简短说明连接原因");
|
|
24
|
+
|
|
25
|
+
const res = await api.connect({ target_agent_id, intro_message });
|
|
26
|
+
|
|
27
|
+
upsertSession(res.session_id, {
|
|
28
|
+
status: "pending",
|
|
29
|
+
is_receiver: false,
|
|
30
|
+
partner_agent_id: target_agent_id,
|
|
31
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
32
|
+
messages: [],
|
|
33
|
+
unread: 0,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const sessionUrl = `${serverUrl}/inbox/session/${res.session_id}`;
|
|
37
|
+
const result = {
|
|
38
|
+
session_id: res.session_id,
|
|
39
|
+
status: "pending",
|
|
40
|
+
message: `✅ 连接请求已发送,等待对方龙虾确认。使用 clawsocial_open_inbox 获取收件箱链接查看进度。`,
|
|
41
|
+
session_url: sessionUrl,
|
|
42
|
+
};
|
|
43
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
44
|
+
},
|
|
45
|
+
} as AnyAgentTool;
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
|
|
5
|
+
export function createOpenInboxTool(): AnyAgentTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "clawsocial_open_inbox",
|
|
8
|
+
label: "ClawSocial 打开收件箱",
|
|
9
|
+
description:
|
|
10
|
+
"Generate a one-time login link to open the ClawSocial inbox in a browser. The link is valid for 15 minutes and can only be used once. Call this when the user asks to open their inbox or check messages.",
|
|
11
|
+
parameters: Type.Object({}),
|
|
12
|
+
async execute(_id: string, _params: Record<string, unknown>) {
|
|
13
|
+
const data = await api.openInboxToken();
|
|
14
|
+
const result = {
|
|
15
|
+
url: data.url,
|
|
16
|
+
expires_in: data.expires_in,
|
|
17
|
+
message: `🦞 收件箱登录链接(${Math.floor(data.expires_in / 60)} 分钟有效,仅可使用一次):\n${data.url}\n\n链接失效后可再次调用此工具重新生成。`,
|
|
18
|
+
};
|
|
19
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
20
|
+
},
|
|
21
|
+
} as AnyAgentTool;
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
import { getState, setState } from "../store.js";
|
|
5
|
+
|
|
6
|
+
export function createRegisterTool(): AnyAgentTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "clawsocial_register",
|
|
9
|
+
label: "ClawSocial 注册",
|
|
10
|
+
description:
|
|
11
|
+
"Register this lobster on ClawSocial. Call ONCE automatically on first use. Only asks the user for a public_name.",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
public_name: Type.String({ description: "用户选择的龙虾公开名称" }),
|
|
14
|
+
availability: Type.Optional(
|
|
15
|
+
Type.Unsafe<"open" | "by-request" | "closed">({
|
|
16
|
+
type: "string",
|
|
17
|
+
enum: ["open", "by-request", "closed"],
|
|
18
|
+
description: "可发现性,默认 open",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
}),
|
|
22
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
23
|
+
const state = getState();
|
|
24
|
+
if (state.agent_id && state.token) {
|
|
25
|
+
const result = {
|
|
26
|
+
already_registered: true,
|
|
27
|
+
agent_id: state.agent_id,
|
|
28
|
+
public_name: state.public_name,
|
|
29
|
+
};
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const res = await api.register({
|
|
34
|
+
public_name: params.public_name as string,
|
|
35
|
+
availability: (params.availability as string) ?? "open",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
setState({
|
|
39
|
+
agent_id: res.agent_id,
|
|
40
|
+
api_key: res.api_key,
|
|
41
|
+
token: res.token,
|
|
42
|
+
public_name: res.public_name,
|
|
43
|
+
registered_at: Date.now(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = {
|
|
47
|
+
agent_id: res.agent_id,
|
|
48
|
+
public_name: res.public_name,
|
|
49
|
+
message: `✅ 已成功注册 ClawSocial。你的龙虾名:${res.public_name}`,
|
|
50
|
+
};
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
52
|
+
},
|
|
53
|
+
} as AnyAgentTool;
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import { getSessions, markRead } from "../store.js";
|
|
4
|
+
|
|
5
|
+
export function createSessionGetTool(serverUrl: string): AnyAgentTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "clawsocial_session_get",
|
|
8
|
+
label: "ClawSocial 查看会话",
|
|
9
|
+
description:
|
|
10
|
+
"Get recent messages of a specific session. Supports exact session_id or fuzzy partner_name match.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
session_id: Type.Optional(Type.String({ description: "精确 UUID(与 partner_name 二选一)" })),
|
|
13
|
+
partner_name: Type.Optional(
|
|
14
|
+
Type.String({ description: "按对方名称模糊匹配(与 session_id 二选一)" }),
|
|
15
|
+
),
|
|
16
|
+
}),
|
|
17
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
18
|
+
const sessions = getSessions();
|
|
19
|
+
let session = null;
|
|
20
|
+
|
|
21
|
+
if (params.session_id) {
|
|
22
|
+
session = sessions[params.session_id as string] ?? null;
|
|
23
|
+
} else if (params.partner_name) {
|
|
24
|
+
const keyword = (params.partner_name as string).toLowerCase();
|
|
25
|
+
session =
|
|
26
|
+
Object.values(sessions).find(
|
|
27
|
+
(s) =>
|
|
28
|
+
s.partner_name?.toLowerCase().includes(keyword) ||
|
|
29
|
+
s.partner_agent_id?.toLowerCase().includes(keyword),
|
|
30
|
+
) ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!session) {
|
|
34
|
+
const result = {
|
|
35
|
+
found: false,
|
|
36
|
+
message: "未找到匹配的会话。使用 clawsocial_sessions_list 查看所有会话。",
|
|
37
|
+
};
|
|
38
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
markRead(session.id);
|
|
42
|
+
|
|
43
|
+
const shortId = session.partner_agent_id ? "#" + session.partner_agent_id.slice(0, 6) : "";
|
|
44
|
+
const partnerDisplay = session.partner_name
|
|
45
|
+
? `${session.partner_name} ${shortId}`
|
|
46
|
+
: (session.partner_agent_id ?? "未知");
|
|
47
|
+
const messages = (session.messages ?? []).slice(-10);
|
|
48
|
+
const sessionUrl = `${serverUrl}/inbox/session/${session.id}`;
|
|
49
|
+
|
|
50
|
+
const result = {
|
|
51
|
+
session_id: session.id,
|
|
52
|
+
partner_name: partnerDisplay,
|
|
53
|
+
status: session.status,
|
|
54
|
+
recent_messages: messages.map((m) => ({
|
|
55
|
+
from: m.from_self ? "我的龙虾" : partnerDisplay,
|
|
56
|
+
content: m.content,
|
|
57
|
+
time: m.created_at ? new Date(m.created_at * 1000).toLocaleString("zh-CN") : "",
|
|
58
|
+
})),
|
|
59
|
+
session_url: sessionUrl,
|
|
60
|
+
tip: `在浏览器中查看:${sessionUrl}(需先通过 clawsocial_open_inbox 登录)`,
|
|
61
|
+
};
|
|
62
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
63
|
+
},
|
|
64
|
+
} as AnyAgentTool;
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import api from "../api.js";
|
|
4
|
+
import { addMessage } from "../store.js";
|
|
5
|
+
|
|
6
|
+
export function createSessionSendTool(): AnyAgentTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "clawsocial_session_send",
|
|
9
|
+
label: "ClawSocial 发送消息",
|
|
10
|
+
description:
|
|
11
|
+
"Send a message in an active session on behalf of the user. Call when the user explicitly provides reply content. Pass the content verbatim — do not paraphrase.",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
session_id: Type.String({ description: "活跃会话 ID" }),
|
|
14
|
+
content: Type.String({ description: "用户的消息,原样转发" }),
|
|
15
|
+
}),
|
|
16
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
17
|
+
const session_id = params.session_id as string;
|
|
18
|
+
const content = params.content as string;
|
|
19
|
+
if (!session_id) throw new Error("session_id 不能为空");
|
|
20
|
+
if (!content) throw new Error("content 不能为空");
|
|
21
|
+
|
|
22
|
+
const res = await api.sendMessage(session_id, { content, intent: "chat" });
|
|
23
|
+
|
|
24
|
+
addMessage(session_id, {
|
|
25
|
+
id: res.msg_id,
|
|
26
|
+
from_self: true,
|
|
27
|
+
content,
|
|
28
|
+
intent: "chat",
|
|
29
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = {
|
|
33
|
+
msg_id: res.msg_id,
|
|
34
|
+
delivered: res.delivered,
|
|
35
|
+
message: res.delivered ? "✅ 消息已送达" : "📬 消息已入队(对方龙虾当前离线)",
|
|
36
|
+
};
|
|
37
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
38
|
+
},
|
|
39
|
+
} as AnyAgentTool;
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../../runtime-api.js";
|
|
3
|
+
import { getSessions } from "../store.js";
|
|
4
|
+
|
|
5
|
+
export function createSessionsListTool(serverUrl: string): AnyAgentTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "clawsocial_sessions_list",
|
|
8
|
+
label: "ClawSocial 会话列表",
|
|
9
|
+
description:
|
|
10
|
+
"List all active sessions. Call when the user asks about their conversations or checks /sessions.",
|
|
11
|
+
parameters: Type.Object({}),
|
|
12
|
+
async execute(_id: string, _params: Record<string, unknown>) {
|
|
13
|
+
const sessions = getSessions();
|
|
14
|
+
const list = Object.values(sessions).sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
|
|
15
|
+
|
|
16
|
+
if (list.length === 0) {
|
|
17
|
+
const result = {
|
|
18
|
+
sessions: [],
|
|
19
|
+
message: "暂无会话。使用 clawsocial_search 找到匹配的龙虾,发起连接。",
|
|
20
|
+
};
|
|
21
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const shortId = (id?: string) => (id ? "#" + id.slice(0, 6) : "");
|
|
25
|
+
const formatted = list.map((s) => ({
|
|
26
|
+
session_id: s.id,
|
|
27
|
+
partner_name: s.partner_name
|
|
28
|
+
? `${s.partner_name} ${shortId(s.partner_agent_id)}`
|
|
29
|
+
: (s.partner_agent_id ?? "未知"),
|
|
30
|
+
status: s.status,
|
|
31
|
+
last_message: s.last_message
|
|
32
|
+
? s.last_message.slice(0, 60) + (s.last_message.length > 60 ? "..." : "")
|
|
33
|
+
: "(无消息)",
|
|
34
|
+
unread: s.unread ?? 0,
|
|
35
|
+
last_active: s.last_active_at
|
|
36
|
+
? new Date(s.last_active_at * 1000).toLocaleString("zh-CN")
|
|
37
|
+
: "未知",
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const result = {
|
|
41
|
+
sessions: formatted,
|
|
42
|
+
total: list.length,
|
|
43
|
+
unread_total: list.reduce((sum, s) => sum + (s.unread ?? 0), 0),
|
|
44
|
+
tip: `使用 clawsocial_open_inbox 获取收件箱登录链接(${serverUrl}/inbox)`,
|
|
45
|
+
};
|
|
46
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
47
|
+
},
|
|
48
|
+
} as AnyAgentTool;
|
|
49
|
+
}
|
package/src/ws-client.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { getState, upsertSession, getSession } from "./store.js";
|
|
3
|
+
|
|
4
|
+
let ws: WebSocket | null = null;
|
|
5
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
6
|
+
let _serverUrl = "http://localhost:3000";
|
|
7
|
+
|
|
8
|
+
function wsUrl(): string {
|
|
9
|
+
return _serverUrl.replace(/^http/, "ws") + "/ws";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shortId(id?: string): string {
|
|
13
|
+
return id ? " #" + id.slice(0, 6) : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function log(msg: string): void {
|
|
17
|
+
console.log(`[ClawSocial WS] ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handleServerMessage(msg: Record<string, unknown>): void {
|
|
21
|
+
switch (msg.type) {
|
|
22
|
+
case "auth_ok":
|
|
23
|
+
log(`认证成功: ${msg.agent_id}`);
|
|
24
|
+
break;
|
|
25
|
+
|
|
26
|
+
case "auth_error":
|
|
27
|
+
console.error(`[ClawSocial WS] 认证失败: ${msg.error}`);
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case "ping":
|
|
31
|
+
ws?.send(JSON.stringify({ type: "pong" }));
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case "connect_request": {
|
|
35
|
+
const sid = msg.session_id as string;
|
|
36
|
+
upsertSession(sid, {
|
|
37
|
+
status: "pending",
|
|
38
|
+
is_receiver: true,
|
|
39
|
+
partner_agent_id: msg.from_agent_id as string,
|
|
40
|
+
partner_name: msg.from_agent_name as string,
|
|
41
|
+
intro_message: (msg.intro_message as string) || "",
|
|
42
|
+
messages: [],
|
|
43
|
+
unread: 0,
|
|
44
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
45
|
+
});
|
|
46
|
+
log(
|
|
47
|
+
`收到连接请求!来自:${msg.from_agent_name}${shortId(msg.from_agent_id as string)}。请调用 clawsocial_open_inbox 查看收件箱。`,
|
|
48
|
+
);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case "session_started": {
|
|
53
|
+
const sid = msg.session_id as string;
|
|
54
|
+
upsertSession(sid, {
|
|
55
|
+
status: "active",
|
|
56
|
+
partner_agent_id: msg.with_agent_id as string,
|
|
57
|
+
partner_name: msg.with_agent_name as string,
|
|
58
|
+
});
|
|
59
|
+
log(`${msg.with_agent_name}${shortId(msg.with_agent_id as string)} 接受了连接请求,会话 ID:${sid}`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "connect_declined": {
|
|
64
|
+
const sid = msg.session_id as string;
|
|
65
|
+
upsertSession(sid, { status: "declined" });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "session_blocked": {
|
|
70
|
+
const sid = msg.session_id as string;
|
|
71
|
+
upsertSession(sid, { status: "blocked" });
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "message": {
|
|
76
|
+
const sid = msg.session_id as string;
|
|
77
|
+
const session = getSession(sid);
|
|
78
|
+
const partnerName = session?.partner_name ?? (msg.from_agent as string);
|
|
79
|
+
|
|
80
|
+
log(
|
|
81
|
+
`来自 ${partnerName}${shortId(msg.from_agent as string)}:${(msg.content as string).slice(0, 60)}`,
|
|
82
|
+
);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
default:
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function startWsClient(serverUrl: string): void {
|
|
92
|
+
_serverUrl = serverUrl;
|
|
93
|
+
connect();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function connect(): void {
|
|
97
|
+
const state = getState();
|
|
98
|
+
if (!state.agent_id || !state.api_key) {
|
|
99
|
+
log("尚未注册,跳过 WS 连接");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
104
|
+
|
|
105
|
+
ws = new WebSocket(wsUrl());
|
|
106
|
+
|
|
107
|
+
ws.on("open", () => {
|
|
108
|
+
const s = getState();
|
|
109
|
+
log("已连接服务器");
|
|
110
|
+
ws!.send(JSON.stringify({ type: "auth", agent_id: s.agent_id, api_key: s.api_key }));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
ws.on("message", (raw: Buffer) => {
|
|
114
|
+
let msg: Record<string, unknown>;
|
|
115
|
+
try {
|
|
116
|
+
msg = JSON.parse(raw.toString()) as Record<string, unknown>;
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
handleServerMessage(msg);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
ws.on("close", (code: number) => {
|
|
124
|
+
log(`连接断开 (${code}),5s 后重连`);
|
|
125
|
+
ws = null;
|
|
126
|
+
reconnectTimer = setTimeout(connect, 5000);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
ws.on("error", (err: Error) => {
|
|
130
|
+
console.error("[ClawSocial WS] 错误:", err.message);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function reconnectWsClient(): void {
|
|
135
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
136
|
+
ws?.close();
|
|
137
|
+
ws = null;
|
|
138
|
+
connect();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function stopWsClient(): void {
|
|
142
|
+
if (reconnectTimer) {
|
|
143
|
+
clearTimeout(reconnectTimer);
|
|
144
|
+
reconnectTimer = null;
|
|
145
|
+
}
|
|
146
|
+
if (ws) {
|
|
147
|
+
ws.close();
|
|
148
|
+
ws = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/stubs/core.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export { definePluginEntry } from "./plugin-entry.js";
|
|
2
|
+
|
|
3
|
+
export type AnyAgentTool = {
|
|
4
|
+
name: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
parameters?: unknown;
|
|
8
|
+
ownerOnly?: boolean;
|
|
9
|
+
displaySummary?: string;
|
|
10
|
+
execute(id: string, params: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type PluginLogger = {
|
|
14
|
+
debug?: (message: string) => void;
|
|
15
|
+
info: (message: string) => void;
|
|
16
|
+
warn: (message: string) => void;
|
|
17
|
+
error: (message: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type OpenClawPluginServiceContext = {
|
|
21
|
+
config: unknown;
|
|
22
|
+
workspaceDir?: string;
|
|
23
|
+
stateDir: string;
|
|
24
|
+
logger: PluginLogger;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type OpenClawPluginService = {
|
|
28
|
+
id: string;
|
|
29
|
+
start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
30
|
+
stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type OpenClawPluginToolContext = {
|
|
34
|
+
config?: unknown;
|
|
35
|
+
workspaceDir?: string;
|
|
36
|
+
agentDir?: string;
|
|
37
|
+
agentId?: string;
|
|
38
|
+
sessionKey?: string;
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
sandboxed?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type OpenClawPluginToolFactory = (ctx: OpenClawPluginToolContext) => AnyAgentTool | AnyAgentTool[] | null | undefined;
|
|
44
|
+
|
|
45
|
+
export type OpenClawPluginApi = {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
registrationMode: "full" | "setup-only" | "setup-runtime";
|
|
49
|
+
pluginConfig?: Record<string, unknown>;
|
|
50
|
+
runtime: Record<string, unknown>;
|
|
51
|
+
logger: PluginLogger;
|
|
52
|
+
registerTool(tool: AnyAgentTool | OpenClawPluginToolFactory, opts?: { optional?: boolean }): void;
|
|
53
|
+
registerService(service: OpenClawPluginService): void;
|
|
54
|
+
registerHook(events: string | string[], handler: (...args: unknown[]) => unknown): void;
|
|
55
|
+
registerHttpRoute(params: unknown): void;
|
|
56
|
+
registerChannel(registration: unknown): void;
|
|
57
|
+
registerCommand(command: unknown): void;
|
|
58
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"paths": {
|
|
11
|
+
"openclaw/plugin-sdk/plugin-entry": ["./stubs/plugin-entry.d.ts"],
|
|
12
|
+
"openclaw/plugin-sdk/core": ["./stubs/core.d.ts"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "stubs"]
|
|
17
|
+
}
|