@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,140 @@
1
+ import type { WeiboResponseContentPart, WeiboResponseMessageInputItem } from "./types.js";
2
+
3
+ type SimStateLike = {
4
+ credentials?: Array<{
5
+ appId?: unknown;
6
+ appSecret?: unknown;
7
+ createdAt?: unknown;
8
+ }>;
9
+ };
10
+
11
+ export type LatestCredential = {
12
+ appId: string;
13
+ appSecret: string;
14
+ };
15
+
16
+ export type SimPageEndpoints = {
17
+ tokenUrl: string;
18
+ wsUrl: string;
19
+ };
20
+
21
+ export type SimInboundPayload = {
22
+ messageId: string;
23
+ fromUserId: string;
24
+ text: string;
25
+ timestamp: number;
26
+ input?: WeiboResponseMessageInputItem[];
27
+ };
28
+
29
+ export type SimComposerAttachment = {
30
+ kind: "image" | "file";
31
+ filename: string;
32
+ mimeType: string;
33
+ dataBase64: string;
34
+ };
35
+
36
+ export function getLatestCredentialFromState(state: SimStateLike): LatestCredential | null {
37
+ const credentials = Array.isArray(state.credentials) ? state.credentials : [];
38
+ if (credentials.length === 0) {
39
+ return null;
40
+ }
41
+
42
+ const sorted = [...credentials].sort((a, b) => {
43
+ const aTs = typeof a.createdAt === "number" ? a.createdAt : 0;
44
+ const bTs = typeof b.createdAt === "number" ? b.createdAt : 0;
45
+ return bTs - aTs;
46
+ });
47
+
48
+ const first = sorted[0];
49
+ const appId = String(first?.appId ?? "").trim();
50
+ const appSecret = String(first?.appSecret ?? "").trim();
51
+
52
+ if (!appId || !appSecret) {
53
+ return null;
54
+ }
55
+
56
+ return { appId, appSecret };
57
+ }
58
+
59
+ export function getSimPageEndpoints({
60
+ pageOrigin,
61
+ wsPort,
62
+ }: {
63
+ pageOrigin: string;
64
+ wsPort: number;
65
+ }): SimPageEndpoints {
66
+ const origin = new URL(pageOrigin);
67
+ const wsProtocol = origin.protocol === "https:" ? "wss:" : "ws:";
68
+ const tokenUrl = new URL("/open/auth/ws_token", origin).toString();
69
+ const wsUrl = `${wsProtocol}//${origin.hostname}:${wsPort}/ws/stream`;
70
+
71
+ return {
72
+ tokenUrl,
73
+ wsUrl,
74
+ };
75
+ }
76
+
77
+ export function getSimUiUrl({
78
+ host,
79
+ httpPort,
80
+ }: {
81
+ host: string;
82
+ httpPort: number;
83
+ }): string {
84
+ return `http://${host}:${httpPort}/`;
85
+ }
86
+
87
+ export function buildSimInputItems(params: {
88
+ text: string;
89
+ attachments?: SimComposerAttachment[];
90
+ }): WeiboResponseMessageInputItem[] | undefined {
91
+ const content: WeiboResponseContentPart[] = [];
92
+ const text = params.text.trim();
93
+
94
+ if (text) {
95
+ content.push({
96
+ type: "input_text",
97
+ text,
98
+ });
99
+ }
100
+
101
+ for (const attachment of params.attachments ?? []) {
102
+ content.push({
103
+ type: attachment.kind === "image" ? "input_image" : "input_file",
104
+ filename: attachment.filename,
105
+ source: {
106
+ type: "base64",
107
+ media_type: attachment.mimeType,
108
+ data: attachment.dataBase64,
109
+ },
110
+ });
111
+ }
112
+
113
+ if (content.length === 0) {
114
+ return undefined;
115
+ }
116
+
117
+ return [
118
+ {
119
+ type: "message",
120
+ role: "user",
121
+ content,
122
+ },
123
+ ];
124
+ }
125
+
126
+ export function buildSimInboundPayload(params: {
127
+ messageId: string;
128
+ fromUserId: string;
129
+ text: string;
130
+ timestamp: number;
131
+ input?: WeiboResponseMessageInputItem[];
132
+ }): SimInboundPayload {
133
+ return {
134
+ messageId: params.messageId,
135
+ fromUserId: params.fromUserId,
136
+ text: params.text,
137
+ timestamp: params.timestamp,
138
+ ...(params.input?.length ? { input: params.input } : {}),
139
+ };
140
+ }
@@ -0,0 +1,186 @@
1
+ import { randomUUID } from "crypto";
2
+
3
+ export type SimLogLevel = "info" | "warn" | "error";
4
+
5
+ export type SimCredential = {
6
+ appId: string;
7
+ appSecret: string;
8
+ createdAt: number;
9
+ };
10
+
11
+ export type SimTokenInfo = {
12
+ token: string;
13
+ appId: string;
14
+ createdAt: number;
15
+ expireIn: number;
16
+ };
17
+
18
+ export type SimMessageDirection = "inbound" | "outbound";
19
+
20
+ export type SimMessageRecord = {
21
+ id: string;
22
+ direction: SimMessageDirection;
23
+ appId: string;
24
+ fromUserId?: string;
25
+ toUserId?: string;
26
+ text: string;
27
+ timestamp: number;
28
+ wsType?: string;
29
+ rawText?: string;
30
+ rawPayload?: unknown;
31
+ };
32
+
33
+ export type SimLogRecord = {
34
+ id: string;
35
+ level: SimLogLevel;
36
+ message: string;
37
+ timestamp: number;
38
+ details?: unknown;
39
+ };
40
+
41
+ type SimStoreOptions = {
42
+ maxLogEntries?: number;
43
+ maxMessageEntries?: number;
44
+ };
45
+
46
+ type AppendMessageInput = Omit<SimMessageRecord, "id"> & { id?: string };
47
+
48
+ const DEFAULT_MAX_LOG_ENTRIES = 300;
49
+ const DEFAULT_MAX_MESSAGE_ENTRIES = 500;
50
+
51
+ function randomSuffix(): string {
52
+ return randomUUID().replace(/-/g, "").slice(0, 16);
53
+ }
54
+
55
+ function boundedPush<T>(arr: T[], value: T, max: number): void {
56
+ arr.push(value);
57
+ if (arr.length > max) {
58
+ arr.splice(0, arr.length - max);
59
+ }
60
+ }
61
+
62
+ export function createSimStore(options: SimStoreOptions = {}) {
63
+ const maxLogEntries = options.maxLogEntries ?? DEFAULT_MAX_LOG_ENTRIES;
64
+ const maxMessageEntries = options.maxMessageEntries ?? DEFAULT_MAX_MESSAGE_ENTRIES;
65
+
66
+ const credentialsByAppId = new Map<string, SimCredential>();
67
+ const tokenByValue = new Map<string, SimTokenInfo>();
68
+ const latestTokenByAppId = new Map<string, string>();
69
+ const messages: SimMessageRecord[] = [];
70
+ const logs: SimLogRecord[] = [];
71
+
72
+ function issueCredentials(): SimCredential {
73
+ const createdAt = Date.now();
74
+ const credential = registerCredentials(
75
+ `app_${createdAt}_${randomSuffix()}`,
76
+ `secret_${randomSuffix()}`,
77
+ createdAt,
78
+ );
79
+ return credential;
80
+ }
81
+
82
+ function registerCredentials(appId: string, appSecret: string, createdAt = Date.now()): SimCredential {
83
+ const credential: SimCredential = {
84
+ appId: String(appId),
85
+ appSecret: String(appSecret),
86
+ createdAt,
87
+ };
88
+ credentialsByAppId.set(credential.appId, credential);
89
+ return credential;
90
+ }
91
+
92
+ function listCredentials(): SimCredential[] {
93
+ return Array.from(credentialsByAppId.values()).sort((a, b) => b.createdAt - a.createdAt);
94
+ }
95
+
96
+ function issueToken(appId: string, appSecret: string, ttlSeconds: number): SimTokenInfo {
97
+ const credential = credentialsByAppId.get(appId);
98
+ if (!credential || credential.appSecret !== appSecret) {
99
+ throw new Error("Invalid credentials");
100
+ }
101
+
102
+ const prior = latestTokenByAppId.get(appId);
103
+ if (prior) {
104
+ tokenByValue.delete(prior);
105
+ }
106
+
107
+ const tokenInfo: SimTokenInfo = {
108
+ token: `wb_${randomSuffix()}_${Date.now()}`,
109
+ appId,
110
+ createdAt: Date.now(),
111
+ expireIn: ttlSeconds,
112
+ };
113
+
114
+ tokenByValue.set(tokenInfo.token, tokenInfo);
115
+ latestTokenByAppId.set(appId, tokenInfo.token);
116
+ return tokenInfo;
117
+ }
118
+
119
+ function validateToken(token: string): SimTokenInfo | null {
120
+ const tokenInfo = tokenByValue.get(token);
121
+ if (!tokenInfo) {
122
+ return null;
123
+ }
124
+
125
+ const expiresAt = tokenInfo.createdAt + tokenInfo.expireIn * 1000;
126
+ if (Date.now() > expiresAt) {
127
+ tokenByValue.delete(token);
128
+ if (latestTokenByAppId.get(tokenInfo.appId) === token) {
129
+ latestTokenByAppId.delete(tokenInfo.appId);
130
+ }
131
+ return null;
132
+ }
133
+
134
+ return tokenInfo;
135
+ }
136
+
137
+ function listTokens(): SimTokenInfo[] {
138
+ return Array.from(tokenByValue.values()).sort((a, b) => b.createdAt - a.createdAt);
139
+ }
140
+
141
+ function appendMessage(message: AppendMessageInput): SimMessageRecord {
142
+ const record: SimMessageRecord = {
143
+ ...message,
144
+ id: message.id ?? `msg_${Date.now()}_${randomSuffix()}`,
145
+ };
146
+ boundedPush(messages, record, maxMessageEntries);
147
+ return record;
148
+ }
149
+
150
+ function listMessages(limit = 50): SimMessageRecord[] {
151
+ if (limit <= 0) return [];
152
+ return messages.slice(-limit).reverse();
153
+ }
154
+
155
+ function appendLog(level: SimLogLevel, message: string, details?: unknown): SimLogRecord {
156
+ const record: SimLogRecord = {
157
+ id: `log_${Date.now()}_${randomSuffix()}`,
158
+ level,
159
+ message,
160
+ details,
161
+ timestamp: Date.now(),
162
+ };
163
+ boundedPush(logs, record, maxLogEntries);
164
+ return record;
165
+ }
166
+
167
+ function listLogs(limit = 200): SimLogRecord[] {
168
+ if (limit <= 0) return [];
169
+ return logs.slice(-limit).reverse();
170
+ }
171
+
172
+ return {
173
+ registerCredentials,
174
+ issueCredentials,
175
+ listCredentials,
176
+ issueToken,
177
+ validateToken,
178
+ listTokens,
179
+ appendMessage,
180
+ listMessages,
181
+ appendLog,
182
+ listLogs,
183
+ };
184
+ }
185
+
186
+ export type SimStore = ReturnType<typeof createSimStore>;
package/src/targets.ts ADDED
@@ -0,0 +1,14 @@
1
+ export function normalizeWeiboTarget(target: string): string {
2
+ const trimmed = target.trim();
3
+ if (!trimmed) return "";
4
+ if (trimmed.startsWith("user:")) return trimmed;
5
+ return `user:${trimmed}`;
6
+ }
7
+
8
+ export function looksLikeWeiboId(str: string): boolean {
9
+ return /^\d+$/.test(str.trim());
10
+ }
11
+
12
+ export function formatWeiboTarget(userId: string): string {
13
+ return `user:${userId}`;
14
+ }
package/src/token.ts ADDED
@@ -0,0 +1,207 @@
1
+ import type { ResolvedWeiboAccount } from "./types.js";
2
+ import { getWeiboTokenFingerprint } from "./fingerprint.js";
3
+
4
+ export type WeiboTokenResponse = {
5
+ data: {
6
+ token: string;
7
+ expire_in: number;
8
+ };
9
+ };
10
+
11
+ export type WeiboTokenResult = {
12
+ token: string;
13
+ expiresIn: number;
14
+ acquiredAt: number;
15
+ };
16
+
17
+ type CachedWeiboTokenResult = WeiboTokenResult & {
18
+ fingerprint: string;
19
+ };
20
+
21
+ const TOKEN_FETCH_BASE_DELAY_MS = 1_000;
22
+ const TOKEN_FETCH_MAX_DELAY_MS = 8_000;
23
+ const TOKEN_FETCH_MAX_RETRIES = 2;
24
+ const RETRYABLE_TOKEN_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
25
+
26
+ // Token cache per account
27
+ const tokenCache = new Map<string, CachedWeiboTokenResult>();
28
+
29
+ // Default token endpoint - configure in your openclaw.config.json
30
+ // Example: tokenEndpoint: "http://localhost:9810/open/auth/ws_token"
31
+ const DEFAULT_TOKEN_ENDPOINT = "http://open-im.api.weibo.com/open/auth/ws_token";
32
+
33
+ export class WeiboTokenFetchError extends Error {
34
+ retryable: boolean;
35
+ status?: number;
36
+
37
+ constructor(message: string, options: { retryable: boolean; status?: number }) {
38
+ super(message);
39
+ this.name = "WeiboTokenFetchError";
40
+ this.retryable = options.retryable;
41
+ this.status = options.status;
42
+ }
43
+ }
44
+
45
+ async function sleep(ms: number): Promise<void> {
46
+ await new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+
49
+ function readTokenEndpoint(value: unknown): string | undefined {
50
+ if (typeof value !== "string") {
51
+ return undefined;
52
+ }
53
+ const trimmed = value.trim();
54
+ return trimmed ? trimmed : undefined;
55
+ }
56
+
57
+ function isRetryableStatus(status: number): boolean {
58
+ return RETRYABLE_TOKEN_STATUS_CODES.has(status);
59
+ }
60
+
61
+ function toTokenFetchError(err: unknown): WeiboTokenFetchError {
62
+ if (err instanceof WeiboTokenFetchError) {
63
+ return err;
64
+ }
65
+ if (err instanceof Error) {
66
+ return new WeiboTokenFetchError(err.message, { retryable: true });
67
+ }
68
+ return new WeiboTokenFetchError(String(err), { retryable: true });
69
+ }
70
+
71
+ export function formatWeiboTokenFetchErrorMessage(err: unknown): string | null {
72
+ if (!(err instanceof WeiboTokenFetchError)) {
73
+ return null;
74
+ }
75
+ return `获取 token 失败: ${err.message}`;
76
+ }
77
+
78
+ export function isRetryableWeiboTokenFetchError(err: unknown): boolean | null {
79
+ if (!(err instanceof WeiboTokenFetchError)) {
80
+ return null;
81
+ }
82
+ return err.retryable;
83
+ }
84
+
85
+ export async function fetchWeiboToken(
86
+ account: ResolvedWeiboAccount,
87
+ tokenEndpoint?: string
88
+ ): Promise<WeiboTokenResult> {
89
+ const { appId, appSecret } = account;
90
+
91
+ if (!appId || !appSecret) {
92
+ throw new Error(`Credentials not configured for account "${account.accountId}"`);
93
+ }
94
+
95
+ // Defensive runtime coercion: config may come from untyped sources.
96
+ const normalizedAppId = String(appId);
97
+ const normalizedAppSecret = String(appSecret);
98
+
99
+ const endpoint = readTokenEndpoint(tokenEndpoint) ?? DEFAULT_TOKEN_ENDPOINT;
100
+ const fingerprint = getWeiboTokenFingerprint(account, endpoint);
101
+
102
+ for (let attempt = 0; ; attempt++) {
103
+ try {
104
+ const response = await fetch(endpoint, {
105
+ method: "POST",
106
+ headers: {
107
+ "Content-Type": "application/json",
108
+ },
109
+ body: JSON.stringify({
110
+ app_id: normalizedAppId,
111
+ app_secret: normalizedAppSecret,
112
+ }),
113
+ });
114
+
115
+ if (!response.ok) {
116
+ throw new WeiboTokenFetchError(
117
+ `Failed to fetch token: ${response.status} ${response.statusText}`,
118
+ {
119
+ retryable: isRetryableStatus(response.status),
120
+ status: response.status,
121
+ }
122
+ );
123
+ }
124
+
125
+ const result = (await response.json()) as WeiboTokenResponse;
126
+
127
+ if (!result.data?.token) {
128
+ throw new WeiboTokenFetchError("Invalid token response: missing token", {
129
+ retryable: false,
130
+ });
131
+ }
132
+
133
+ const tokenResult: CachedWeiboTokenResult = {
134
+ token: result.data.token,
135
+ expiresIn: result.data.expire_in,
136
+ acquiredAt: Date.now(),
137
+ fingerprint,
138
+ };
139
+
140
+ // Cache the token
141
+ tokenCache.set(account.accountId, tokenResult);
142
+
143
+ return tokenResult;
144
+ } catch (err) {
145
+ const tokenError = toTokenFetchError(err);
146
+ if (!tokenError.retryable || attempt >= TOKEN_FETCH_MAX_RETRIES) {
147
+ throw tokenError;
148
+ }
149
+
150
+ const delay = Math.min(
151
+ TOKEN_FETCH_BASE_DELAY_MS * Math.pow(2, attempt),
152
+ TOKEN_FETCH_MAX_DELAY_MS
153
+ );
154
+ await sleep(delay);
155
+ }
156
+ }
157
+ }
158
+
159
+ export function getCachedToken(
160
+ accountId: string,
161
+ fingerprint?: string
162
+ ): WeiboTokenResult | undefined {
163
+ const cached = tokenCache.get(accountId);
164
+ if (!cached) return undefined;
165
+
166
+ if (fingerprint && cached.fingerprint !== fingerprint) {
167
+ tokenCache.delete(accountId);
168
+ return undefined;
169
+ }
170
+
171
+ // Check if token is expired (with 60s buffer)
172
+ const expiresAt = cached.acquiredAt + cached.expiresIn * 1000 - 60000;
173
+ if (Date.now() > expiresAt) {
174
+ tokenCache.delete(accountId);
175
+ return undefined;
176
+ }
177
+
178
+ return cached;
179
+ }
180
+
181
+ export function clearTokenCache(accountId?: string): void {
182
+ if (accountId) {
183
+ tokenCache.delete(accountId);
184
+ } else {
185
+ tokenCache.clear();
186
+ }
187
+ }
188
+
189
+ export async function getValidToken(
190
+ account: ResolvedWeiboAccount,
191
+ tokenEndpoint?: string
192
+ ): Promise<string> {
193
+ const fingerprint = getWeiboTokenFingerprint(
194
+ account,
195
+ readTokenEndpoint(tokenEndpoint) ?? readTokenEndpoint(account.tokenEndpoint) ?? DEFAULT_TOKEN_ENDPOINT
196
+ );
197
+
198
+ // Try to get cached token
199
+ const cached = getCachedToken(account.accountId, fingerprint);
200
+ if (cached) {
201
+ return cached.token;
202
+ }
203
+
204
+ // Fetch new token
205
+ const result = await fetchWeiboToken(account, readTokenEndpoint(tokenEndpoint));
206
+ return result.token;
207
+ }
@@ -0,0 +1,55 @@
1
+ import type { WeiboToolsConfig } from "./types.js";
2
+
3
+ export type ResolvedWeiboToolsConfig = Required<{
4
+ search: boolean;
5
+ myWeibo: boolean;
6
+ hotSearch: boolean;
7
+ }>;
8
+
9
+ const DEFAULT_TOOLS_CONFIG: ResolvedWeiboToolsConfig = {
10
+ search: true, // Search enabled by default
11
+ myWeibo: true, // My weibo enabled by default
12
+ hotSearch: true, // Hot search enabled by default
13
+ };
14
+
15
+ /**
16
+ * Resolve tools configuration with defaults.
17
+ * Missing values default to true (enabled).
18
+ */
19
+ export function resolveToolsConfig(
20
+ tools: WeiboToolsConfig | undefined
21
+ ): ResolvedWeiboToolsConfig {
22
+ if (!tools) {
23
+ return { ...DEFAULT_TOOLS_CONFIG };
24
+ }
25
+ return {
26
+ search: tools.search ?? DEFAULT_TOOLS_CONFIG.search,
27
+ myWeibo: tools.myWeibo ?? DEFAULT_TOOLS_CONFIG.myWeibo,
28
+ hotSearch: tools.hotSearch ?? DEFAULT_TOOLS_CONFIG.hotSearch,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Check if any account has a specific tool enabled.
34
+ * Used to determine whether to register a tool at all.
35
+ */
36
+ export function resolveAnyEnabledWeiboToolsConfig(
37
+ accounts: Array<{ config: { tools?: WeiboToolsConfig } }>
38
+ ): ResolvedWeiboToolsConfig {
39
+ const merged: ResolvedWeiboToolsConfig = {
40
+ search: false,
41
+ myWeibo: false,
42
+ hotSearch: false,
43
+ };
44
+ for (const account of accounts) {
45
+ const cfg = resolveToolsConfig(account.config.tools);
46
+ merged.search = merged.search || cfg.search;
47
+ merged.myWeibo = merged.myWeibo || cfg.myWeibo;
48
+ merged.hotSearch = merged.hotSearch || cfg.hotSearch;
49
+ }
50
+ // If no accounts have explicit config, use defaults
51
+ if (accounts.length === 0) {
52
+ return { ...DEFAULT_TOOLS_CONFIG };
53
+ }
54
+ return merged;
55
+ }
package/src/types.ts ADDED
@@ -0,0 +1,95 @@
1
+ import type { z } from "zod";
2
+ import type { WeiboConfigSchema, WeiboAccountConfigSchema, WeiboToolsConfigSchema } from "./config-schema.js";
3
+
4
+ export type WeiboConfig = z.infer<typeof WeiboConfigSchema>;
5
+ export type WeiboAccountConfig = z.infer<typeof WeiboAccountConfigSchema>;
6
+ export type WeiboToolsConfig = z.infer<typeof WeiboToolsConfigSchema>;
7
+
8
+ export type ResolvedWeiboAccount = {
9
+ accountId: string;
10
+ enabled: boolean;
11
+ configured: boolean;
12
+ name?: string;
13
+ appId?: string;
14
+ appSecret?: string;
15
+ wsEndpoint?: string;
16
+ tokenEndpoint?: string;
17
+ config: WeiboAccountConfig;
18
+ };
19
+
20
+ export type WeiboConnectionState =
21
+ | "idle"
22
+ | "connecting"
23
+ | "connected"
24
+ | "backoff"
25
+ | "error"
26
+ | "stopped";
27
+
28
+ export type WeiboDisconnectInfo = {
29
+ code: number;
30
+ reason: string;
31
+ at: number;
32
+ };
33
+
34
+ export type WeiboRuntimeStatusPatch = {
35
+ running?: boolean;
36
+ connected?: boolean;
37
+ connectionState?: WeiboConnectionState;
38
+ reconnectAttempts?: number;
39
+ nextRetryAt?: number | null;
40
+ lastConnectedAt?: number | null;
41
+ lastDisconnect?: WeiboDisconnectInfo | null;
42
+ lastError?: string | null;
43
+ lastStartAt?: number | null;
44
+ lastStopAt?: number | null;
45
+ lastInboundAt?: number | null;
46
+ lastOutboundAt?: number | null;
47
+ };
48
+
49
+ export type WeiboMessageContext = {
50
+ messageId: string;
51
+ senderId: string;
52
+ text: string;
53
+ createTime?: number;
54
+ };
55
+
56
+ export type WeiboResponseInputSource = {
57
+ type: "base64";
58
+ media_type: string;
59
+ data: string;
60
+ };
61
+
62
+ export type WeiboResponseContentPart =
63
+ | {
64
+ type: "input_text";
65
+ text: string;
66
+ }
67
+ | {
68
+ type: "input_image";
69
+ source: WeiboResponseInputSource;
70
+ filename?: string;
71
+ }
72
+ | {
73
+ type: "input_file";
74
+ source: WeiboResponseInputSource;
75
+ filename?: string;
76
+ };
77
+
78
+ export type WeiboResponseMessageInputItem = {
79
+ type: "message";
80
+ role: "system" | "developer" | "user" | "assistant";
81
+ content: WeiboResponseContentPart[];
82
+ };
83
+
84
+ export type WeiboInboundAttachmentPart = {
85
+ mimeType: string;
86
+ filename?: string;
87
+ base64: string;
88
+ };
89
+
90
+ export type WeiboSendResult = {
91
+ messageId: string;
92
+ chatId: string;
93
+ chunkId: number;
94
+ done: boolean;
95
+ };