@shenhh/popo-native 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/index.ts +53 -0
- package/package.json +65 -0
- package/src/accounts.ts +52 -0
- package/src/auth.ts +86 -0
- package/src/bot.ts +363 -0
- package/src/cards.ts +217 -0
- package/src/channel.ts +709 -0
- package/src/client.ts +118 -0
- package/src/config-schema.ts +89 -0
- package/src/crypto.ts +66 -0
- package/src/media.ts +603 -0
- package/src/monitor.ts +261 -0
- package/src/outbound.ts +138 -0
- package/src/policy.ts +93 -0
- package/src/probe.ts +29 -0
- package/src/reply-dispatcher.ts +126 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +424 -0
- package/src/subscription.ts +171 -0
- package/src/targets.ts +68 -0
- package/src/team.ts +460 -0
- package/src/types.ts +104 -0
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { registerPluginHttpRoute, normalizePluginHttpPath } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
|
4
|
+
import type { PopoNativeConfig } from "./types.js";
|
|
5
|
+
import { resolvePopoNativeCredentials } from "./accounts.js";
|
|
6
|
+
import { verifySignature, decryptMessage, encryptMessage } from "./crypto.js";
|
|
7
|
+
import { handlePopoNativeMessage, type PopoNativeMessageEvent } from "./bot.js";
|
|
8
|
+
import { probePopoNative } from "./probe.js";
|
|
9
|
+
import { configureSubscription } from "./subscription.js";
|
|
10
|
+
|
|
11
|
+
export type MonitorPopoNativeOpts = {
|
|
12
|
+
config?: ClawdbotConfig;
|
|
13
|
+
runtime?: RuntimeEnv;
|
|
14
|
+
abortSignal?: AbortSignal;
|
|
15
|
+
accountId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Helper function to read request body
|
|
19
|
+
function readRequestBody(req: http.IncomingMessage): Promise<string> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const chunks: Buffer[] = [];
|
|
22
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
23
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
24
|
+
req.on("error", reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function monitorPopoNativeProvider(opts: MonitorPopoNativeOpts = {}): Promise<void> {
|
|
29
|
+
const cfg = opts.config;
|
|
30
|
+
if (!cfg) {
|
|
31
|
+
throw new Error("Config is required for POPO Native monitor");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const popoCfg = cfg.channels?.["popo-native"] as PopoNativeConfig | undefined;
|
|
35
|
+
const creds = resolvePopoNativeCredentials(popoCfg);
|
|
36
|
+
if (!creds) {
|
|
37
|
+
throw new Error("POPO Native credentials not configured (appId, appSecret required)");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const log = opts.runtime?.log ?? console.log;
|
|
41
|
+
const error = opts.runtime?.error ?? console.error;
|
|
42
|
+
|
|
43
|
+
// Verify credentials by getting a token
|
|
44
|
+
const probeResult = await probePopoNative(popoCfg);
|
|
45
|
+
if (!probeResult.ok) {
|
|
46
|
+
throw new Error(`POPO Native probe failed: ${probeResult.error}`);
|
|
47
|
+
}
|
|
48
|
+
log(`popo-native: credentials verified for appId ${probeResult.appId}`);
|
|
49
|
+
|
|
50
|
+
const webhookPath = popoCfg?.webhookPath?.trim() || "/popo-native/events";
|
|
51
|
+
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
52
|
+
|
|
53
|
+
// Normalize path
|
|
54
|
+
const normalizedPath = normalizePluginHttpPath(webhookPath, "/popo-native/events") ?? "/popo-native/events";
|
|
55
|
+
|
|
56
|
+
// Configure event subscription if subscription config is provided
|
|
57
|
+
if (popoCfg?.subscription) {
|
|
58
|
+
try {
|
|
59
|
+
const webhookUrl = `${cfg.server?.publicUrl ?? ""}${normalizedPath}`;
|
|
60
|
+
const subResult = await configureSubscription({
|
|
61
|
+
cfg,
|
|
62
|
+
webhookUrl,
|
|
63
|
+
uid: popoCfg.subscription.robotUid,
|
|
64
|
+
authTypes: popoCfg.subscription.authTypes,
|
|
65
|
+
teams: popoCfg.subscription.teams,
|
|
66
|
+
});
|
|
67
|
+
if (subResult.success) {
|
|
68
|
+
log(`popo-native: event subscription configured successfully`);
|
|
69
|
+
} else {
|
|
70
|
+
error(`popo-native: failed to configure event subscription: ${subResult.error}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
error(`popo-native: error configuring subscription: ${String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Register HTTP route to gateway
|
|
78
|
+
const unregisterHttp = registerPluginHttpRoute({
|
|
79
|
+
path: normalizedPath,
|
|
80
|
+
pluginId: "popo-native",
|
|
81
|
+
accountId: opts.accountId,
|
|
82
|
+
log: (msg: string) => log(msg),
|
|
83
|
+
handler: async (req, res) => {
|
|
84
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
85
|
+
log(`popo-native: received ${req.method} request to ${url.pathname}`);
|
|
86
|
+
|
|
87
|
+
// Handle CORS preflight
|
|
88
|
+
if (req.method === "OPTIONS") {
|
|
89
|
+
res.writeHead(200, {
|
|
90
|
+
"Access-Control-Allow-Origin": "*",
|
|
91
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
92
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
93
|
+
});
|
|
94
|
+
res.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle URL validation (GET request)
|
|
99
|
+
if (req.method === "GET") {
|
|
100
|
+
const nonce = url.searchParams.get("nonce");
|
|
101
|
+
const timestamp = url.searchParams.get("timestamp");
|
|
102
|
+
const signature = url.searchParams.get("signature");
|
|
103
|
+
|
|
104
|
+
log(`popo-native: URL validation attempt - nonce=${nonce}, timestamp=${timestamp}, signature=${signature}`);
|
|
105
|
+
|
|
106
|
+
if (nonce && timestamp && signature && creds.token) {
|
|
107
|
+
const valid = verifySignature({
|
|
108
|
+
token: creds.token,
|
|
109
|
+
nonce,
|
|
110
|
+
timestamp,
|
|
111
|
+
signature,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (valid) {
|
|
115
|
+
log(`popo-native: URL validation successful`);
|
|
116
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
117
|
+
res.end(nonce);
|
|
118
|
+
return;
|
|
119
|
+
} else {
|
|
120
|
+
log(`popo-native: signature verification failed`);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
log(`popo-native: missing required parameters for validation`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
res.writeHead(400);
|
|
127
|
+
res.end("Invalid validation request");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle webhook event (POST request)
|
|
132
|
+
if (req.method === "POST") {
|
|
133
|
+
try {
|
|
134
|
+
const body = await readRequestBody(req);
|
|
135
|
+
const payload = JSON.parse(body);
|
|
136
|
+
|
|
137
|
+
// Check for encrypted payload
|
|
138
|
+
let eventData: unknown;
|
|
139
|
+
if (payload.encrypt && creds.aesKey) {
|
|
140
|
+
// Verify signature first
|
|
141
|
+
const { nonce, timestamp, signature } = payload;
|
|
142
|
+
if (nonce && timestamp && signature && creds.token) {
|
|
143
|
+
const valid = verifySignature({
|
|
144
|
+
token: creds.token,
|
|
145
|
+
nonce,
|
|
146
|
+
timestamp,
|
|
147
|
+
signature,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!valid) {
|
|
151
|
+
log(`popo-native: invalid signature in webhook event`);
|
|
152
|
+
res.writeHead(403);
|
|
153
|
+
res.end("Invalid signature");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Decrypt the message
|
|
159
|
+
const decrypted = decryptMessage(payload.encrypt, creds.aesKey);
|
|
160
|
+
eventData = JSON.parse(decrypted);
|
|
161
|
+
} else {
|
|
162
|
+
eventData = payload;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const event = eventData as { eventType?: string };
|
|
166
|
+
|
|
167
|
+
// Handle valid_url event
|
|
168
|
+
if (event.eventType === "valid_url") {
|
|
169
|
+
log(`popo-native: received valid_url event`);
|
|
170
|
+
const response = { eventType: "valid_url" };
|
|
171
|
+
|
|
172
|
+
if (creds.aesKey) {
|
|
173
|
+
const encrypted = encryptMessage(JSON.stringify(response), creds.aesKey);
|
|
174
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
175
|
+
res.end(JSON.stringify({ encrypt: encrypted }));
|
|
176
|
+
} else {
|
|
177
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
178
|
+
res.end(JSON.stringify(response));
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle MSG_SEND events (native API event type)
|
|
184
|
+
if (event.eventType === "MSG_SEND") {
|
|
185
|
+
const messageEvent = eventData as PopoNativeMessageEvent;
|
|
186
|
+
log(`popo-native: received MSG_SEND event`);
|
|
187
|
+
|
|
188
|
+
// Process message asynchronously
|
|
189
|
+
handlePopoNativeMessage({
|
|
190
|
+
cfg,
|
|
191
|
+
event: messageEvent,
|
|
192
|
+
runtime: opts.runtime,
|
|
193
|
+
chatHistories,
|
|
194
|
+
}).catch((err) => {
|
|
195
|
+
error(`popo-native: error handling message: ${String(err)}`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handle MSG_RECALL events
|
|
200
|
+
if (event.eventType === "MSG_RECALL") {
|
|
201
|
+
log(`popo-native: received MSG_RECALL event`);
|
|
202
|
+
// TODO: Handle message recall if needed
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle ACTION events (card interactions)
|
|
206
|
+
if (event.eventType === "ACTION") {
|
|
207
|
+
log(`popo-native: received ACTION event`);
|
|
208
|
+
// TODO: Implement card action handling if needed
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Return success response
|
|
212
|
+
const successResponse = { success: true };
|
|
213
|
+
if (creds.aesKey) {
|
|
214
|
+
const encrypted = encryptMessage(JSON.stringify(successResponse), creds.aesKey);
|
|
215
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
216
|
+
res.end(JSON.stringify({ encrypt: encrypted }));
|
|
217
|
+
} else {
|
|
218
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
219
|
+
res.end(JSON.stringify(successResponse));
|
|
220
|
+
}
|
|
221
|
+
} catch (err) {
|
|
222
|
+
error(`popo-native: error processing webhook: ${String(err)}`);
|
|
223
|
+
res.writeHead(500);
|
|
224
|
+
res.end("Internal Server Error");
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
res.writeHead(405);
|
|
230
|
+
res.end("Method Not Allowed");
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
log(`popo-native: registered webhook handler at ${normalizedPath}`);
|
|
235
|
+
|
|
236
|
+
// Handle abort signal
|
|
237
|
+
const stopHandler = () => {
|
|
238
|
+
log("popo-native: stopping provider");
|
|
239
|
+
unregisterHttp();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (opts.abortSignal?.aborted) {
|
|
243
|
+
stopHandler();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
opts.abortSignal?.addEventListener("abort", stopHandler, { once: true });
|
|
248
|
+
|
|
249
|
+
// Keep promise pending until abort
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
const handler = () => {
|
|
252
|
+
stopHandler();
|
|
253
|
+
resolve();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (opts.abortSignal) {
|
|
257
|
+
opts.abortSignal.removeEventListener("abort", stopHandler);
|
|
258
|
+
opts.abortSignal.addEventListener("abort", handler, { once: true });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getPopoNativeRuntime } from "./runtime.js";
|
|
3
|
+
import {
|
|
4
|
+
sendMessagePopoNative,
|
|
5
|
+
sendCardPopoNative,
|
|
6
|
+
createStreamCardPopoNative,
|
|
7
|
+
updateStreamCardPopoNative,
|
|
8
|
+
} from "./send.js";
|
|
9
|
+
import { sendMediaPopoNative } from "./media.js";
|
|
10
|
+
|
|
11
|
+
export const popoNativeOutbound: ChannelOutboundAdapter = {
|
|
12
|
+
deliveryMode: "direct",
|
|
13
|
+
chunker: (text, limit) => getPopoNativeRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
14
|
+
chunkerMode: "markdown",
|
|
15
|
+
textChunkLimit: 4000,
|
|
16
|
+
sendText: async ({ cfg, to, text }) => {
|
|
17
|
+
const result = await sendMessagePopoNative({ cfg, to, text });
|
|
18
|
+
return { channel: "popo-native", ...result };
|
|
19
|
+
},
|
|
20
|
+
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
|
21
|
+
// Send text first if provided
|
|
22
|
+
if (text?.trim()) {
|
|
23
|
+
await sendMessagePopoNative({ cfg, to, text });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Upload and send media if URL provided
|
|
27
|
+
if (mediaUrl) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await sendMediaPopoNative({ cfg, to, mediaUrl });
|
|
30
|
+
return { channel: "popo-native", ...result };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Log the error for debugging
|
|
33
|
+
console.error(`[popo-native] sendMediaPopoNative failed:`, err);
|
|
34
|
+
// Fallback to URL link if upload fails
|
|
35
|
+
const fallbackText = `📎 ${mediaUrl}`;
|
|
36
|
+
const result = await sendMessagePopoNative({ cfg, to, text: fallbackText });
|
|
37
|
+
return { channel: "popo-native", ...result };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// No media URL, just return text result
|
|
42
|
+
const result = await sendMessagePopoNative({ cfg, to, text: text ?? "" });
|
|
43
|
+
return { channel: "popo-native", ...result };
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
sendCard: async ({ cfg, to, card }) => {
|
|
47
|
+
const {
|
|
48
|
+
templateUuid,
|
|
49
|
+
instanceUuid,
|
|
50
|
+
callBackConfigKey,
|
|
51
|
+
publicVariableMap,
|
|
52
|
+
batchPrivateVariableMap,
|
|
53
|
+
options,
|
|
54
|
+
} = card as {
|
|
55
|
+
templateUuid: string;
|
|
56
|
+
instanceUuid: string;
|
|
57
|
+
callBackConfigKey?: string;
|
|
58
|
+
publicVariableMap?: Record<string, unknown>;
|
|
59
|
+
batchPrivateVariableMap?: Record<string, Record<string, unknown>>;
|
|
60
|
+
options?: import("./send.js").PopoNativeCardOptions;
|
|
61
|
+
};
|
|
62
|
+
const result = await sendCardPopoNative({
|
|
63
|
+
cfg,
|
|
64
|
+
to,
|
|
65
|
+
templateUuid,
|
|
66
|
+
instanceUuid,
|
|
67
|
+
callBackConfigKey,
|
|
68
|
+
publicVariableMap,
|
|
69
|
+
batchPrivateVariableMap,
|
|
70
|
+
options,
|
|
71
|
+
});
|
|
72
|
+
return { channel: "popo-native", ...result };
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Streaming card support
|
|
76
|
+
createStreamCard: async ({ cfg, to, card }) => {
|
|
77
|
+
const {
|
|
78
|
+
templateUuid,
|
|
79
|
+
instanceUuid,
|
|
80
|
+
robotAccount,
|
|
81
|
+
fromUser,
|
|
82
|
+
sessionType,
|
|
83
|
+
callbackKey,
|
|
84
|
+
initialContent,
|
|
85
|
+
} = card as {
|
|
86
|
+
templateUuid: string;
|
|
87
|
+
instanceUuid: string;
|
|
88
|
+
robotAccount: string;
|
|
89
|
+
fromUser?: string;
|
|
90
|
+
sessionType?: number;
|
|
91
|
+
callbackKey?: string;
|
|
92
|
+
initialContent?: string;
|
|
93
|
+
};
|
|
94
|
+
const result = await createStreamCardPopoNative({
|
|
95
|
+
cfg,
|
|
96
|
+
to,
|
|
97
|
+
templateUuid,
|
|
98
|
+
instanceUuid,
|
|
99
|
+
robotAccount,
|
|
100
|
+
fromUser,
|
|
101
|
+
sessionType,
|
|
102
|
+
callbackKey,
|
|
103
|
+
initialContent,
|
|
104
|
+
});
|
|
105
|
+
return { channel: "popo-native", success: result.success, instanceUuid: result.instanceUuid };
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
updateStreamCard: async ({ cfg, card }) => {
|
|
109
|
+
const {
|
|
110
|
+
templateUuid,
|
|
111
|
+
instanceUuid,
|
|
112
|
+
content,
|
|
113
|
+
sequence,
|
|
114
|
+
isFinalize,
|
|
115
|
+
isError,
|
|
116
|
+
streamKey,
|
|
117
|
+
} = card as {
|
|
118
|
+
templateUuid: string;
|
|
119
|
+
instanceUuid: string;
|
|
120
|
+
content: string;
|
|
121
|
+
sequence: number;
|
|
122
|
+
isFinalize?: boolean;
|
|
123
|
+
isError?: boolean;
|
|
124
|
+
streamKey?: string;
|
|
125
|
+
};
|
|
126
|
+
const result = await updateStreamCardPopoNative({
|
|
127
|
+
cfg,
|
|
128
|
+
templateUuid,
|
|
129
|
+
instanceUuid,
|
|
130
|
+
content,
|
|
131
|
+
sequence,
|
|
132
|
+
isFinalize,
|
|
133
|
+
isError,
|
|
134
|
+
streamKey,
|
|
135
|
+
});
|
|
136
|
+
return { channel: "popo-native", success: result.success };
|
|
137
|
+
},
|
|
138
|
+
};
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { PopoNativeConfig } from "./types.js";
|
|
3
|
+
import type { PopoNativeGroupConfig } from "./config-schema.js";
|
|
4
|
+
|
|
5
|
+
export type PopoNativeAllowlistMatch = {
|
|
6
|
+
allowed: boolean;
|
|
7
|
+
matchKey?: string;
|
|
8
|
+
matchSource?: "wildcard" | "id" | "name";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function resolvePopoNativeAllowlistMatch(params: {
|
|
12
|
+
allowFrom: Array<string | number>;
|
|
13
|
+
senderId: string;
|
|
14
|
+
senderName?: string | null;
|
|
15
|
+
}): PopoNativeAllowlistMatch {
|
|
16
|
+
const allowFrom = params.allowFrom
|
|
17
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
|
|
20
|
+
if (allowFrom.length === 0) return { allowed: false };
|
|
21
|
+
if (allowFrom.includes("*")) {
|
|
22
|
+
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const senderId = params.senderId.toLowerCase();
|
|
26
|
+
if (allowFrom.includes(senderId)) {
|
|
27
|
+
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const senderName = params.senderName?.toLowerCase();
|
|
31
|
+
if (senderName && allowFrom.includes(senderName)) {
|
|
32
|
+
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { allowed: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolvePopoNativeGroupConfig(params: {
|
|
39
|
+
cfg?: PopoNativeConfig;
|
|
40
|
+
groupId?: string | null;
|
|
41
|
+
}): PopoNativeGroupConfig | undefined {
|
|
42
|
+
const groups = params.cfg?.groups ?? {};
|
|
43
|
+
const groupId = params.groupId?.trim();
|
|
44
|
+
if (!groupId) return undefined;
|
|
45
|
+
|
|
46
|
+
const direct = groups[groupId] as PopoNativeGroupConfig | undefined;
|
|
47
|
+
if (direct) return direct;
|
|
48
|
+
|
|
49
|
+
const lowered = groupId.toLowerCase();
|
|
50
|
+
const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
|
|
51
|
+
return matchKey ? (groups[matchKey] as PopoNativeGroupConfig | undefined) : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolvePopoNativeGroupToolPolicy(
|
|
55
|
+
params: ChannelGroupContext
|
|
56
|
+
): GroupToolPolicyConfig | undefined {
|
|
57
|
+
const cfg = params.cfg.channels?.["popo-native"] as PopoNativeConfig | undefined;
|
|
58
|
+
if (!cfg) return undefined;
|
|
59
|
+
|
|
60
|
+
const groupConfig = resolvePopoNativeGroupConfig({
|
|
61
|
+
cfg,
|
|
62
|
+
groupId: params.groupId,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return groupConfig?.tools;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isPopoNativeGroupAllowed(params: {
|
|
69
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
70
|
+
allowFrom: Array<string | number>;
|
|
71
|
+
senderId: string;
|
|
72
|
+
senderName?: string | null;
|
|
73
|
+
}): boolean {
|
|
74
|
+
const { groupPolicy } = params;
|
|
75
|
+
if (groupPolicy === "disabled") return false;
|
|
76
|
+
if (groupPolicy === "open") return true;
|
|
77
|
+
return resolvePopoNativeAllowlistMatch(params).allowed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolvePopoNativeReplyPolicy(params: {
|
|
81
|
+
isDirectMessage: boolean;
|
|
82
|
+
globalConfig?: PopoNativeConfig;
|
|
83
|
+
groupConfig?: PopoNativeGroupConfig;
|
|
84
|
+
}): { requireMention: boolean } {
|
|
85
|
+
if (params.isDirectMessage) {
|
|
86
|
+
return { requireMention: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const requireMention =
|
|
90
|
+
params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true;
|
|
91
|
+
|
|
92
|
+
return { requireMention };
|
|
93
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PopoNativeConfig, PopoNativeProbeResult } from "./types.js";
|
|
2
|
+
import { resolvePopoNativeCredentials } from "./accounts.js";
|
|
3
|
+
import { getAccessToken } from "./auth.js";
|
|
4
|
+
|
|
5
|
+
export async function probePopoNative(cfg?: PopoNativeConfig): Promise<PopoNativeProbeResult> {
|
|
6
|
+
const creds = resolvePopoNativeCredentials(cfg);
|
|
7
|
+
if (!creds) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
error: "missing credentials (appId, appSecret)",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Try to get an access token to verify credentials
|
|
16
|
+
await getAccessToken(cfg!);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
appId: creds.appId,
|
|
21
|
+
};
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
appId: creds.appId,
|
|
26
|
+
error: err instanceof Error ? err.message : String(err),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createReplyPrefixContext,
|
|
3
|
+
createTypingCallbacks,
|
|
4
|
+
logTypingFailure,
|
|
5
|
+
type ClawdbotConfig,
|
|
6
|
+
type RuntimeEnv,
|
|
7
|
+
type ReplyPayload,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
9
|
+
import { getPopoNativeRuntime } from "./runtime.js";
|
|
10
|
+
import { sendMessagePopoNative } from "./send.js";
|
|
11
|
+
import type { PopoNativeConfig } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export type CreatePopoNativeReplyDispatcherParams = {
|
|
14
|
+
cfg: ClawdbotConfig;
|
|
15
|
+
agentId: string;
|
|
16
|
+
runtime: RuntimeEnv;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createPopoNativeReplyDispatcher(params: CreatePopoNativeReplyDispatcherParams) {
|
|
21
|
+
const core = getPopoNativeRuntime();
|
|
22
|
+
const { cfg, agentId, sessionId } = params;
|
|
23
|
+
|
|
24
|
+
const prefixContext = createReplyPrefixContext({
|
|
25
|
+
cfg,
|
|
26
|
+
agentId,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Track whether any tool results have been sent
|
|
30
|
+
let hasToolResults = false;
|
|
31
|
+
let toolResultCount = 0;
|
|
32
|
+
|
|
33
|
+
// POPO doesn't have a native typing indicator API
|
|
34
|
+
const typingCallbacks = createTypingCallbacks({
|
|
35
|
+
start: async () => {
|
|
36
|
+
// No-op for POPO
|
|
37
|
+
},
|
|
38
|
+
stop: async () => {
|
|
39
|
+
// No-op for POPO
|
|
40
|
+
},
|
|
41
|
+
onStartError: (err) => {
|
|
42
|
+
logTypingFailure({
|
|
43
|
+
log: (message) => params.runtime.log?.(message),
|
|
44
|
+
channel: "popo-native",
|
|
45
|
+
action: "start",
|
|
46
|
+
error: err,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
onStopError: (err) => {
|
|
50
|
+
logTypingFailure({
|
|
51
|
+
log: (message) => params.runtime.log?.(message),
|
|
52
|
+
channel: "popo-native",
|
|
53
|
+
action: "stop",
|
|
54
|
+
error: err,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const textChunkLimit = core.channel.text.resolveTextChunkLimit({
|
|
60
|
+
cfg,
|
|
61
|
+
channel: "popo-native",
|
|
62
|
+
defaultLimit: 4000,
|
|
63
|
+
});
|
|
64
|
+
const chunkMode = core.channel.text.resolveChunkMode(cfg, "popo-native");
|
|
65
|
+
|
|
66
|
+
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
67
|
+
core.channel.reply.createReplyDispatcherWithTyping({
|
|
68
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
69
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
70
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
71
|
+
onReplyStart: typingCallbacks.onReplyStart,
|
|
72
|
+
deliver: async (payload: ReplyPayload) => {
|
|
73
|
+
params.runtime.log?.(`popo-native deliver called: text=${payload.text?.slice(0, 100)}`);
|
|
74
|
+
const text = payload.text ?? "";
|
|
75
|
+
|
|
76
|
+
// Build tool execution indicator if tools were used
|
|
77
|
+
let fullText = text;
|
|
78
|
+
if (hasToolResults) {
|
|
79
|
+
const toolIndicator = toolResultCount === 1
|
|
80
|
+
? "🛠️ **使用了 1 个工具**\n\n"
|
|
81
|
+
: `🛠️ **使用了 ${toolResultCount} 个工具**\n\n`;
|
|
82
|
+
fullText = toolIndicator + text;
|
|
83
|
+
// Reset after including in message
|
|
84
|
+
hasToolResults = false;
|
|
85
|
+
toolResultCount = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!fullText.trim()) {
|
|
89
|
+
params.runtime.log?.(`popo-native deliver: empty text, skipping`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const chunks = core.channel.text.chunkTextWithMode(fullText, textChunkLimit, chunkMode);
|
|
94
|
+
params.runtime.log?.(`popo-native deliver: sending ${chunks.length} chunks to ${sessionId}`);
|
|
95
|
+
|
|
96
|
+
for (const chunk of chunks) {
|
|
97
|
+
// Native API only supports raw text mode (no rich_text variant)
|
|
98
|
+
await sendMessagePopoNative({
|
|
99
|
+
cfg,
|
|
100
|
+
to: sessionId,
|
|
101
|
+
text: chunk,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
onError: (err, info) => {
|
|
106
|
+
params.runtime.error?.(`popo-native ${info.kind} reply failed: ${String(err)}`);
|
|
107
|
+
typingCallbacks.onIdle?.();
|
|
108
|
+
},
|
|
109
|
+
onIdle: typingCallbacks.onIdle,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
dispatcher,
|
|
114
|
+
replyOptions: {
|
|
115
|
+
...replyOptions,
|
|
116
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
117
|
+
// Track tool results as they are executed
|
|
118
|
+
onToolResult: (_payload: { text?: string; mediaUrls?: string[] }) => {
|
|
119
|
+
hasToolResults = true;
|
|
120
|
+
toolResultCount++;
|
|
121
|
+
params.runtime.log?.(`popo-native: tracked tool result #${toolResultCount}`);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
markDispatchIdle,
|
|
125
|
+
};
|
|
126
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setPopoNativeRuntime(next: PluginRuntime) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getPopoNativeRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("POPO Native runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|