@skrillex1224/playwright-toolkit 2.1.113 → 2.1.114

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/dist/index.cjs CHANGED
@@ -70,12 +70,23 @@ var createActorInfo = (info) => {
70
70
  const normalizeShareIdentities = (value) => {
71
71
  if (!Array.isArray(value)) return [];
72
72
  const unique = /* @__PURE__ */ new Set();
73
+ const normalized = [];
73
74
  for (const raw of value) {
75
+ if (raw instanceof RegExp) {
76
+ const key2 = `regex:${raw.source}/${raw.flags}`;
77
+ if (unique.has(key2)) continue;
78
+ unique.add(key2);
79
+ normalized.push(raw);
80
+ continue;
81
+ }
74
82
  const identity = String(raw || "").trim();
75
83
  if (!identity) continue;
76
- unique.add(identity);
84
+ const key = `string:${identity}`;
85
+ if (unique.has(key)) continue;
86
+ unique.add(key);
87
+ normalized.push(identity);
77
88
  }
78
- return Array.from(unique);
89
+ return normalized;
79
90
  };
80
91
  const buildLandingUrl = ({ protocol: protocol2, domain: domain2, path: path2 }) => {
81
92
  const safeProtocol = String(protocol2).trim();
@@ -111,42 +122,42 @@ var ActorInfo = {
111
122
  name: "\u8C46\u5305",
112
123
  domain: "www.doubao.com",
113
124
  path: "/",
114
- shareIdentities: ["/thread/"]
125
+ shareIdentities: [/\/thread\/[^/?#]+(?:[/?#]|$)/i]
115
126
  }),
116
127
  deepseek: createActorInfo({
117
128
  key: "deepseek",
118
129
  name: "DeepSeek",
119
130
  domain: "chat.deepseek.com",
120
131
  path: "/",
121
- shareIdentities: ["/share/"]
132
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
122
133
  }),
123
134
  erine: createActorInfo({
124
135
  key: "erine",
125
136
  name: "\u6587\u5FC3\u4E00\u8A00",
126
137
  domain: "yiyan.baidu.com",
127
138
  path: "/",
128
- shareIdentities: ["/share/"]
139
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
129
140
  }),
130
141
  yuanbao: createActorInfo({
131
142
  key: "yuanbao",
132
143
  name: "\u5143\u5B9D",
133
144
  domain: "yuanbao.tencent.com",
134
145
  path: "/chat/",
135
- shareIdentities: ["/s/"]
146
+ shareIdentities: [/\/s\/[^/?#]+(?:[/?#]|$)/i]
136
147
  }),
137
148
  kimi: createActorInfo({
138
149
  key: "kimi",
139
150
  name: "Kimi",
140
151
  domain: "www.kimi.com",
141
152
  path: "/",
142
- shareIdentities: ["/share/"]
153
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
143
154
  }),
144
155
  qwen: createActorInfo({
145
156
  key: "qwen",
146
157
  name: "\u901A\u4E49\u5343\u95EE",
147
158
  domain: "www.qianwen.com",
148
159
  path: "/chat",
149
- shareIdentities: ["/share/"]
160
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
150
161
  })
151
162
  };
152
163
 
@@ -510,21 +521,36 @@ var Utils = {
510
521
  * 从字符串中提取 URL 链接
511
522
  * @param {string} text
512
523
  * @param {Object} [options]
513
- * @param {string[]} [options.identities] - 关键路径标识,如 ['/share/', '/s/']
524
+ * @param {(string | RegExp)[]} [options.identities] - 关键路径标识,支持字符串 includes 或正则
514
525
  * @returns {string[]}
515
526
  */
516
527
  parseLinks(text, options = {}) {
517
528
  const raw = String(text || "");
518
529
  if (!raw) return [];
519
530
  const opts = options && typeof options === "object" ? options : {};
520
- const identities = Array.isArray(opts.identities) ? opts.identities.map((item) => String(item || "").trim()).filter(Boolean) : [];
531
+ const identities = Array.isArray(opts.identities) ? opts.identities : [];
532
+ const matchers = identities.map((item) => {
533
+ if (item instanceof RegExp) {
534
+ return { type: "regex", value: item };
535
+ }
536
+ const value = String(item || "").trim();
537
+ if (!value) return null;
538
+ return { type: "string", value };
539
+ }).filter(Boolean);
521
540
  const matched = raw.match(/https?:\/\/[\w\-._~:/?#[\]@!$&'()*+,;=%]+/g) || [];
522
541
  const unique = /* @__PURE__ */ new Set();
523
542
  for (const item of matched) {
524
543
  const link = String(item || "").trim().replace(/["'“”‘’>\].,,。;;!!??]+$/, "");
525
544
  if (!link || !/^https?:\/\//i.test(link)) continue;
526
- if (identities.length > 0 && !identities.some((key) => link.includes(key))) {
527
- continue;
545
+ if (matchers.length > 0) {
546
+ const ok = matchers.some((matcher) => {
547
+ if (matcher.type === "string") {
548
+ return link.includes(matcher.value);
549
+ }
550
+ matcher.value.lastIndex = 0;
551
+ return matcher.value.test(link);
552
+ });
553
+ if (!ok) continue;
528
554
  }
529
555
  unique.add(link);
530
556
  }
@@ -3034,10 +3060,10 @@ var Monitor = {
3034
3060
  *
3035
3061
  * @param {import('playwright').Page} page
3036
3062
  * @param {Object} [options]
3037
- * @param {string[]} [options.identities]
3063
+ * @param {(string | RegExp)[]} [options.identities]
3038
3064
  * @param {string | string[]} [options.selectors]
3039
3065
  * @param {'added' | 'changed' | 'all'} [options.mode]
3040
- * @param {(text: string, options?: { identities?: string[] }) => string[]} [options.parseLinks]
3066
+ * @param {(text: string, options?: { identities?: (string | RegExp)[] }) => string[]} [options.parseLinks]
3041
3067
  * @param {(payload: { link: string; rawDom: string; mutationCount: number; html: string; text: string; mutationNodes: Array<{ html: string; text: string; mutationType: string }> }) => void} [options.onMatch]
3042
3068
  * @returns {Promise<{ stop: () => Promise<{ totalMutations: number }> }>}
3043
3069
  */
@@ -3082,55 +3108,67 @@ ${text}`;
3082
3108
  // src/share.js
3083
3109
  var DEFAULT_TIMEOUT_AFTER_ACTION_MS = 10 * 1e3;
3084
3110
  var DEFAULT_PAYLOAD_SNAPSHOT_MAX_LEN = 500;
3111
+ var createRuntimeKey = (prefix) => `__${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
3085
3112
  var Share = {
3086
3113
  /**
3087
- * 捕获分享链接(接口响应 + DOM 监听 双通道)
3114
+ * 捕获分享链接(剪切板 + DOM + 接口 三通道)
3115
+ * 优先级:clipboard > dom > response
3088
3116
  *
3089
3117
  * @param {import('playwright').Page} page
3090
3118
  * @param {Object} [options]
3091
- * @param {string[]} [options.identities]
3119
+ * @param {(string | RegExp)[]} [options.identities]
3092
3120
  * @param {number} [options.timeoutAfterActionMs]
3093
3121
  * @param {number} [options.payloadSnapshotMaxLen]
3094
- * @param {boolean} [options.enableResponse=true]
3122
+ * @param {boolean} [options.enableClipboard=true]
3095
3123
  * @param {boolean} [options.enableDom=true]
3124
+ * @param {boolean} [options.enableResponse=true]
3096
3125
  * @param {string | string[]} [options.domSelectors='html']
3097
3126
  * @param {'added' | 'changed' | 'all'} [options.domMode='added']
3127
+ * @param {number} [options.clipboardPollIntervalMs=220]
3098
3128
  * @param {(response: import('playwright').Response) => boolean | Promise<boolean>} [options.responseFilter]
3099
- * @param {(text: string, options?: { identities?: string[] }) => string[]} [options.parseLinks]
3129
+ * @param {(text: string, options?: { identities?: (string | RegExp)[] }) => string[]} [options.parseLinks]
3100
3130
  * @param {() => Promise<void>} [options.performActions]
3101
- * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'response' | 'dom' | 'none' }>}
3131
+ * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'clipboard' | 'dom' | 'response' | 'none' }>}
3102
3132
  */
3103
3133
  async captureLink(page, options = {}) {
3104
3134
  const identities = Array.isArray(options.identities) ? options.identities : [];
3105
3135
  const timeoutAfterActionMs = options.timeoutAfterActionMs ?? DEFAULT_TIMEOUT_AFTER_ACTION_MS;
3106
3136
  const payloadSnapshotMaxLen = options.payloadSnapshotMaxLen ?? DEFAULT_PAYLOAD_SNAPSHOT_MAX_LEN;
3107
- const enableResponse = options.enableResponse !== false;
3137
+ const enableClipboard = options.enableClipboard !== false;
3108
3138
  const enableDom = options.enableDom !== false;
3139
+ const enableResponse = options.enableResponse !== false;
3109
3140
  const domSelectors = options.domSelectors ?? "html";
3110
- const domMode = options.domMode ?? Mutation.Mode.All;
3141
+ const domMode = options.domMode ?? Mutation.Mode.Added;
3142
+ const clipboardPollIntervalMs = Math.max(80, options.clipboardPollIntervalMs ?? 220);
3111
3143
  const responseFilter = typeof options.responseFilter === "function" ? options.responseFilter : null;
3112
3144
  const parseLinks = typeof options.parseLinks === "function" ? options.parseLinks : Utils.parseLinks;
3113
3145
  const performActions = typeof options.performActions === "function" ? options.performActions : async () => {
3114
3146
  };
3115
- if (!enableResponse && !enableDom) {
3116
- throw new Error("Share.captureLink requires at least one channel: response or dom");
3147
+ const clipboardKeys = {
3148
+ state: createRuntimeKey("pk_clip_state"),
3149
+ hookReady: createRuntimeKey("pk_clip_hook_ready"),
3150
+ copyListener: createRuntimeKey("pk_clip_copy_listener"),
3151
+ execWrapped: createRuntimeKey("pk_clip_exec_wrapped"),
3152
+ writeWrapped: createRuntimeKey("pk_clip_write_wrapped")
3153
+ };
3154
+ if (!enableClipboard && !enableResponse && !enableDom) {
3155
+ throw new Error("Share.captureLink requires at least one channel: clipboard/dom/response");
3117
3156
  }
3118
- let link = null;
3119
- let payloadText = "";
3120
- let source = "none";
3121
- let resolveMatched = null;
3122
- let domMonitor = null;
3123
- const matchedPromise = new Promise((resolve) => {
3124
- resolveMatched = resolve;
3125
- });
3126
- const finalizeMatch = (candidate, matchedSource, payload) => {
3127
- if (link || !candidate) return false;
3128
- link = candidate;
3129
- source = matchedSource;
3130
- payloadText = String(payload || "");
3131
- if (resolveMatched) resolveMatched(candidate);
3157
+ const candidates = {
3158
+ clipboard: null,
3159
+ dom: null,
3160
+ response: null
3161
+ };
3162
+ const setCandidate = (sourceKey, candidateLink, payload) => {
3163
+ if (!candidateLink) return false;
3164
+ if (candidates[sourceKey]?.link) return false;
3165
+ candidates[sourceKey] = {
3166
+ link: candidateLink,
3167
+ payloadText: String(payload || "")
3168
+ };
3132
3169
  return true;
3133
3170
  };
3171
+ let domMonitor = null;
3134
3172
  const stopDomMonitor = async () => {
3135
3173
  if (!domMonitor) return;
3136
3174
  const monitor = domMonitor;
@@ -3141,7 +3179,7 @@ var Share = {
3141
3179
  }
3142
3180
  };
3143
3181
  const onResponse = async (response) => {
3144
- if (link) return;
3182
+ if (candidates.response?.link) return;
3145
3183
  try {
3146
3184
  if (responseFilter) {
3147
3185
  const accepted = await responseFilter(response);
@@ -3151,13 +3189,105 @@ var Share = {
3151
3189
  if (!text) return;
3152
3190
  const [candidate] = parseLinks(text, { identities }) || [];
3153
3191
  if (!candidate) return;
3154
- const matched = finalizeMatch(candidate, "response", text);
3155
- if (!matched) return;
3156
- page.off("response", onResponse);
3157
- void stopDomMonitor();
3192
+ setCandidate("response", candidate, text);
3193
+ } catch {
3194
+ }
3195
+ };
3196
+ const ensureClipboardHooks = async () => {
3197
+ if (!enableClipboard) return;
3198
+ try {
3199
+ await page.evaluate(({ keys }) => {
3200
+ if (document[keys.hookReady]) return;
3201
+ const state = document[keys.state] || { captured: "" };
3202
+ document[keys.state] = state;
3203
+ const normalize = (value) => String(value || "").trim();
3204
+ const setClipboard = (value) => {
3205
+ const safe = normalize(value);
3206
+ if (!safe) return;
3207
+ state.captured = safe;
3208
+ };
3209
+ try {
3210
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
3211
+ const clipboard = navigator.clipboard;
3212
+ if (!clipboard[keys.writeWrapped]) {
3213
+ const originalWriteText = clipboard.writeText.bind(clipboard);
3214
+ clipboard.writeText = async (text) => {
3215
+ setClipboard(text);
3216
+ return await originalWriteText(text);
3217
+ };
3218
+ clipboard[keys.writeWrapped] = true;
3219
+ }
3220
+ }
3221
+ } catch {
3222
+ }
3223
+ try {
3224
+ if (!document[keys.copyListener]) {
3225
+ document.addEventListener("copy", (event) => {
3226
+ try {
3227
+ const copied = event?.clipboardData?.getData?.("text/plain") || "";
3228
+ setClipboard(copied);
3229
+ } catch {
3230
+ }
3231
+ }, true);
3232
+ document[keys.copyListener] = true;
3233
+ }
3234
+ } catch {
3235
+ }
3236
+ try {
3237
+ if (typeof document.execCommand === "function" && !document[keys.execWrapped]) {
3238
+ const originalExecCommand = document.execCommand.bind(document);
3239
+ document.execCommand = function(command, ...args) {
3240
+ try {
3241
+ if (String(command || "").toLowerCase() === "copy") {
3242
+ const selected = globalThis.getSelection?.().toString?.() || "";
3243
+ setClipboard(selected);
3244
+ }
3245
+ } catch {
3246
+ }
3247
+ return originalExecCommand(command, ...args);
3248
+ };
3249
+ document[keys.execWrapped] = true;
3250
+ }
3251
+ } catch {
3252
+ }
3253
+ document[keys.hookReady] = true;
3254
+ }, { keys: clipboardKeys });
3255
+ } catch {
3256
+ }
3257
+ };
3258
+ const probeClipboard = async () => {
3259
+ if (!enableClipboard) return null;
3260
+ try {
3261
+ const data = await page.evaluate(async ({ keys }) => {
3262
+ const capturedText = String(document[keys.state]?.captured || "").trim();
3263
+ let directText = "";
3264
+ try {
3265
+ if (navigator.clipboard && typeof navigator.clipboard.readText === "function") {
3266
+ directText = String(await navigator.clipboard.readText() || "").trim();
3267
+ }
3268
+ } catch {
3269
+ }
3270
+ return { capturedText, directText };
3271
+ }, { keys: clipboardKeys });
3272
+ const [capturedLink] = parseLinks(data?.capturedText || "", { identities }) || [];
3273
+ if (capturedLink) {
3274
+ return {
3275
+ link: capturedLink,
3276
+ payloadText: data.capturedText || ""
3277
+ };
3278
+ }
3279
+ const [directLink] = parseLinks(data?.directText || "", { identities }) || [];
3280
+ if (directLink) {
3281
+ return {
3282
+ link: directLink,
3283
+ payloadText: data.directText || ""
3284
+ };
3285
+ }
3158
3286
  } catch {
3159
3287
  }
3288
+ return null;
3160
3289
  };
3290
+ await ensureClipboardHooks();
3161
3291
  if (enableDom) {
3162
3292
  domMonitor = await Monitor.useShareLinkMonitor(page, {
3163
3293
  identities,
@@ -3165,11 +3295,7 @@ var Share = {
3165
3295
  mode: domMode,
3166
3296
  parseLinks,
3167
3297
  onMatch: ({ link: domLink, rawDom }) => {
3168
- const matched = finalizeMatch(domLink, "dom", rawDom);
3169
- if (!matched) return;
3170
- if (enableResponse) {
3171
- page.off("response", onResponse);
3172
- }
3298
+ setCandidate("dom", domLink, rawDom);
3173
3299
  }
3174
3300
  });
3175
3301
  }
@@ -3178,18 +3304,43 @@ var Share = {
3178
3304
  }
3179
3305
  try {
3180
3306
  await performActions();
3181
- if (!link && timeoutAfterActionMs > 0) {
3182
- await Promise.race([
3183
- matchedPromise,
3184
- new Promise((resolve) => setTimeout(resolve, timeoutAfterActionMs))
3185
- ]);
3307
+ const deadline = Date.now() + Math.max(0, timeoutAfterActionMs);
3308
+ const nonClipboardGraceMs = Math.max(120, Math.min(500, clipboardPollIntervalMs * 2));
3309
+ let nonClipboardSeenAt = null;
3310
+ while (true) {
3311
+ if (enableClipboard) {
3312
+ const clipboardResult = await probeClipboard();
3313
+ if (clipboardResult?.link) {
3314
+ setCandidate("clipboard", clipboardResult.link, clipboardResult.payloadText);
3315
+ }
3316
+ }
3317
+ if (candidates.clipboard?.link) {
3318
+ break;
3319
+ }
3320
+ if (candidates.dom?.link || candidates.response?.link) {
3321
+ if (!nonClipboardSeenAt) {
3322
+ nonClipboardSeenAt = Date.now();
3323
+ } else if (Date.now() - nonClipboardSeenAt >= nonClipboardGraceMs) {
3324
+ break;
3325
+ }
3326
+ }
3327
+ if (Date.now() >= deadline) {
3328
+ break;
3329
+ }
3330
+ const remaining = deadline - Date.now();
3331
+ await new Promise((resolve) => {
3332
+ setTimeout(resolve, Math.max(0, Math.min(clipboardPollIntervalMs, remaining)));
3333
+ });
3186
3334
  }
3335
+ const finalMatch = candidates.clipboard || candidates.dom || candidates.response || null;
3336
+ const finalSource = candidates.clipboard ? "clipboard" : candidates.dom ? "dom" : candidates.response ? "response" : "none";
3337
+ const payloadText = finalMatch?.payloadText || "";
3187
3338
  const payloadSnapshot = payloadText ? payloadText.replace(/\s+/g, " ").trim().slice(0, payloadSnapshotMaxLen) : "";
3188
3339
  return {
3189
- link,
3340
+ link: finalMatch?.link || null,
3190
3341
  payloadText,
3191
3342
  payloadSnapshot,
3192
- source
3343
+ source: finalSource
3193
3344
  };
3194
3345
  } finally {
3195
3346
  if (enableResponse) {