@wecode-ai/weibo-openclaw-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.
@@ -0,0 +1,341 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { Type, type Static } from "@sinclair/typebox";
3
+
4
+ // ============ Schema ============
5
+
6
+ export const WeiboStatusSchema = Type.Object({
7
+ count: Type.Optional(
8
+ Type.Number({
9
+ description: "每页数量,最大 100,默认 20",
10
+ minimum: 1,
11
+ maximum: 100,
12
+ })
13
+ ),
14
+ });
15
+
16
+ export type WeiboStatusParams = Static<typeof WeiboStatusSchema>;
17
+
18
+ // ============ Helpers ============
19
+
20
+ function json(data: unknown) {
21
+ return {
22
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
23
+ details: data,
24
+ };
25
+ }
26
+
27
+ // ============ API Types ============
28
+
29
+ /**
30
+ * 用户信息结构(简化版)
31
+ */
32
+ export type WeiboStatusUser = {
33
+ screen_name: string;
34
+ };
35
+
36
+ /**
37
+ * 微博状态项结构
38
+ */
39
+ export type WeiboStatusItem = {
40
+ /** 微博 ID */
41
+ id: number;
42
+ /** 微博 MID */
43
+ mid: string;
44
+ /** 微博内容 */
45
+ text: string;
46
+ /** 发布时间 */
47
+ created_at: string;
48
+ /** 是否有图片 */
49
+ has_image: boolean;
50
+ /** 图片列表(图片ID数组) */
51
+ images?: string[];
52
+ /** 图片数量 */
53
+ pic_num?: number;
54
+ /** 评论数 */
55
+ comments_count: number;
56
+ /** 转发数 */
57
+ reposts_count: number;
58
+ /** 点赞数 */
59
+ attitudes_count: number;
60
+ /** 用户信息 */
61
+ user: WeiboStatusUser;
62
+ /** 转发的原微博 */
63
+ repost?: WeiboStatusItem;
64
+ };
65
+
66
+ /**
67
+ * 用户微博 API 响应结构
68
+ */
69
+ export type WeiboStatusApiResponse = {
70
+ code: number;
71
+ message: string;
72
+ data: {
73
+ statuses: WeiboStatusItem[];
74
+ total_number: number;
75
+ };
76
+ };
77
+
78
+ // ============ Token Management ============
79
+
80
+ // Token 过期时间:2小时(7200秒),提前60秒刷新
81
+ const TOKEN_EXPIRE_SECONDS = 7200;
82
+ const TOKEN_REFRESH_BUFFER_SECONDS = 60;
83
+
84
+ // 默认 token 端点
85
+ const DEFAULT_TOKEN_ENDPOINT = "http://open-im.api.weibo.com/open/auth/ws_token";
86
+
87
+ type WeiboStatusTokenCache = {
88
+ token: string;
89
+ acquiredAt: number;
90
+ expiresIn: number;
91
+ };
92
+
93
+ // 专用的 token 缓存
94
+ let weiboStatusTokenCache: WeiboStatusTokenCache | null = null;
95
+
96
+ type TokenResponse = {
97
+ data: {
98
+ token: string;
99
+ expire_in: number;
100
+ };
101
+ };
102
+
103
+ /**
104
+ * 获取 token
105
+ */
106
+ async function fetchWeiboStatusToken(
107
+ appId: string,
108
+ appSecret: string,
109
+ tokenEndpoint?: string
110
+ ): Promise<WeiboStatusTokenCache> {
111
+ const endpoint = tokenEndpoint || DEFAULT_TOKEN_ENDPOINT;
112
+
113
+ const response = await fetch(endpoint, {
114
+ method: "POST",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({
119
+ app_id: appId,
120
+ app_secret: appSecret,
121
+ }),
122
+ });
123
+
124
+ if (!response.ok) {
125
+ const errorText = await response.text().catch(() => "");
126
+ throw new Error(
127
+ `获取 token 失败: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`
128
+ );
129
+ }
130
+
131
+ const result = (await response.json()) as TokenResponse;
132
+
133
+ if (!result.data?.token) {
134
+ throw new Error("获取 token 失败: 响应中缺少 token");
135
+ }
136
+
137
+ const tokenCache: WeiboStatusTokenCache = {
138
+ token: result.data.token,
139
+ acquiredAt: Date.now(),
140
+ expiresIn: result.data.expire_in || TOKEN_EXPIRE_SECONDS,
141
+ };
142
+
143
+ weiboStatusTokenCache = tokenCache;
144
+ return tokenCache;
145
+ }
146
+
147
+ /**
148
+ * 获取有效的 token
149
+ */
150
+ async function getValidWeiboStatusToken(
151
+ appId: string,
152
+ appSecret: string,
153
+ tokenEndpoint?: string
154
+ ): Promise<string> {
155
+ if (weiboStatusTokenCache) {
156
+ const expiresAt =
157
+ weiboStatusTokenCache.acquiredAt +
158
+ weiboStatusTokenCache.expiresIn * 1000 -
159
+ TOKEN_REFRESH_BUFFER_SECONDS * 1000;
160
+ if (Date.now() < expiresAt) {
161
+ return weiboStatusTokenCache.token;
162
+ }
163
+ }
164
+
165
+ const tokenResult = await fetchWeiboStatusToken(appId, appSecret, tokenEndpoint);
166
+ return tokenResult.token;
167
+ }
168
+
169
+ // ============ Core Functions ============
170
+
171
+ const DEFAULT_WEIBO_STATUS_ENDPOINT = "http://open-im.api.weibo.com/open/weibo/user_status";
172
+
173
+ type FetchWeiboStatusOptions = {
174
+ token: string;
175
+ count?: number;
176
+ endpoint?: string;
177
+ };
178
+
179
+ /**
180
+ * 获取用户自己发布的微博
181
+ */
182
+ async function fetchWeiboStatus(
183
+ options: FetchWeiboStatusOptions
184
+ ): Promise<WeiboStatusApiResponse> {
185
+ const apiEndpoint = options.endpoint || DEFAULT_WEIBO_STATUS_ENDPOINT;
186
+
187
+ const url = new URL(apiEndpoint);
188
+ url.searchParams.set("token", options.token);
189
+
190
+ if (options.count !== undefined) {
191
+ url.searchParams.set("count", String(options.count));
192
+ }
193
+
194
+ const response = await fetch(url.toString(), {
195
+ method: "GET",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ },
199
+ });
200
+
201
+ if (!response.ok) {
202
+ const errorText = await response.text().catch(() => "");
203
+ throw new Error(
204
+ `获取用户微博失败: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`
205
+ );
206
+ }
207
+
208
+ const result = (await response.json()) as WeiboStatusApiResponse;
209
+ return result;
210
+ }
211
+
212
+ /**
213
+ * 格式化单条微博
214
+ */
215
+ function formatStatusItem(status: WeiboStatusItem) {
216
+ const formatted: Record<string, unknown> = {
217
+ id: status.id,
218
+ mid: status.mid,
219
+ text: status.text,
220
+ createdAt: status.created_at,
221
+ hasImage: status.has_image,
222
+ images: status.images,
223
+ picNum: status.pic_num,
224
+ commentsCount: status.comments_count,
225
+ repostsCount: status.reposts_count,
226
+ attitudesCount: status.attitudes_count,
227
+ user: {
228
+ screenName: status.user.screen_name,
229
+ },
230
+ };
231
+
232
+ // 添加转发微博信息
233
+ if (status.repost) {
234
+ formatted.repost = formatStatusItem(status.repost);
235
+ }
236
+
237
+ return formatted;
238
+ }
239
+
240
+ /**
241
+ * 格式化结果
242
+ */
243
+ function formatWeiboStatusResult(result: WeiboStatusApiResponse) {
244
+ if (result.code !== 0) {
245
+ return {
246
+ success: false,
247
+ error: result.message || "获取用户微博失败",
248
+ };
249
+ }
250
+
251
+ const data = result.data;
252
+
253
+ if (!data.statuses || data.statuses.length === 0) {
254
+ return {
255
+ success: true,
256
+ total: 0,
257
+ statuses: [],
258
+ message: "没有找到微博内容",
259
+ };
260
+ }
261
+
262
+ return {
263
+ success: true,
264
+ total: data.total_number,
265
+ statuses: data.statuses.map(formatStatusItem),
266
+ };
267
+ }
268
+
269
+ // ============ Configuration Types ============
270
+
271
+ export type WeiboStatusConfig = {
272
+ weiboStatusEndpoint?: string;
273
+ appId?: string;
274
+ appSecret?: string;
275
+ tokenEndpoint?: string;
276
+ enabled?: boolean;
277
+ };
278
+
279
+ function getWeiboStatusConfig(api: OpenClawPluginApi): WeiboStatusConfig {
280
+ const weiboCfg = api.config?.channels?.weibo as Record<string, unknown> | undefined;
281
+ return {
282
+ weiboStatusEndpoint: weiboCfg?.weiboStatusEndpoint as string | undefined,
283
+ appId: weiboCfg?.appId as string | undefined,
284
+ appSecret: weiboCfg?.appSecret as string | undefined,
285
+ tokenEndpoint: weiboCfg?.tokenEndpoint as string | undefined,
286
+ enabled: weiboCfg?.weiboStatusEnabled !== false,
287
+ };
288
+ }
289
+
290
+ // ============ Tool Registration ============
291
+
292
+ export function registerWeiboStatusTools(api: OpenClawPluginApi) {
293
+ const cfg = getWeiboStatusConfig(api);
294
+
295
+ if (!cfg.enabled) {
296
+ api.logger.debug?.("weibo_status: Tool disabled, skipping registration");
297
+ return;
298
+ }
299
+
300
+ if (!cfg.appId || !cfg.appSecret) {
301
+ api.logger.warn?.("weibo_status: appId or appSecret not configured, tool disabled");
302
+ return;
303
+ }
304
+
305
+ const appId = cfg.appId;
306
+ const appSecret = cfg.appSecret;
307
+
308
+ api.registerTool(
309
+ () => ({
310
+ name: "weibo_status",
311
+ label: "Weibo Status",
312
+ description:
313
+ "获取用户自己发布的微博列表。返回用户发布的微博内容(包含原博内容)、互动数据等信息。",
314
+ parameters: WeiboStatusSchema,
315
+ async execute(_toolCallId, params) {
316
+ const p = params as WeiboStatusParams;
317
+ try {
318
+ const token = await getValidWeiboStatusToken(
319
+ appId,
320
+ appSecret,
321
+ cfg.tokenEndpoint
322
+ );
323
+
324
+ const result = await fetchWeiboStatus({
325
+ token,
326
+ count: p.count,
327
+ endpoint: cfg.weiboStatusEndpoint,
328
+ });
329
+
330
+ return json(formatWeiboStatusResult(result));
331
+ } catch (err) {
332
+ return json({
333
+ error: err instanceof Error ? err.message : String(err),
334
+ });
335
+ }
336
+ },
337
+ }),
338
+ { name: "weibo_status" }
339
+ );
340
+ api.logger.info?.("weibo_status: Registered weibo_status tool");
341
+ }