@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.js CHANGED
@@ -43,12 +43,23 @@ var createActorInfo = (info) => {
43
43
  const normalizeShareIdentities = (value) => {
44
44
  if (!Array.isArray(value)) return [];
45
45
  const unique = /* @__PURE__ */ new Set();
46
+ const normalized = [];
46
47
  for (const raw of value) {
48
+ if (raw instanceof RegExp) {
49
+ const key2 = `regex:${raw.source}/${raw.flags}`;
50
+ if (unique.has(key2)) continue;
51
+ unique.add(key2);
52
+ normalized.push(raw);
53
+ continue;
54
+ }
47
55
  const identity = String(raw || "").trim();
48
56
  if (!identity) continue;
49
- unique.add(identity);
57
+ const key = `string:${identity}`;
58
+ if (unique.has(key)) continue;
59
+ unique.add(key);
60
+ normalized.push(identity);
50
61
  }
51
- return Array.from(unique);
62
+ return normalized;
52
63
  };
53
64
  const buildLandingUrl = ({ protocol: protocol2, domain: domain2, path: path2 }) => {
54
65
  const safeProtocol = String(protocol2).trim();
@@ -84,42 +95,42 @@ var ActorInfo = {
84
95
  name: "\u8C46\u5305",
85
96
  domain: "www.doubao.com",
86
97
  path: "/",
87
- shareIdentities: ["/thread/"]
98
+ shareIdentities: [/\/thread\/[^/?#]+(?:[/?#]|$)/i]
88
99
  }),
89
100
  deepseek: createActorInfo({
90
101
  key: "deepseek",
91
102
  name: "DeepSeek",
92
103
  domain: "chat.deepseek.com",
93
104
  path: "/",
94
- shareIdentities: ["/share/"]
105
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
95
106
  }),
96
107
  erine: createActorInfo({
97
108
  key: "erine",
98
109
  name: "\u6587\u5FC3\u4E00\u8A00",
99
110
  domain: "yiyan.baidu.com",
100
111
  path: "/",
101
- shareIdentities: ["/share/"]
112
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
102
113
  }),
103
114
  yuanbao: createActorInfo({
104
115
  key: "yuanbao",
105
116
  name: "\u5143\u5B9D",
106
117
  domain: "yuanbao.tencent.com",
107
118
  path: "/chat/",
108
- shareIdentities: ["/s/"]
119
+ shareIdentities: [/\/s\/[^/?#]+(?:[/?#]|$)/i]
109
120
  }),
110
121
  kimi: createActorInfo({
111
122
  key: "kimi",
112
123
  name: "Kimi",
113
124
  domain: "www.kimi.com",
114
125
  path: "/",
115
- shareIdentities: ["/share/"]
126
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
116
127
  }),
117
128
  qwen: createActorInfo({
118
129
  key: "qwen",
119
130
  name: "\u901A\u4E49\u5343\u95EE",
120
131
  domain: "www.qianwen.com",
121
132
  path: "/chat",
122
- shareIdentities: ["/share/"]
133
+ shareIdentities: [/\/share\/[^/?#]+(?:[/?#]|$)/i]
123
134
  })
124
135
  };
125
136
 
@@ -483,21 +494,36 @@ var Utils = {
483
494
  * 从字符串中提取 URL 链接
484
495
  * @param {string} text
485
496
  * @param {Object} [options]
486
- * @param {string[]} [options.identities] - 关键路径标识,如 ['/share/', '/s/']
497
+ * @param {(string | RegExp)[]} [options.identities] - 关键路径标识,支持字符串 includes 或正则
487
498
  * @returns {string[]}
488
499
  */
489
500
  parseLinks(text, options = {}) {
490
501
  const raw = String(text || "");
491
502
  if (!raw) return [];
492
503
  const opts = options && typeof options === "object" ? options : {};
493
- const identities = Array.isArray(opts.identities) ? opts.identities.map((item) => String(item || "").trim()).filter(Boolean) : [];
504
+ const identities = Array.isArray(opts.identities) ? opts.identities : [];
505
+ const matchers = identities.map((item) => {
506
+ if (item instanceof RegExp) {
507
+ return { type: "regex", value: item };
508
+ }
509
+ const value = String(item || "").trim();
510
+ if (!value) return null;
511
+ return { type: "string", value };
512
+ }).filter(Boolean);
494
513
  const matched = raw.match(/https?:\/\/[\w\-._~:/?#[\]@!$&'()*+,;=%]+/g) || [];
495
514
  const unique = /* @__PURE__ */ new Set();
496
515
  for (const item of matched) {
497
516
  const link = String(item || "").trim().replace(/["'“”‘’>\].,,。;;!!??]+$/, "");
498
517
  if (!link || !/^https?:\/\//i.test(link)) continue;
499
- if (identities.length > 0 && !identities.some((key) => link.includes(key))) {
500
- continue;
518
+ if (matchers.length > 0) {
519
+ const ok = matchers.some((matcher) => {
520
+ if (matcher.type === "string") {
521
+ return link.includes(matcher.value);
522
+ }
523
+ matcher.value.lastIndex = 0;
524
+ return matcher.value.test(link);
525
+ });
526
+ if (!ok) continue;
501
527
  }
502
528
  unique.add(link);
503
529
  }
@@ -3007,10 +3033,10 @@ var Monitor = {
3007
3033
  *
3008
3034
  * @param {import('playwright').Page} page
3009
3035
  * @param {Object} [options]
3010
- * @param {string[]} [options.identities]
3036
+ * @param {(string | RegExp)[]} [options.identities]
3011
3037
  * @param {string | string[]} [options.selectors]
3012
3038
  * @param {'added' | 'changed' | 'all'} [options.mode]
3013
- * @param {(text: string, options?: { identities?: string[] }) => string[]} [options.parseLinks]
3039
+ * @param {(text: string, options?: { identities?: (string | RegExp)[] }) => string[]} [options.parseLinks]
3014
3040
  * @param {(payload: { link: string; rawDom: string; mutationCount: number; html: string; text: string; mutationNodes: Array<{ html: string; text: string; mutationType: string }> }) => void} [options.onMatch]
3015
3041
  * @returns {Promise<{ stop: () => Promise<{ totalMutations: number }> }>}
3016
3042
  */
@@ -3055,55 +3081,67 @@ ${text}`;
3055
3081
  // src/share.js
3056
3082
  var DEFAULT_TIMEOUT_AFTER_ACTION_MS = 10 * 1e3;
3057
3083
  var DEFAULT_PAYLOAD_SNAPSHOT_MAX_LEN = 500;
3084
+ var createRuntimeKey = (prefix) => `__${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
3058
3085
  var Share = {
3059
3086
  /**
3060
- * 捕获分享链接(接口响应 + DOM 监听 双通道)
3087
+ * 捕获分享链接(剪切板 + DOM + 接口 三通道)
3088
+ * 优先级:clipboard > dom > response
3061
3089
  *
3062
3090
  * @param {import('playwright').Page} page
3063
3091
  * @param {Object} [options]
3064
- * @param {string[]} [options.identities]
3092
+ * @param {(string | RegExp)[]} [options.identities]
3065
3093
  * @param {number} [options.timeoutAfterActionMs]
3066
3094
  * @param {number} [options.payloadSnapshotMaxLen]
3067
- * @param {boolean} [options.enableResponse=true]
3095
+ * @param {boolean} [options.enableClipboard=true]
3068
3096
  * @param {boolean} [options.enableDom=true]
3097
+ * @param {boolean} [options.enableResponse=true]
3069
3098
  * @param {string | string[]} [options.domSelectors='html']
3070
3099
  * @param {'added' | 'changed' | 'all'} [options.domMode='added']
3100
+ * @param {number} [options.clipboardPollIntervalMs=220]
3071
3101
  * @param {(response: import('playwright').Response) => boolean | Promise<boolean>} [options.responseFilter]
3072
- * @param {(text: string, options?: { identities?: string[] }) => string[]} [options.parseLinks]
3102
+ * @param {(text: string, options?: { identities?: (string | RegExp)[] }) => string[]} [options.parseLinks]
3073
3103
  * @param {() => Promise<void>} [options.performActions]
3074
- * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'response' | 'dom' | 'none' }>}
3104
+ * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'clipboard' | 'dom' | 'response' | 'none' }>}
3075
3105
  */
3076
3106
  async captureLink(page, options = {}) {
3077
3107
  const identities = Array.isArray(options.identities) ? options.identities : [];
3078
3108
  const timeoutAfterActionMs = options.timeoutAfterActionMs ?? DEFAULT_TIMEOUT_AFTER_ACTION_MS;
3079
3109
  const payloadSnapshotMaxLen = options.payloadSnapshotMaxLen ?? DEFAULT_PAYLOAD_SNAPSHOT_MAX_LEN;
3080
- const enableResponse = options.enableResponse !== false;
3110
+ const enableClipboard = options.enableClipboard !== false;
3081
3111
  const enableDom = options.enableDom !== false;
3112
+ const enableResponse = options.enableResponse !== false;
3082
3113
  const domSelectors = options.domSelectors ?? "html";
3083
- const domMode = options.domMode ?? Mutation.Mode.All;
3114
+ const domMode = options.domMode ?? Mutation.Mode.Added;
3115
+ const clipboardPollIntervalMs = Math.max(80, options.clipboardPollIntervalMs ?? 220);
3084
3116
  const responseFilter = typeof options.responseFilter === "function" ? options.responseFilter : null;
3085
3117
  const parseLinks = typeof options.parseLinks === "function" ? options.parseLinks : Utils.parseLinks;
3086
3118
  const performActions = typeof options.performActions === "function" ? options.performActions : async () => {
3087
3119
  };
3088
- if (!enableResponse && !enableDom) {
3089
- throw new Error("Share.captureLink requires at least one channel: response or dom");
3120
+ const clipboardKeys = {
3121
+ state: createRuntimeKey("pk_clip_state"),
3122
+ hookReady: createRuntimeKey("pk_clip_hook_ready"),
3123
+ copyListener: createRuntimeKey("pk_clip_copy_listener"),
3124
+ execWrapped: createRuntimeKey("pk_clip_exec_wrapped"),
3125
+ writeWrapped: createRuntimeKey("pk_clip_write_wrapped")
3126
+ };
3127
+ if (!enableClipboard && !enableResponse && !enableDom) {
3128
+ throw new Error("Share.captureLink requires at least one channel: clipboard/dom/response");
3090
3129
  }
3091
- let link = null;
3092
- let payloadText = "";
3093
- let source = "none";
3094
- let resolveMatched = null;
3095
- let domMonitor = null;
3096
- const matchedPromise = new Promise((resolve) => {
3097
- resolveMatched = resolve;
3098
- });
3099
- const finalizeMatch = (candidate, matchedSource, payload) => {
3100
- if (link || !candidate) return false;
3101
- link = candidate;
3102
- source = matchedSource;
3103
- payloadText = String(payload || "");
3104
- if (resolveMatched) resolveMatched(candidate);
3130
+ const candidates = {
3131
+ clipboard: null,
3132
+ dom: null,
3133
+ response: null
3134
+ };
3135
+ const setCandidate = (sourceKey, candidateLink, payload) => {
3136
+ if (!candidateLink) return false;
3137
+ if (candidates[sourceKey]?.link) return false;
3138
+ candidates[sourceKey] = {
3139
+ link: candidateLink,
3140
+ payloadText: String(payload || "")
3141
+ };
3105
3142
  return true;
3106
3143
  };
3144
+ let domMonitor = null;
3107
3145
  const stopDomMonitor = async () => {
3108
3146
  if (!domMonitor) return;
3109
3147
  const monitor = domMonitor;
@@ -3114,7 +3152,7 @@ var Share = {
3114
3152
  }
3115
3153
  };
3116
3154
  const onResponse = async (response) => {
3117
- if (link) return;
3155
+ if (candidates.response?.link) return;
3118
3156
  try {
3119
3157
  if (responseFilter) {
3120
3158
  const accepted = await responseFilter(response);
@@ -3124,13 +3162,105 @@ var Share = {
3124
3162
  if (!text) return;
3125
3163
  const [candidate] = parseLinks(text, { identities }) || [];
3126
3164
  if (!candidate) return;
3127
- const matched = finalizeMatch(candidate, "response", text);
3128
- if (!matched) return;
3129
- page.off("response", onResponse);
3130
- void stopDomMonitor();
3165
+ setCandidate("response", candidate, text);
3166
+ } catch {
3167
+ }
3168
+ };
3169
+ const ensureClipboardHooks = async () => {
3170
+ if (!enableClipboard) return;
3171
+ try {
3172
+ await page.evaluate(({ keys }) => {
3173
+ if (document[keys.hookReady]) return;
3174
+ const state = document[keys.state] || { captured: "" };
3175
+ document[keys.state] = state;
3176
+ const normalize = (value) => String(value || "").trim();
3177
+ const setClipboard = (value) => {
3178
+ const safe = normalize(value);
3179
+ if (!safe) return;
3180
+ state.captured = safe;
3181
+ };
3182
+ try {
3183
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
3184
+ const clipboard = navigator.clipboard;
3185
+ if (!clipboard[keys.writeWrapped]) {
3186
+ const originalWriteText = clipboard.writeText.bind(clipboard);
3187
+ clipboard.writeText = async (text) => {
3188
+ setClipboard(text);
3189
+ return await originalWriteText(text);
3190
+ };
3191
+ clipboard[keys.writeWrapped] = true;
3192
+ }
3193
+ }
3194
+ } catch {
3195
+ }
3196
+ try {
3197
+ if (!document[keys.copyListener]) {
3198
+ document.addEventListener("copy", (event) => {
3199
+ try {
3200
+ const copied = event?.clipboardData?.getData?.("text/plain") || "";
3201
+ setClipboard(copied);
3202
+ } catch {
3203
+ }
3204
+ }, true);
3205
+ document[keys.copyListener] = true;
3206
+ }
3207
+ } catch {
3208
+ }
3209
+ try {
3210
+ if (typeof document.execCommand === "function" && !document[keys.execWrapped]) {
3211
+ const originalExecCommand = document.execCommand.bind(document);
3212
+ document.execCommand = function(command, ...args) {
3213
+ try {
3214
+ if (String(command || "").toLowerCase() === "copy") {
3215
+ const selected = globalThis.getSelection?.().toString?.() || "";
3216
+ setClipboard(selected);
3217
+ }
3218
+ } catch {
3219
+ }
3220
+ return originalExecCommand(command, ...args);
3221
+ };
3222
+ document[keys.execWrapped] = true;
3223
+ }
3224
+ } catch {
3225
+ }
3226
+ document[keys.hookReady] = true;
3227
+ }, { keys: clipboardKeys });
3228
+ } catch {
3229
+ }
3230
+ };
3231
+ const probeClipboard = async () => {
3232
+ if (!enableClipboard) return null;
3233
+ try {
3234
+ const data = await page.evaluate(async ({ keys }) => {
3235
+ const capturedText = String(document[keys.state]?.captured || "").trim();
3236
+ let directText = "";
3237
+ try {
3238
+ if (navigator.clipboard && typeof navigator.clipboard.readText === "function") {
3239
+ directText = String(await navigator.clipboard.readText() || "").trim();
3240
+ }
3241
+ } catch {
3242
+ }
3243
+ return { capturedText, directText };
3244
+ }, { keys: clipboardKeys });
3245
+ const [capturedLink] = parseLinks(data?.capturedText || "", { identities }) || [];
3246
+ if (capturedLink) {
3247
+ return {
3248
+ link: capturedLink,
3249
+ payloadText: data.capturedText || ""
3250
+ };
3251
+ }
3252
+ const [directLink] = parseLinks(data?.directText || "", { identities }) || [];
3253
+ if (directLink) {
3254
+ return {
3255
+ link: directLink,
3256
+ payloadText: data.directText || ""
3257
+ };
3258
+ }
3131
3259
  } catch {
3132
3260
  }
3261
+ return null;
3133
3262
  };
3263
+ await ensureClipboardHooks();
3134
3264
  if (enableDom) {
3135
3265
  domMonitor = await Monitor.useShareLinkMonitor(page, {
3136
3266
  identities,
@@ -3138,11 +3268,7 @@ var Share = {
3138
3268
  mode: domMode,
3139
3269
  parseLinks,
3140
3270
  onMatch: ({ link: domLink, rawDom }) => {
3141
- const matched = finalizeMatch(domLink, "dom", rawDom);
3142
- if (!matched) return;
3143
- if (enableResponse) {
3144
- page.off("response", onResponse);
3145
- }
3271
+ setCandidate("dom", domLink, rawDom);
3146
3272
  }
3147
3273
  });
3148
3274
  }
@@ -3151,18 +3277,43 @@ var Share = {
3151
3277
  }
3152
3278
  try {
3153
3279
  await performActions();
3154
- if (!link && timeoutAfterActionMs > 0) {
3155
- await Promise.race([
3156
- matchedPromise,
3157
- new Promise((resolve) => setTimeout(resolve, timeoutAfterActionMs))
3158
- ]);
3280
+ const deadline = Date.now() + Math.max(0, timeoutAfterActionMs);
3281
+ const nonClipboardGraceMs = Math.max(120, Math.min(500, clipboardPollIntervalMs * 2));
3282
+ let nonClipboardSeenAt = null;
3283
+ while (true) {
3284
+ if (enableClipboard) {
3285
+ const clipboardResult = await probeClipboard();
3286
+ if (clipboardResult?.link) {
3287
+ setCandidate("clipboard", clipboardResult.link, clipboardResult.payloadText);
3288
+ }
3289
+ }
3290
+ if (candidates.clipboard?.link) {
3291
+ break;
3292
+ }
3293
+ if (candidates.dom?.link || candidates.response?.link) {
3294
+ if (!nonClipboardSeenAt) {
3295
+ nonClipboardSeenAt = Date.now();
3296
+ } else if (Date.now() - nonClipboardSeenAt >= nonClipboardGraceMs) {
3297
+ break;
3298
+ }
3299
+ }
3300
+ if (Date.now() >= deadline) {
3301
+ break;
3302
+ }
3303
+ const remaining = deadline - Date.now();
3304
+ await new Promise((resolve) => {
3305
+ setTimeout(resolve, Math.max(0, Math.min(clipboardPollIntervalMs, remaining)));
3306
+ });
3159
3307
  }
3308
+ const finalMatch = candidates.clipboard || candidates.dom || candidates.response || null;
3309
+ const finalSource = candidates.clipboard ? "clipboard" : candidates.dom ? "dom" : candidates.response ? "response" : "none";
3310
+ const payloadText = finalMatch?.payloadText || "";
3160
3311
  const payloadSnapshot = payloadText ? payloadText.replace(/\s+/g, " ").trim().slice(0, payloadSnapshotMaxLen) : "";
3161
3312
  return {
3162
- link,
3313
+ link: finalMatch?.link || null,
3163
3314
  payloadText,
3164
3315
  payloadSnapshot,
3165
- source
3316
+ source: finalSource
3166
3317
  };
3167
3318
  } finally {
3168
3319
  if (enableResponse) {