@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.
- package/README.md +59 -0
- package/index.ts +67 -0
- package/package.json +51 -0
- package/src/accounts.ts +134 -0
- package/src/bot.ts +486 -0
- package/src/channel.ts +391 -0
- package/src/client.ts +435 -0
- package/src/config-schema.ts +58 -0
- package/src/fingerprint.ts +25 -0
- package/src/monitor.ts +206 -0
- package/src/outbound-stream.ts +241 -0
- package/src/outbound.ts +49 -0
- package/src/plugin-sdk-compat.ts +82 -0
- package/src/policy.ts +10 -0
- package/src/runtime.ts +14 -0
- package/src/search-schema.ts +7 -0
- package/src/send.ts +80 -0
- package/src/sim-page.ts +140 -0
- package/src/sim-store.ts +186 -0
- package/src/targets.ts +14 -0
- package/src/token.ts +207 -0
- package/src/tools-config.ts +55 -0
- package/src/types.ts +95 -0
- package/src/weibo-hot-search.ts +345 -0
- package/src/weibo-search.ts +333 -0
- package/src/weibo-status.ts +341 -0
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { ResolvedWeiboAccount, WeiboRuntimeStatusPatch } from "./types.js";
|
|
3
|
+
import { resolveWeiboAccount, listEnabledWeiboAccounts } from "./accounts.js";
|
|
4
|
+
import { clearClientCache, createWeiboClient, WeiboWebSocketClient } from "./client.js";
|
|
5
|
+
import { clearTokenCache, formatWeiboTokenFetchErrorMessage } from "./token.js";
|
|
6
|
+
import { handleWeiboMessage, type WeiboMessageEvent } from "./bot.js";
|
|
7
|
+
import { waitUntilAbortCompat } from "./plugin-sdk-compat.js";
|
|
8
|
+
|
|
9
|
+
export type MonitorWeiboOpts = {
|
|
10
|
+
config?: ClawdbotConfig;
|
|
11
|
+
runtime?: RuntimeEnv;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
accountId?: string;
|
|
14
|
+
statusSink?: (patch: WeiboRuntimeStatusPatch) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Track connections per account
|
|
18
|
+
const wsClients = new Map<string, WeiboWebSocketClient>();
|
|
19
|
+
|
|
20
|
+
async function monitorSingleAccount(params: {
|
|
21
|
+
cfg: ClawdbotConfig;
|
|
22
|
+
account: ResolvedWeiboAccount;
|
|
23
|
+
runtime?: RuntimeEnv;
|
|
24
|
+
abortSignal?: AbortSignal;
|
|
25
|
+
statusSink?: (patch: WeiboRuntimeStatusPatch) => void;
|
|
26
|
+
}): Promise<void> {
|
|
27
|
+
const { cfg, account, runtime, abortSignal, statusSink } = params;
|
|
28
|
+
const { accountId } = account;
|
|
29
|
+
const log = runtime?.log ?? console.log;
|
|
30
|
+
const error = runtime?.error ?? console.error;
|
|
31
|
+
const emitStatus = (patch: WeiboRuntimeStatusPatch) => statusSink?.(patch);
|
|
32
|
+
|
|
33
|
+
log(`weibo[${accountId}]: connecting WebSocket...`);
|
|
34
|
+
emitStatus({
|
|
35
|
+
running: true,
|
|
36
|
+
connected: false,
|
|
37
|
+
connectionState: "connecting",
|
|
38
|
+
lastStartAt: Date.now(),
|
|
39
|
+
lastStopAt: null,
|
|
40
|
+
lastError: null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const client = createWeiboClient(account);
|
|
44
|
+
wsClients.set(accountId, client);
|
|
45
|
+
client.onStatus(emitStatus);
|
|
46
|
+
|
|
47
|
+
client.onMessage((data) => {
|
|
48
|
+
try {
|
|
49
|
+
const msg = data as { type?: string; payload?: unknown };
|
|
50
|
+
if (msg.type === "message" && msg.payload) {
|
|
51
|
+
emitStatus({ lastInboundAt: Date.now() });
|
|
52
|
+
const event = msg as WeiboMessageEvent;
|
|
53
|
+
handleWeiboMessage({ cfg, event, accountId, runtime }).catch((err) => {
|
|
54
|
+
error(`weibo[${accountId}]: error handling message: ${String(err)}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
error(`weibo[${accountId}]: error processing message: ${String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
client.onError((err) => {
|
|
63
|
+
emitStatus({
|
|
64
|
+
running: true,
|
|
65
|
+
connected: false,
|
|
66
|
+
connectionState: "error",
|
|
67
|
+
lastError: err.message,
|
|
68
|
+
});
|
|
69
|
+
error(`weibo[${accountId}]: WebSocket error: ${err.message}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
client.onOpen(() => {
|
|
73
|
+
emitStatus({
|
|
74
|
+
running: true,
|
|
75
|
+
connected: true,
|
|
76
|
+
connectionState: "connected",
|
|
77
|
+
reconnectAttempts: 0,
|
|
78
|
+
nextRetryAt: null,
|
|
79
|
+
lastConnectedAt: Date.now(),
|
|
80
|
+
lastError: null,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
client.onClose((code, reason) => {
|
|
85
|
+
emitStatus({
|
|
86
|
+
running: !abortSignal?.aborted,
|
|
87
|
+
connected: false,
|
|
88
|
+
connectionState: abortSignal?.aborted ? "stopped" : "error",
|
|
89
|
+
lastDisconnect: {
|
|
90
|
+
code,
|
|
91
|
+
reason,
|
|
92
|
+
at: Date.now(),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
log(`weibo[${accountId}]: WebSocket closed (code: ${code}, reason: ${reason})`);
|
|
96
|
+
wsClients.delete(accountId);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Handle abort signal
|
|
100
|
+
const handleAbort = () => {
|
|
101
|
+
log(`weibo[${accountId}]: abort signal received, closing connection`);
|
|
102
|
+
client.close();
|
|
103
|
+
wsClients.delete(accountId);
|
|
104
|
+
emitStatus({
|
|
105
|
+
running: false,
|
|
106
|
+
connected: false,
|
|
107
|
+
connectionState: "stopped",
|
|
108
|
+
nextRetryAt: null,
|
|
109
|
+
lastStopAt: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (abortSignal?.aborted) {
|
|
114
|
+
handleAbort();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await client.connect();
|
|
122
|
+
log(`weibo[${accountId}]: WebSocket connected`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const message = formatWeiboTokenFetchErrorMessage(err) ?? String(err);
|
|
125
|
+
emitStatus({
|
|
126
|
+
running: true,
|
|
127
|
+
connected: false,
|
|
128
|
+
connectionState: "error",
|
|
129
|
+
lastError: message,
|
|
130
|
+
});
|
|
131
|
+
error(`weibo[${accountId}]: failed to connect: ${message}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Keep the channel task alive until the gateway aborts it. Returning early here
|
|
135
|
+
// causes the gateway supervisor to interpret startup as an exit and trigger
|
|
136
|
+
// provider auto-restart loops.
|
|
137
|
+
await waitUntilAbortCompat(abortSignal);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function monitorWeiboProvider(opts: MonitorWeiboOpts = {}): Promise<void> {
|
|
141
|
+
const cfg = opts.config;
|
|
142
|
+
if (!cfg) {
|
|
143
|
+
throw new Error("Config is required for Weibo monitor");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const log = opts.runtime?.log ?? console.log;
|
|
147
|
+
|
|
148
|
+
// If accountId is specified, only monitor that account
|
|
149
|
+
if (opts.accountId) {
|
|
150
|
+
const account = resolveWeiboAccount({ cfg, accountId: opts.accountId });
|
|
151
|
+
if (!account.enabled || !account.configured) {
|
|
152
|
+
throw new Error(`Weibo account "${opts.accountId}" not configured or disabled`);
|
|
153
|
+
}
|
|
154
|
+
return monitorSingleAccount({
|
|
155
|
+
cfg,
|
|
156
|
+
account,
|
|
157
|
+
runtime: opts.runtime,
|
|
158
|
+
abortSignal: opts.abortSignal,
|
|
159
|
+
statusSink: opts.statusSink,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Otherwise, start all enabled accounts
|
|
164
|
+
const accounts = listEnabledWeiboAccounts(cfg);
|
|
165
|
+
if (accounts.length === 0) {
|
|
166
|
+
throw new Error("No enabled Weibo accounts configured");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
log(`weibo: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
|
|
170
|
+
|
|
171
|
+
// Start all accounts in parallel
|
|
172
|
+
await Promise.all(
|
|
173
|
+
accounts.map((account) =>
|
|
174
|
+
monitorSingleAccount({
|
|
175
|
+
cfg,
|
|
176
|
+
account,
|
|
177
|
+
runtime: opts.runtime,
|
|
178
|
+
abortSignal: opts.abortSignal,
|
|
179
|
+
statusSink: opts.statusSink,
|
|
180
|
+
}),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function stopWeiboMonitor(accountId?: string): void {
|
|
186
|
+
if (accountId) {
|
|
187
|
+
const client = wsClients.get(accountId);
|
|
188
|
+
if (client) {
|
|
189
|
+
client.close();
|
|
190
|
+
wsClients.delete(accountId);
|
|
191
|
+
}
|
|
192
|
+
clearClientCache(accountId);
|
|
193
|
+
clearTokenCache(accountId);
|
|
194
|
+
} else {
|
|
195
|
+
for (const client of wsClients.values()) {
|
|
196
|
+
client.close();
|
|
197
|
+
}
|
|
198
|
+
wsClients.clear();
|
|
199
|
+
clearClientCache();
|
|
200
|
+
clearTokenCache();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function reconnectWeiboMonitor(accountId?: string): Promise<void> {
|
|
205
|
+
stopWeiboMonitor(accountId);
|
|
206
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export type WeiboChunkMode = "length" | "newline" | "raw";
|
|
2
|
+
|
|
3
|
+
type StreamDebugFn = (tag: string, data?: Record<string, unknown>) => void;
|
|
4
|
+
|
|
5
|
+
export type WeiboOutboundEmitFn = (params: {
|
|
6
|
+
text: string;
|
|
7
|
+
done: boolean;
|
|
8
|
+
source: "partial" | "deliver" | "settled";
|
|
9
|
+
}) => Promise<void>;
|
|
10
|
+
|
|
11
|
+
type CreateWeiboOutboundStreamParams = {
|
|
12
|
+
chunkMode: WeiboChunkMode;
|
|
13
|
+
textChunkLimit: number;
|
|
14
|
+
emit: WeiboOutboundEmitFn;
|
|
15
|
+
chunkTextWithMode: (text: string, limit: number, mode: "length" | "newline") => Iterable<string>;
|
|
16
|
+
streamDebug?: StreamDebugFn;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type StreamStateSnapshot = {
|
|
20
|
+
hasSeenPartial: boolean;
|
|
21
|
+
hasEmittedChunks: boolean;
|
|
22
|
+
hasEmittedDone: boolean;
|
|
23
|
+
newlineBufferLen: number;
|
|
24
|
+
pendingDeliverBufferLen: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const PARAGRAPH_DELIMITER_RE = /\n[\t ]*\n+/g;
|
|
28
|
+
|
|
29
|
+
function findLastParagraphDelimiterEnd(value: string): number {
|
|
30
|
+
let lastEnd = -1;
|
|
31
|
+
let match: RegExpExecArray | null = null;
|
|
32
|
+
while ((match = PARAGRAPH_DELIMITER_RE.exec(value)) !== null) {
|
|
33
|
+
lastEnd = match.index + match[0].length;
|
|
34
|
+
}
|
|
35
|
+
return lastEnd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveDeltaFromSnapshot(previous: string, next: string): {
|
|
39
|
+
delta: string;
|
|
40
|
+
nextSnapshot: string;
|
|
41
|
+
nonMonotonic: boolean;
|
|
42
|
+
} {
|
|
43
|
+
if (!next || next === previous) {
|
|
44
|
+
return { delta: "", nextSnapshot: next, nonMonotonic: false };
|
|
45
|
+
}
|
|
46
|
+
if (next.startsWith(previous)) {
|
|
47
|
+
return {
|
|
48
|
+
delta: next.slice(previous.length),
|
|
49
|
+
nextSnapshot: next,
|
|
50
|
+
nonMonotonic: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (previous.startsWith(next)) {
|
|
54
|
+
return {
|
|
55
|
+
delta: "",
|
|
56
|
+
nextSnapshot: next,
|
|
57
|
+
nonMonotonic: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let prefixLen = 0;
|
|
62
|
+
const maxLen = Math.min(previous.length, next.length);
|
|
63
|
+
while (prefixLen < maxLen && previous.charCodeAt(prefixLen) === next.charCodeAt(prefixLen)) {
|
|
64
|
+
prefixLen += 1;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
delta: next.slice(prefixLen),
|
|
68
|
+
nextSnapshot: next,
|
|
69
|
+
nonMonotonic: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createWeiboOutboundStream(params: CreateWeiboOutboundStreamParams) {
|
|
74
|
+
const {
|
|
75
|
+
chunkMode,
|
|
76
|
+
textChunkLimit,
|
|
77
|
+
emit,
|
|
78
|
+
chunkTextWithMode,
|
|
79
|
+
streamDebug,
|
|
80
|
+
} = params;
|
|
81
|
+
|
|
82
|
+
let hasSeenPartial = false;
|
|
83
|
+
let hasEmittedChunks = false;
|
|
84
|
+
let hasEmittedDone = false;
|
|
85
|
+
let lastPartialSnapshot = "";
|
|
86
|
+
let newlineBuffer = "";
|
|
87
|
+
let pendingDeliverBuffer = "";
|
|
88
|
+
|
|
89
|
+
const splitOutboundText = (text: string): string[] =>
|
|
90
|
+
chunkMode === "raw"
|
|
91
|
+
? [text]
|
|
92
|
+
: Array.from(chunkTextWithMode(text, textChunkLimit, chunkMode));
|
|
93
|
+
|
|
94
|
+
const emitChunks = async (chunks: string[], markLastDone: boolean, source: "partial" | "deliver" | "settled"): Promise<boolean> => {
|
|
95
|
+
const normalizedChunks = chunks.filter((chunk) => chunk.length > 0);
|
|
96
|
+
streamDebug?.("emit_chunks_prepare", {
|
|
97
|
+
candidateCount: chunks.length,
|
|
98
|
+
normalizedCount: normalizedChunks.length,
|
|
99
|
+
markLastDone,
|
|
100
|
+
chunkMode,
|
|
101
|
+
source,
|
|
102
|
+
});
|
|
103
|
+
if (normalizedChunks.length === 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (let index = 0; index < normalizedChunks.length; index += 1) {
|
|
108
|
+
const done = markLastDone && index === normalizedChunks.length - 1;
|
|
109
|
+
await emit({
|
|
110
|
+
text: normalizedChunks[index] ?? "",
|
|
111
|
+
done,
|
|
112
|
+
source,
|
|
113
|
+
});
|
|
114
|
+
hasEmittedChunks = true;
|
|
115
|
+
if (done) {
|
|
116
|
+
hasEmittedDone = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const flushText = async (text: string, isFinal: boolean, source: "partial" | "deliver" | "settled"): Promise<boolean> => {
|
|
123
|
+
if (chunkMode === "newline") {
|
|
124
|
+
if (!isFinal) {
|
|
125
|
+
newlineBuffer += text;
|
|
126
|
+
const flushBoundary = findLastParagraphDelimiterEnd(newlineBuffer);
|
|
127
|
+
streamDebug?.("newline_buffered", {
|
|
128
|
+
source,
|
|
129
|
+
incomingLen: text.length,
|
|
130
|
+
bufferLen: newlineBuffer.length,
|
|
131
|
+
flushBoundary,
|
|
132
|
+
});
|
|
133
|
+
if (flushBoundary <= 0) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const flushableText = newlineBuffer.slice(0, flushBoundary);
|
|
137
|
+
newlineBuffer = newlineBuffer.slice(flushBoundary);
|
|
138
|
+
streamDebug?.("newline_flush", {
|
|
139
|
+
source,
|
|
140
|
+
flushableLen: flushableText.length,
|
|
141
|
+
remainingBufferLen: newlineBuffer.length,
|
|
142
|
+
});
|
|
143
|
+
return emitChunks(splitOutboundText(flushableText), false, source);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const textToSend = `${newlineBuffer}${text}`;
|
|
147
|
+
newlineBuffer = "";
|
|
148
|
+
return emitChunks(splitOutboundText(textToSend), true, source);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return emitChunks(splitOutboundText(text), isFinal, source);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const finalizeWithDoneMarker = async (source: "deliver" | "settled"): Promise<void> => {
|
|
155
|
+
if (hasEmittedDone) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const emitted = await flushText("", true, source);
|
|
159
|
+
if (!emitted && hasEmittedChunks && !hasEmittedDone) {
|
|
160
|
+
streamDebug?.("emit_done_marker", { source, reason: "final_without_text" });
|
|
161
|
+
await emit({
|
|
162
|
+
text: "",
|
|
163
|
+
done: true,
|
|
164
|
+
source,
|
|
165
|
+
});
|
|
166
|
+
hasEmittedDone = true;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
async pushPartialSnapshot(snapshot: string): Promise<void> {
|
|
172
|
+
if (!snapshot) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
hasSeenPartial = true;
|
|
177
|
+
const previousSnapshotLen = lastPartialSnapshot.length;
|
|
178
|
+
const deltaResult = resolveDeltaFromSnapshot(lastPartialSnapshot, snapshot);
|
|
179
|
+
lastPartialSnapshot = deltaResult.nextSnapshot;
|
|
180
|
+
|
|
181
|
+
streamDebug?.("partial_snapshot", {
|
|
182
|
+
snapshotLen: snapshot.length,
|
|
183
|
+
prevLen: previousSnapshotLen,
|
|
184
|
+
deltaLen: deltaResult.delta.length,
|
|
185
|
+
nonMonotonic: deltaResult.nonMonotonic,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!deltaResult.delta) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await flushText(deltaResult.delta, false, "partial");
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async pushDeliverText(params: { text: string; isFinal: boolean }): Promise<void> {
|
|
195
|
+
const text = params.text ?? "";
|
|
196
|
+
if (!params.isFinal) {
|
|
197
|
+
if (!hasSeenPartial && text.length > 0) {
|
|
198
|
+
pendingDeliverBuffer += text;
|
|
199
|
+
streamDebug?.("deliver_buffered", {
|
|
200
|
+
kind: "block",
|
|
201
|
+
incomingLen: text.length,
|
|
202
|
+
pendingDeliverBufferLen: pendingDeliverBuffer.length,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (hasSeenPartial) {
|
|
209
|
+
if (text.length > 0) {
|
|
210
|
+
await this.pushPartialSnapshot(text);
|
|
211
|
+
}
|
|
212
|
+
await finalizeWithDoneMarker("deliver");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const fallbackText = `${pendingDeliverBuffer}${text}`;
|
|
217
|
+
pendingDeliverBuffer = "";
|
|
218
|
+
await flushText(fallbackText, true, "deliver");
|
|
219
|
+
await finalizeWithDoneMarker("deliver");
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async settle(): Promise<void> {
|
|
223
|
+
if (!hasSeenPartial && pendingDeliverBuffer.length > 0) {
|
|
224
|
+
const buffered = pendingDeliverBuffer;
|
|
225
|
+
pendingDeliverBuffer = "";
|
|
226
|
+
await flushText(buffered, true, "settled");
|
|
227
|
+
}
|
|
228
|
+
await finalizeWithDoneMarker("settled");
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
snapshot(): StreamStateSnapshot {
|
|
232
|
+
return {
|
|
233
|
+
hasSeenPartial,
|
|
234
|
+
hasEmittedChunks,
|
|
235
|
+
hasEmittedDone,
|
|
236
|
+
newlineBufferLen: newlineBuffer.length,
|
|
237
|
+
pendingDeliverBufferLen: pendingDeliverBuffer.length,
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOutboundAdapter,
|
|
3
|
+
ChannelOutboundContext,
|
|
4
|
+
} from "openclaw/plugin-sdk";
|
|
5
|
+
import { sendMessageWeibo } from "./send.js";
|
|
6
|
+
|
|
7
|
+
// Simple text chunker - splits by character length
|
|
8
|
+
// Mode is handled by the SDK's chunkTextWithMode helper
|
|
9
|
+
function chunkText(text: string, limit: number): string[] {
|
|
10
|
+
if (text.length <= limit) {
|
|
11
|
+
return [text];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const chunks: string[] = [];
|
|
15
|
+
for (let i = 0; i < text.length; i += limit) {
|
|
16
|
+
chunks.push(text.slice(i, i + limit));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return chunks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const weiboOutbound: ChannelOutboundAdapter = {
|
|
23
|
+
deliveryMode: "direct",
|
|
24
|
+
chunker: chunkText,
|
|
25
|
+
chunkerMode: "text",
|
|
26
|
+
textChunkLimit: 2000, // Weibo DM text limit is around 2000 chars
|
|
27
|
+
|
|
28
|
+
sendText: async (ctx: ChannelOutboundContext) => {
|
|
29
|
+
const { cfg, to, text, accountId } = ctx;
|
|
30
|
+
|
|
31
|
+
const result = await sendMessageWeibo({
|
|
32
|
+
cfg,
|
|
33
|
+
to: to ?? "",
|
|
34
|
+
text: text ?? "",
|
|
35
|
+
accountId: accountId ?? undefined,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
channel: "weibo" as const,
|
|
40
|
+
messageId: result.messageId,
|
|
41
|
+
chatId: result.chatId,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
sendMedia: async (_ctx: ChannelOutboundContext) => {
|
|
46
|
+
// Weibo plugin doesn't support media
|
|
47
|
+
throw new Error("Weibo channel does not support media messages");
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as pluginSdk from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export type MediaItem = {
|
|
4
|
+
path: string;
|
|
5
|
+
contentType?: string | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type AgentMediaPayload = {
|
|
9
|
+
MediaPath?: string;
|
|
10
|
+
MediaPaths?: string[];
|
|
11
|
+
MediaType?: string;
|
|
12
|
+
MediaTypes?: string[];
|
|
13
|
+
MediaUrl?: string;
|
|
14
|
+
MediaUrls?: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PluginSdkCompat = {
|
|
18
|
+
buildAgentMediaPayload?: (mediaList: MediaItem[]) => AgentMediaPayload;
|
|
19
|
+
buildMediaPayload?: (mediaList: MediaItem[]) => AgentMediaPayload;
|
|
20
|
+
waitUntilAbort?: (abortSignal?: AbortSignal) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function buildAgentMediaPayloadFallback(mediaList: MediaItem[]): AgentMediaPayload {
|
|
24
|
+
const normalized = mediaList.filter((item) => typeof item.path === "string" && item.path.trim().length > 0);
|
|
25
|
+
if (normalized.length === 0) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const paths = normalized.map((item) => item.path);
|
|
30
|
+
const types = normalized.map((item) => item.contentType ?? undefined).filter((value): value is string => typeof value === "string");
|
|
31
|
+
const payload: AgentMediaPayload = {
|
|
32
|
+
MediaPath: paths[0],
|
|
33
|
+
MediaPaths: paths,
|
|
34
|
+
MediaUrl: paths[0],
|
|
35
|
+
MediaUrls: paths,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (types.length > 0) {
|
|
39
|
+
payload.MediaType = types[0];
|
|
40
|
+
payload.MediaTypes = types;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return payload;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function waitUntilAbortFallback(abortSignal?: AbortSignal): Promise<void> {
|
|
47
|
+
if (!abortSignal) {
|
|
48
|
+
return new Promise<void>(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
if (abortSignal.aborted) {
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
}
|
|
53
|
+
return new Promise<void>((resolve) => {
|
|
54
|
+
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildAgentMediaPayloadCompat(
|
|
59
|
+
mediaList: MediaItem[],
|
|
60
|
+
sdk: PluginSdkCompat = pluginSdk as PluginSdkCompat,
|
|
61
|
+
): AgentMediaPayload {
|
|
62
|
+
if (typeof sdk.buildAgentMediaPayload === "function") {
|
|
63
|
+
return sdk.buildAgentMediaPayload(mediaList);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof sdk.buildMediaPayload === "function") {
|
|
67
|
+
return sdk.buildMediaPayload(mediaList);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return buildAgentMediaPayloadFallback(mediaList);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function waitUntilAbortCompat(
|
|
74
|
+
abortSignal?: AbortSignal,
|
|
75
|
+
sdk: PluginSdkCompat = pluginSdk as PluginSdkCompat,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
if (typeof sdk.waitUntilAbort === "function") {
|
|
78
|
+
return sdk.waitUntilAbort(abortSignal);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return waitUntilAbortFallback(abortSignal);
|
|
82
|
+
}
|
package/src/policy.ts
ADDED
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let weiboRuntime: PluginRuntime | undefined;
|
|
4
|
+
|
|
5
|
+
export function setWeiboRuntime(runtime: PluginRuntime): void {
|
|
6
|
+
weiboRuntime = runtime;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getWeiboRuntime(): PluginRuntime {
|
|
10
|
+
if (!weiboRuntime) {
|
|
11
|
+
throw new Error("Weibo runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return weiboRuntime;
|
|
14
|
+
}
|
package/src/send.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { WeiboSendResult } from "./types.js";
|
|
3
|
+
import { resolveWeiboAccount } from "./accounts.js";
|
|
4
|
+
import { createWeiboClient } from "./client.js";
|
|
5
|
+
import { normalizeWeiboTarget } from "./targets.js";
|
|
6
|
+
|
|
7
|
+
export type SendWeiboMessageParams = {
|
|
8
|
+
cfg: ClawdbotConfig;
|
|
9
|
+
to: string;
|
|
10
|
+
text: string;
|
|
11
|
+
accountId?: string;
|
|
12
|
+
messageId?: string;
|
|
13
|
+
chunkId?: number;
|
|
14
|
+
done?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function generateWeiboMessageId(): string {
|
|
18
|
+
return `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeChunkId(chunkId?: number): number {
|
|
22
|
+
if (typeof chunkId !== "number" || !Number.isFinite(chunkId) || chunkId < 0) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
return Math.floor(chunkId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function sendMessageWeibo(params: SendWeiboMessageParams): Promise<WeiboSendResult> {
|
|
29
|
+
const { cfg, to, text, accountId, messageId, chunkId, done } = params;
|
|
30
|
+
const streamDebugEnabled = process.env.WEIBO_STREAM_DEBUG === "1";
|
|
31
|
+
const account = resolveWeiboAccount({ cfg, accountId });
|
|
32
|
+
|
|
33
|
+
if (!account.configured) {
|
|
34
|
+
throw new Error(`Weibo account "${account.accountId}" not configured`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const client = createWeiboClient(account);
|
|
38
|
+
const receiveId = normalizeWeiboTarget(to);
|
|
39
|
+
|
|
40
|
+
if (!receiveId) {
|
|
41
|
+
throw new Error(`Invalid Weibo target: ${to}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const userId = receiveId.replace(/^user:/, "");
|
|
45
|
+
const outboundMessageId = typeof messageId === "string" && messageId.trim()
|
|
46
|
+
? messageId.trim()
|
|
47
|
+
: generateWeiboMessageId();
|
|
48
|
+
const outboundChunkId = normalizeChunkId(chunkId);
|
|
49
|
+
const outboundDone = typeof done === "boolean" ? done : true;
|
|
50
|
+
|
|
51
|
+
client.send({
|
|
52
|
+
type: "send_message",
|
|
53
|
+
payload: {
|
|
54
|
+
toUserId: userId,
|
|
55
|
+
text: text ?? "",
|
|
56
|
+
messageId: outboundMessageId,
|
|
57
|
+
chunkId: outboundChunkId,
|
|
58
|
+
done: outboundDone,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (streamDebugEnabled) {
|
|
62
|
+
console.log(
|
|
63
|
+
`[weibo][stream-debug] ws_send ${JSON.stringify({
|
|
64
|
+
toUserId: userId,
|
|
65
|
+
messageId: outboundMessageId,
|
|
66
|
+
chunkId: outboundChunkId,
|
|
67
|
+
done: outboundDone,
|
|
68
|
+
textLen: (text ?? "").length,
|
|
69
|
+
preview: (text ?? "").slice(0, 80),
|
|
70
|
+
})}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
messageId: outboundMessageId,
|
|
76
|
+
chatId: receiveId,
|
|
77
|
+
chunkId: outboundChunkId,
|
|
78
|
+
done: outboundDone,
|
|
79
|
+
};
|
|
80
|
+
}
|