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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TIM Client — 腾讯云 IM 实时消息接收
|
|
3
|
+
*
|
|
4
|
+
* 替代 ws-client.ts,使用 @tencentcloud/chat Node.js SDK
|
|
5
|
+
* 接收来自对方 Agent 的消息,dispatch 到 OpenClaw 活跃会话
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getState, upsertSession, getSession, addMessage } from "./store.js";
|
|
9
|
+
import api from "./api.js";
|
|
10
|
+
|
|
11
|
+
// @tencentcloud/chat exports a default object with create() + EVENT + TYPES
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
let TencentCloudChat: any = null;
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
let timSdk: any = null;
|
|
16
|
+
|
|
17
|
+
let _ready = false;
|
|
18
|
+
let _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
+
|
|
20
|
+
// Dispatch function injected by index.ts (subagent.run → real agent run)
|
|
21
|
+
let _dispatch: ((text: string, conversationId: string, senderName: string) => Promise<void>) | null = null;
|
|
22
|
+
|
|
23
|
+
export function setDispatch(fn: (text: string, conversationId: string, senderName: string) => Promise<void>): void {
|
|
24
|
+
_dispatch = fn;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function log(msg: string): void {
|
|
28
|
+
console.log(`[ClawSocial TIM] ${msg}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
async function handleMessage(msg: any): Promise<void> {
|
|
33
|
+
// Only handle C2C (one-to-one) messages
|
|
34
|
+
if (!TencentCloudChat || msg.conversationType !== TencentCloudChat.TYPES.CONV_C2C) return;
|
|
35
|
+
|
|
36
|
+
const fromUserId: string = msg.from as string;
|
|
37
|
+
const myId = getState().agent_id;
|
|
38
|
+
if (!fromUserId || fromUserId === myId) return; // ignore own messages (echo)
|
|
39
|
+
|
|
40
|
+
// Extract text payload
|
|
41
|
+
const elements: unknown[] = Array.isArray(msg.payload?.elems) ? msg.payload.elems : [];
|
|
42
|
+
let text = "";
|
|
43
|
+
for (const el of elements) {
|
|
44
|
+
const e = el as { type?: string; content?: { text?: string } };
|
|
45
|
+
if (e.type === "TIMTextElem" && e.content?.text) {
|
|
46
|
+
text += e.content.text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Fallback: tim-js-sdk shape (payload.text)
|
|
50
|
+
if (!text && (msg.payload as { text?: string })?.text) {
|
|
51
|
+
text = (msg.payload as { text: string }).text;
|
|
52
|
+
}
|
|
53
|
+
if (!text) return;
|
|
54
|
+
|
|
55
|
+
// Look up session & partner name from local store
|
|
56
|
+
// session id is stored as the sessionId key in sessions.json
|
|
57
|
+
// We search by partner_agent_id
|
|
58
|
+
const session = Object.values(
|
|
59
|
+
(await import("./store.js")).getSessions()
|
|
60
|
+
).find((s) => s.partner_agent_id === fromUserId);
|
|
61
|
+
|
|
62
|
+
const sessionId = session?.id ?? fromUserId; // fallback to userId as conversationId
|
|
63
|
+
const partnerName = session?.partner_name ?? fromUserId;
|
|
64
|
+
|
|
65
|
+
addMessage(sessionId, {
|
|
66
|
+
id: `tim-${Date.now()}`,
|
|
67
|
+
from_self: false,
|
|
68
|
+
partner_name: partnerName,
|
|
69
|
+
content: text,
|
|
70
|
+
created_at: (msg.time as number) || Math.floor(Date.now() / 1000),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
log(`来自 ${partnerName}:${text.slice(0, 60)}`);
|
|
74
|
+
|
|
75
|
+
if (_dispatch) {
|
|
76
|
+
await _dispatch(text, sessionId, partnerName);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function initTIM(): Promise<void> {
|
|
81
|
+
if (!TencentCloudChat) {
|
|
82
|
+
// Dynamic import so the module resolves at runtime (ESM)
|
|
83
|
+
const mod = await import("@tencentcloud/chat");
|
|
84
|
+
TencentCloudChat = mod.default ?? mod;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const state = getState();
|
|
88
|
+
if (!state.agent_id) {
|
|
89
|
+
log("尚未注册,跳过 TIM 初始化");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fetch UserSig from our server (server-cn-tim /auth/usersig endpoint)
|
|
94
|
+
let userSigResult: { user_id: string; user_sig: string; sdk_app_id: number };
|
|
95
|
+
try {
|
|
96
|
+
userSigResult = await api.getUserSig();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
log(`获取 UserSig 失败:${(err as Error).message},30s 后重试`);
|
|
99
|
+
_reconnectTimer = setTimeout(initTIM, 30_000);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { user_sig: userSig, sdk_app_id: sdkAppId } = userSigResult;
|
|
104
|
+
|
|
105
|
+
// Destroy previous instance if any
|
|
106
|
+
if (timSdk) {
|
|
107
|
+
try { timSdk.destroy(); } catch { /* ignore */ }
|
|
108
|
+
timSdk = null;
|
|
109
|
+
_ready = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
timSdk = TencentCloudChat.create({ SDKAppID: sdkAppId });
|
|
113
|
+
timSdk.setLogLevel(2); // 0=debug, 1=log, 2=warn, 3=error
|
|
114
|
+
|
|
115
|
+
timSdk.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
116
|
+
_ready = true;
|
|
117
|
+
log("TIM SDK 就绪,开始接收消息");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
timSdk.on(TencentCloudChat.EVENT.SDK_NOT_READY, () => {
|
|
121
|
+
_ready = false;
|
|
122
|
+
log("TIM SDK 未就绪");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
|
+
timSdk.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, (event: any) => {
|
|
127
|
+
const messages: unknown[] = Array.isArray(event?.data) ? event.data : [];
|
|
128
|
+
for (const msg of messages) {
|
|
129
|
+
handleMessage(msg).catch((err) =>
|
|
130
|
+
console.error("[ClawSocial TIM] dispatch error:", err)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
timSdk.on(TencentCloudChat.EVENT.KICKED_OUT, () => {
|
|
136
|
+
log("账号被踢下线,30s 后重新登录");
|
|
137
|
+
_ready = false;
|
|
138
|
+
_reconnectTimer = setTimeout(initTIM, 30_000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Login
|
|
142
|
+
const loginRes = await timSdk.login({ userID: state.agent_id, userSig });
|
|
143
|
+
if (loginRes.code !== 0) {
|
|
144
|
+
log(`登录失败 code=${loginRes.code},30s 后重试`);
|
|
145
|
+
_reconnectTimer = setTimeout(initTIM, 30_000);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
log(`已登录 TIM,用户 ID: ${state.agent_id}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function startTimClient(): void {
|
|
153
|
+
initTIM().catch((err) => {
|
|
154
|
+
console.error("[ClawSocial TIM] 初始化错误:", err);
|
|
155
|
+
_reconnectTimer = setTimeout(startTimClient, 30_000);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function reconnectTimClient(): void {
|
|
160
|
+
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
|
161
|
+
initTIM().catch((err) => {
|
|
162
|
+
console.error("[ClawSocial TIM] 重连错误:", err);
|
|
163
|
+
_reconnectTimer = setTimeout(startTimClient, 30_000);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function stopTimClient(): void {
|
|
168
|
+
if (_reconnectTimer) {
|
|
169
|
+
clearTimeout(_reconnectTimer);
|
|
170
|
+
_reconnectTimer = null;
|
|
171
|
+
}
|
|
172
|
+
if (timSdk) {
|
|
173
|
+
try { timSdk.destroy(); } catch { /* ignore */ }
|
|
174
|
+
timSdk = null;
|
|
175
|
+
_ready = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../types.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: "active",
|
|
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: "active",
|
|
40
|
+
message: `✅ Connected! You can start chatting now. Use clawsocial_open_inbox to open the inbox link.`,
|
|
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 "../types.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 "../types.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.api_key) {
|
|
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,51 @@
|
|
|
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
|
+
...(c.manual_intro ? { manual_intro: c.manual_intro } : {}),
|
|
42
|
+
...(c.auto_bio ? { auto_bio: c.auto_bio } : {}),
|
|
43
|
+
...(c.match_reason ? { match_reason: c.match_reason } : {}),
|
|
44
|
+
})),
|
|
45
|
+
total: res.candidates.length,
|
|
46
|
+
query_intent: intent,
|
|
47
|
+
};
|
|
48
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
49
|
+
},
|
|
50
|
+
} as AnyAgentTool;
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "../types.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 "../types.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 "../types.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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import type { AnyAgentTool } from "../types.js";
|
|
6
|
+
|
|
7
|
+
type LocalFiles = { soul: string; memory: string; user: string };
|
|
8
|
+
|
|
9
|
+
function readLocalFiles(): LocalFiles {
|
|
10
|
+
const home = os.homedir();
|
|
11
|
+
const bases = [
|
|
12
|
+
path.join(home, ".openclaw", "workspace"),
|
|
13
|
+
path.join(home, ".clawdbot", "workspace"),
|
|
14
|
+
];
|
|
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 "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
soul: readFirst("SOUL.md"),
|
|
28
|
+
memory: readFirst("memory/MEMORY.md"),
|
|
29
|
+
user: readFirst("USER.md"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createSuggestProfileTool(): AnyAgentTool {
|
|
34
|
+
return {
|
|
35
|
+
name: "clawsocial_suggest_profile",
|
|
36
|
+
label: "ClawSocial 建议兴趣资料",
|
|
37
|
+
description:
|
|
38
|
+
"Read the user's OpenClaw memory to help draft a ClawSocial interest profile. " +
|
|
39
|
+
"Call this after registration or when the user wants to update their profile. " +
|
|
40
|
+
"After calling this tool, draft a 2-3 sentence interest description based on the memory content, " +
|
|
41
|
+
"show it to the user, and ONLY call clawsocial_update_profile after the user explicitly confirms or edits it. " +
|
|
42
|
+
"NEVER update the profile silently.",
|
|
43
|
+
parameters: Type.Object({}),
|
|
44
|
+
async execute(_id: string, _params: Record<string, unknown>) {
|
|
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) {
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: JSON.stringify({
|
|
55
|
+
files_found: 0,
|
|
56
|
+
message: "No local OpenClaw files found. Please ask the user to describe their interests directly.",
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
files_found: found.length,
|
|
69
|
+
completeness_score: completeness,
|
|
70
|
+
soul: files.soul || null,
|
|
71
|
+
memory: files.memory || null,
|
|
72
|
+
user: files.user || null,
|
|
73
|
+
instruction:
|
|
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 with auto_bio (not interest_text) and completeness_score after explicit user approval.",
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
} as AnyAgentTool;
|
|
84
|
+
}
|