@shenhh/popo-oa 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/src/channel.ts ADDED
@@ -0,0 +1,209 @@
1
+ import type { ChannelPlugin } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
3
+ import type { ResolvedPopoOaAccount, PopoOaConfig } from "./types.js";
4
+ import { resolvePopoOaAccount, resolvePopoOaCredentials } from "./accounts.js";
5
+ import { popoOaOutbound } from "./outbound.js";
6
+ import { probePopoOa } from "./probe.js";
7
+ import { normalizePopoOaTarget, looksLikePopoOaId } from "./targets.js";
8
+ import { sendTextPopoOa } from "./send.js";
9
+
10
+ const meta = {
11
+ id: "popo-oa",
12
+ label: "POPO 服务号",
13
+ selectionLabel: "POPO 服务号 (网易)",
14
+ docsPath: "/channels/popo-oa",
15
+ docsLabel: "popo-oa",
16
+ blurb: "POPO Official Account messaging via Open API.",
17
+ aliases: [],
18
+ order: 82,
19
+ } as const;
20
+
21
+ export const popoOaPlugin: ChannelPlugin<ResolvedPopoOaAccount> = {
22
+ id: "popo-oa",
23
+ meta: {
24
+ ...meta,
25
+ },
26
+ pairing: {
27
+ idLabel: "openid",
28
+ normalizeAllowEntry: (entry) => entry.replace(/^(popo-oa|user):/i, ""),
29
+ notifyApproval: async ({ cfg, id }) => {
30
+ const account = resolvePopoOaAccount({ cfg });
31
+ await sendTextPopoOa({
32
+ cfg: account,
33
+ to: id,
34
+ text: PAIRING_APPROVED_MESSAGE,
35
+ });
36
+ },
37
+ },
38
+ capabilities: {
39
+ chatTypes: ["direct"], // POPO OA only supports direct messages
40
+ polls: false,
41
+ threads: false,
42
+ media: false, // Limited media support compared to Native API
43
+ reactions: false,
44
+ edit: false,
45
+ reply: false,
46
+ },
47
+ agentPrompt: {
48
+ messageToolHints: () => [
49
+ "- POPO 服务号 targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:OPENID`.",
50
+ "- POPO 服务号 uses OpenID (not email) as user identifier.",
51
+ ],
52
+ },
53
+ reload: { configPrefixes: ["channels.popo-oa"] },
54
+ configSchema: {
55
+ schema: {
56
+ type: "object",
57
+ additionalProperties: false,
58
+ properties: {
59
+ enabled: { type: "boolean" },
60
+ systemPrompt: { type: "string" },
61
+ appId: { type: "string" },
62
+ appSecret: { type: "string" },
63
+ token: { type: "string" },
64
+ server: { type: "string" },
65
+ webhookPath: { type: "string" },
66
+ dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
67
+ allowFrom: { type: "array", items: { type: "string" } },
68
+ historyLimit: { type: "integer", minimum: 0 },
69
+ dmHistoryLimit: { type: "integer", minimum: 0 },
70
+ textChunkLimit: { type: "integer", minimum: 1 },
71
+ chunkMode: { type: "string", enum: ["length", "newline"] },
72
+ },
73
+ },
74
+ },
75
+ config: {
76
+ listAccountIds: () => [DEFAULT_ACCOUNT_ID],
77
+ resolveAccount: (cfg) => resolvePopoOaAccount({ cfg }),
78
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
79
+ setAccountEnabled: ({ cfg, enabled }) => ({
80
+ ...cfg,
81
+ channels: {
82
+ ...cfg.channels,
83
+ "popo-oa": {
84
+ ...cfg.channels?.["popo-oa"],
85
+ enabled,
86
+ },
87
+ },
88
+ }),
89
+ deleteAccount: ({ cfg }) => {
90
+ const next = { ...cfg };
91
+ const nextChannels = { ...cfg.channels };
92
+ delete (nextChannels as Record<string, unknown>)["popo-oa"];
93
+ if (Object.keys(nextChannels).length > 0) {
94
+ next.channels = nextChannels;
95
+ } else {
96
+ delete next.channels;
97
+ }
98
+ return next;
99
+ },
100
+ isConfigured: (_account, cfg) =>
101
+ Boolean(resolvePopoOaCredentials(cfg.channels?.["popo-oa"] as PopoOaConfig | undefined)),
102
+ describeAccount: (account) => ({
103
+ accountId: account.id,
104
+ enabled: account.cfg.enabled !== false,
105
+ configured: Boolean(resolvePopoOaCredentials(account.cfg)),
106
+ }),
107
+ resolveAllowFrom: ({ cfg }) =>
108
+ (cfg.channels?.["popo-oa"] as PopoOaConfig | undefined)?.allowFrom ?? [],
109
+ formatAllowFrom: ({ allowFrom }) =>
110
+ allowFrom
111
+ .map((entry) => String(entry).trim())
112
+ .filter(Boolean),
113
+ },
114
+ security: {
115
+ collectWarnings: ({ cfg }) => {
116
+ const popoCfg = cfg.channels?.["popo-oa"] as PopoOaConfig | undefined;
117
+ const dmPolicy = popoCfg?.dmPolicy ?? "pairing";
118
+ if (dmPolicy !== "open") return [];
119
+ return [
120
+ `- POPO 服务号: dmPolicy="open" allows any user to interact. Set channels.popo-oa.dmPolicy="allowlist" + channels.popo-oa.allowFrom to restrict users.`,
121
+ ];
122
+ },
123
+ },
124
+ setup: {
125
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
126
+ applyAccountConfig: ({ cfg }) => ({
127
+ ...cfg,
128
+ channels: {
129
+ ...cfg.channels,
130
+ "popo-oa": {
131
+ ...cfg.channels?.["popo-oa"],
132
+ enabled: true,
133
+ },
134
+ },
135
+ }),
136
+ },
137
+ messaging: {
138
+ normalizeTarget: normalizePopoOaTarget,
139
+ targetResolver: {
140
+ looksLikeId: looksLikePopoOaId,
141
+ hint: "<openid|user:OPENID>",
142
+ },
143
+ },
144
+ outbound: popoOaOutbound,
145
+ actions: {
146
+ listActions: ({ cfg }) => {
147
+ const enabled = cfg.channels?.["popo-oa"]?.enabled !== false;
148
+ if (!enabled) return [];
149
+ return ["send"];
150
+ },
151
+ supportsCards: () => false, // POPO OA doesn't support cards like Native API
152
+ handleAction: async (ctx) => {
153
+ const { action, params, cfg } = ctx;
154
+
155
+ if (action === "send") {
156
+ const to =
157
+ typeof params.to === "string"
158
+ ? params.to.trim()
159
+ : typeof params.target === "string"
160
+ ? params.target.trim()
161
+ : "";
162
+ const text =
163
+ typeof params.text === "string"
164
+ ? params.text
165
+ : typeof params.message === "string"
166
+ ? params.message
167
+ : "";
168
+
169
+ if (!to) {
170
+ return {
171
+ isError: true,
172
+ content: [{ type: "text", text: "Send requires a target (to)." }],
173
+ };
174
+ }
175
+
176
+ const account = resolvePopoOaAccount({ cfg });
177
+ const result = await sendTextPopoOa({ cfg: account, to, text });
178
+
179
+ return {
180
+ content: [
181
+ {
182
+ type: "text",
183
+ text: JSON.stringify({
184
+ ok: result.code === 0,
185
+ channel: "popo-oa",
186
+ messageId: result.data?.msgId,
187
+ }),
188
+ },
189
+ ],
190
+ };
191
+ }
192
+
193
+ return {
194
+ isError: true,
195
+ content: [{ type: "text", text: `Unknown action: ${action}` }],
196
+ };
197
+ },
198
+ },
199
+ health: {
200
+ check: async ({ cfg }) => {
201
+ const popoCfg = cfg.channels?.["popo-oa"] as PopoOaConfig | undefined;
202
+ const result = await probePopoOa(popoCfg);
203
+ return {
204
+ ok: result.ok,
205
+ message: result.error,
206
+ };
207
+ },
208
+ },
209
+ };
package/src/client.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { getAccessToken, clearAccessToken } from "./auth.js";
2
+ import type {
3
+ ResolvedPopoOaAccount,
4
+ PopoOaSendMessage,
5
+ SendMessageResponse,
6
+ } from "./types.js";
7
+
8
+ /**
9
+ * HTTP client for POPO OA API.
10
+ */
11
+ export class PopoOaClient {
12
+ private account: ResolvedPopoOaAccount;
13
+
14
+ constructor(account: ResolvedPopoOaAccount) {
15
+ this.account = account;
16
+ }
17
+
18
+ /**
19
+ * Make an authenticated API request.
20
+ */
21
+ async request<T>({
22
+ path,
23
+ method = "GET",
24
+ body,
25
+ query,
26
+ }: {
27
+ path: string;
28
+ method?: "GET" | "POST" | "PUT" | "DELETE";
29
+ body?: unknown;
30
+ query?: Record<string, string>;
31
+ }): Promise<T> {
32
+ const accessToken = await getAccessToken({
33
+ appId: this.account.appId,
34
+ appSecret: this.account.appSecret,
35
+ server: this.account.server,
36
+ });
37
+
38
+ const url = new URL(path, this.account.server);
39
+ url.searchParams.set("access_token", accessToken);
40
+
41
+ if (query) {
42
+ for (const [key, value] of Object.entries(query)) {
43
+ url.searchParams.set(key, value);
44
+ }
45
+ }
46
+
47
+ const response = await fetch(url.toString(), {
48
+ method,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ },
52
+ body: body ? JSON.stringify(body) : undefined,
53
+ });
54
+
55
+ // Handle token expiration
56
+ if (response.status === 401) {
57
+ clearAccessToken(this.account.appId);
58
+ // Retry once with new token
59
+ return this.request({ path, method, body, query });
60
+ }
61
+
62
+ if (!response.ok) {
63
+ const text = await response.text();
64
+ throw new Error(
65
+ `POPO OA API error: ${response.status} ${response.statusText} - ${text}`
66
+ );
67
+ }
68
+
69
+ const data = await response.json();
70
+ return data as T;
71
+ }
72
+
73
+ /**
74
+ * Send a message to a user.
75
+ */
76
+ async sendMessage(message: PopoOaSendMessage): Promise<SendMessageResponse> {
77
+ return this.request<SendMessageResponse>({
78
+ path: "/open/api/v1/message/send",
79
+ method: "POST",
80
+ body: message,
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Send a text message.
86
+ */
87
+ async sendText(openid: string, content: string): Promise<SendMessageResponse> {
88
+ return this.sendMessage({
89
+ openid,
90
+ type: "text",
91
+ body: { content },
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Send an image message.
97
+ */
98
+ async sendImage(openid: string, mediaId: string): Promise<SendMessageResponse> {
99
+ return this.sendMessage({
100
+ openid,
101
+ type: "image",
102
+ body: { mediaId },
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Send a news (article) message.
108
+ */
109
+ async sendNews(
110
+ openid: string,
111
+ articles: Array<{
112
+ title: string;
113
+ description: string;
114
+ url: string;
115
+ picurl?: string;
116
+ }>
117
+ ): Promise<SendMessageResponse> {
118
+ return this.sendMessage({
119
+ openid,
120
+ type: "news",
121
+ body: { articles },
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Send a list/select message.
127
+ */
128
+ async sendList(
129
+ openid: string,
130
+ header: string,
131
+ items: Array<{
132
+ title: string;
133
+ description?: string;
134
+ url?: string;
135
+ }>
136
+ ): Promise<SendMessageResponse> {
137
+ return this.sendMessage({
138
+ openid,
139
+ type: "list",
140
+ body: { header, items },
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Send a button message.
146
+ */
147
+ async sendButton(
148
+ openid: string,
149
+ content: string,
150
+ buttons: Array<{ key: string; value: string }>
151
+ ): Promise<SendMessageResponse> {
152
+ return this.sendMessage({
153
+ openid,
154
+ type: "button",
155
+ body: { content, buttons },
156
+ });
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Create a client for an account.
162
+ */
163
+ export function createPopoOaClient(
164
+ account: ResolvedPopoOaAccount
165
+ ): PopoOaClient {
166
+ return new PopoOaClient(account);
167
+ }
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ export { z };
3
+
4
+ const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
5
+
6
+ const DmConfigSchema = z
7
+ .object({
8
+ enabled: z.boolean().optional(),
9
+ systemPrompt: z.string().optional(),
10
+ })
11
+ .strict()
12
+ .optional();
13
+
14
+ export const PopoOaConfigSchema = z
15
+ .object({
16
+ enabled: z.boolean().optional(),
17
+ systemPrompt: z.string().optional(),
18
+ appId: z.string().optional(), // 服务号 AppID
19
+ appSecret: z.string().optional(), // 服务号 AppSecret
20
+ token: z.string().optional(), // 用于签名验证的 Token
21
+ server: z
22
+ .string()
23
+ .optional()
24
+ .default("https://open.popo.netease.com"),
25
+ webhookPath: z.string().optional().default("/popo-oa/events"),
26
+ dmPolicy: DmPolicySchema.optional().default("pairing"),
27
+ allowFrom: z.array(z.string()).optional(), // OpenID 白名单
28
+ historyLimit: z.number().int().min(0).optional(),
29
+ dmHistoryLimit: z.number().int().min(0).optional(),
30
+ dms: z.record(z.string(), DmConfigSchema).optional(),
31
+ textChunkLimit: z.number().int().positive().optional(),
32
+ chunkMode: z.enum(["length", "newline"]).optional(),
33
+ })
34
+ .strict()
35
+ .superRefine((value, ctx) => {
36
+ if (value.dmPolicy === "open") {
37
+ const allowFrom = value.allowFrom ?? [];
38
+ const hasWildcard = allowFrom.some((entry) => entry.trim() === "*");
39
+ if (!hasWildcard) {
40
+ ctx.addIssue({
41
+ code: z.ZodIssueCode.custom,
42
+ path: ["allowFrom"],
43
+ message:
44
+ 'channels.popo-oa.dmPolicy="open" requires channels.popo-oa.allowFrom to include "*"',
45
+ });
46
+ }
47
+ }
48
+ });
49
+
50
+ export type PopoOaConfig = z.infer<typeof PopoOaConfigSchema>;
package/src/crypto.ts ADDED
@@ -0,0 +1,44 @@
1
+ import crypto from "crypto";
2
+
3
+ /**
4
+ * Verify POPO OA webhook signature using SHA1.
5
+ * Signature = SHA1(token + timestamp + nonce) with sorted params
6
+ */
7
+ export function verifySignature(
8
+ token: string,
9
+ timestamp: string,
10
+ nonce: string,
11
+ signature: string
12
+ ): boolean {
13
+ // Sort token, timestamp, nonce alphabetically and join
14
+ const str = [token, timestamp, nonce].sort().join("");
15
+ const computed = crypto.createHash("sha1").update(str).digest("hex");
16
+ return computed.toLowerCase() === signature.toLowerCase();
17
+ }
18
+
19
+ /**
20
+ * Generate signature for webhook verification.
21
+ * Used when constructing verification responses.
22
+ */
23
+ export function generateSignature(
24
+ token: string,
25
+ timestamp: string,
26
+ nonce: string
27
+ ): string {
28
+ const str = [token, timestamp, nonce].sort().join("");
29
+ return crypto.createHash("sha1").update(str).digest("hex");
30
+ }
31
+
32
+ /**
33
+ * Generate a random nonce for webhook verification.
34
+ */
35
+ export function generateNonce(): string {
36
+ return crypto.randomBytes(16).toString("hex");
37
+ }
38
+
39
+ /**
40
+ * Get current timestamp in seconds.
41
+ */
42
+ export function getTimestamp(): string {
43
+ return Math.floor(Date.now() / 1000).toString();
44
+ }
package/src/monitor.ts ADDED
@@ -0,0 +1,215 @@
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 { PopoOaConfig } from "./types.js";
5
+ import { resolvePopoOaCredentials } from "./accounts.js";
6
+ import { verifySignature } from "./crypto.js";
7
+ import { handlePopoOaMessage, type PopoOaMessageEvent, buildPassiveReply } from "./bot.js";
8
+ import { probePopoOa } from "./probe.js";
9
+
10
+ export type MonitorPopoOaOpts = {
11
+ config?: ClawdbotConfig;
12
+ runtime?: RuntimeEnv;
13
+ abortSignal?: AbortSignal;
14
+ accountId?: string;
15
+ };
16
+
17
+ // Helper function to read request body
18
+ function readRequestBody(req: http.IncomingMessage): Promise<string> {
19
+ return new Promise((resolve, reject) => {
20
+ const chunks: Buffer[] = [];
21
+ req.on("data", (chunk) => chunks.push(chunk));
22
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
23
+ req.on("error", reject);
24
+ });
25
+ }
26
+
27
+ export async function monitorPopoOaProvider(opts: MonitorPopoOaOpts = {}): Promise<void> {
28
+ const cfg = opts.config;
29
+ if (!cfg) {
30
+ throw new Error("Config is required for POPO OA monitor");
31
+ }
32
+
33
+ const popoCfg = cfg.channels?.["popo-oa"] as PopoOaConfig | undefined;
34
+ const creds = resolvePopoOaCredentials(popoCfg);
35
+ if (!creds) {
36
+ throw new Error("POPO OA credentials not configured (appId, appSecret, token required)");
37
+ }
38
+
39
+ const log = opts.runtime?.log ?? console.log;
40
+ const error = opts.runtime?.error ?? console.error;
41
+
42
+ // Verify credentials by getting a token
43
+ const probeResult = await probePopoOa(popoCfg);
44
+ if (!probeResult.ok) {
45
+ throw new Error(`POPO OA probe failed: ${probeResult.error}`);
46
+ }
47
+ log(`popo-oa: credentials verified for appId ${probeResult.appId}`);
48
+
49
+ const webhookPath = popoCfg?.webhookPath?.trim() || "/popo-oa/events";
50
+ const chatHistories = new Map<string, HistoryEntry[]>();
51
+
52
+ // Track approved users (for pairing policy)
53
+ const approvedUsers = new Set<string>();
54
+
55
+ // Normalize path
56
+ const normalizedPath = normalizePluginHttpPath(webhookPath, "/popo-oa/events") ?? "/popo-oa/events";
57
+
58
+ // Register HTTP route to gateway
59
+ const unregisterHttp = registerPluginHttpRoute({
60
+ path: normalizedPath,
61
+ pluginId: "popo-oa",
62
+ accountId: opts.accountId,
63
+ log: (msg: string) => log(msg),
64
+ handler: async (req, res) => {
65
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
66
+ log(`popo-oa: received ${req.method} request to ${url.pathname}`);
67
+
68
+ // Handle CORS preflight
69
+ if (req.method === "OPTIONS") {
70
+ res.writeHead(200, {
71
+ "Access-Control-Allow-Origin": "*",
72
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
73
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
74
+ });
75
+ res.end();
76
+ return;
77
+ }
78
+
79
+ // Handle URL validation (GET request) - POPO OA webhook verification
80
+ if (req.method === "GET") {
81
+ const signature = url.searchParams.get("signature");
82
+ const timestamp = url.searchParams.get("timestamp");
83
+ const nonce = url.searchParams.get("nonce");
84
+ const echostr = url.searchParams.get("echostr");
85
+
86
+ log(`popo-oa: URL validation attempt - signature=${signature?.slice(0, 10)}..., timestamp=${timestamp}, nonce=${nonce?.slice(0, 10)}...`);
87
+
88
+ if (signature && timestamp && nonce && echostr) {
89
+ const valid = verifySignature(
90
+ creds.token,
91
+ timestamp,
92
+ nonce,
93
+ signature
94
+ );
95
+
96
+ if (valid) {
97
+ log(`popo-oa: URL validation successful`);
98
+ res.writeHead(200, { "Content-Type": "text/plain" });
99
+ res.end(echostr);
100
+ return;
101
+ } else {
102
+ log(`popo-oa: signature verification failed`);
103
+ }
104
+ } else {
105
+ log(`popo-oa: missing required parameters for validation`);
106
+ }
107
+
108
+ res.writeHead(403);
109
+ res.end("Invalid validation request");
110
+ return;
111
+ }
112
+
113
+ // Handle webhook event (POST request)
114
+ if (req.method === "POST") {
115
+ try {
116
+ const body = await readRequestBody(req);
117
+
118
+ // Verify signature for POST requests too
119
+ const signature = url.searchParams.get("signature");
120
+ const timestamp = url.searchParams.get("timestamp");
121
+ const nonce = url.searchParams.get("nonce");
122
+
123
+ if (signature && timestamp && nonce) {
124
+ const valid = verifySignature(
125
+ creds.token,
126
+ timestamp,
127
+ nonce,
128
+ signature
129
+ );
130
+
131
+ if (!valid) {
132
+ log(`popo-oa: invalid signature in webhook event`);
133
+ res.writeHead(403);
134
+ res.end("Invalid signature");
135
+ return;
136
+ }
137
+ }
138
+
139
+ log(`popo-oa: received XML message body length=${body.length}`);
140
+
141
+ const event: PopoOaMessageEvent = {
142
+ xml: {
143
+ ToUserName: "",
144
+ FromUserName: "",
145
+ CreateTime: String(Date.now()),
146
+ MsgType: "text",
147
+ },
148
+ rawBody: body,
149
+ };
150
+
151
+ // Process message
152
+ const passiveReply = await handlePopoOaMessage({
153
+ cfg,
154
+ event,
155
+ runtime: opts.runtime,
156
+ approvedUsers,
157
+ chatHistories,
158
+ });
159
+
160
+ // If there's a passive reply, return it in the response
161
+ if (passiveReply) {
162
+ const xml = buildPassiveReply({
163
+ toUser: event.xml.FromUserName,
164
+ fromUser: event.xml.ToUserName,
165
+ content: passiveReply,
166
+ });
167
+ res.writeHead(200, { "Content-Type": "application/xml" });
168
+ res.end(xml);
169
+ return;
170
+ }
171
+
172
+ // Return success response
173
+ res.writeHead(200, { "Content-Type": "text/plain" });
174
+ res.end("success");
175
+ } catch (err) {
176
+ error(`popo-oa: error processing webhook: ${String(err)}`);
177
+ res.writeHead(500);
178
+ res.end("Internal Server Error");
179
+ }
180
+ return;
181
+ }
182
+
183
+ res.writeHead(405);
184
+ res.end("Method Not Allowed");
185
+ },
186
+ });
187
+
188
+ log(`popo-oa: registered webhook handler at ${normalizedPath}`);
189
+
190
+ // Handle abort signal
191
+ const stopHandler = () => {
192
+ log("popo-oa: stopping provider");
193
+ unregisterHttp();
194
+ };
195
+
196
+ if (opts.abortSignal?.aborted) {
197
+ stopHandler();
198
+ return;
199
+ }
200
+
201
+ opts.abortSignal?.addEventListener("abort", stopHandler, { once: true });
202
+
203
+ // Keep promise pending until abort
204
+ return new Promise((resolve) => {
205
+ const handler = () => {
206
+ stopHandler();
207
+ resolve();
208
+ };
209
+
210
+ if (opts.abortSignal) {
211
+ opts.abortSignal.removeEventListener("abort", stopHandler);
212
+ opts.abortSignal.addEventListener("abort", handler, { once: true });
213
+ }
214
+ });
215
+ }