@skrillex1224/playwright-toolkit 2.1.113 → 2.1.115

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
  }
@@ -3025,178 +3051,178 @@ var Logger = {
3025
3051
 
3026
3052
  // src/share.js
3027
3053
  var import_delay3 = __toESM(require("delay"), 1);
3028
-
3029
- // src/internals/monitor.js
3030
- var DEFAULT_DOM_SELECTORS = "html";
3031
- var Monitor = {
3032
- /**
3033
- * 使用 Mutation.useMonitor 监控 DOM,并提取分享链接
3034
- *
3035
- * @param {import('playwright').Page} page
3036
- * @param {Object} [options]
3037
- * @param {string[]} [options.identities]
3038
- * @param {string | string[]} [options.selectors]
3039
- * @param {'added' | 'changed' | 'all'} [options.mode]
3040
- * @param {(text: string, options?: { identities?: string[] }) => string[]} [options.parseLinks]
3041
- * @param {(payload: { link: string; rawDom: string; mutationCount: number; html: string; text: string; mutationNodes: Array<{ html: string; text: string; mutationType: string }> }) => void} [options.onMatch]
3042
- * @returns {Promise<{ stop: () => Promise<{ totalMutations: number }> }>}
3043
- */
3044
- async useShareLinkMonitor(page, options = {}) {
3045
- const identities = Array.isArray(options.identities) ? options.identities : [];
3046
- const selectors = options.selectors ?? DEFAULT_DOM_SELECTORS;
3047
- const mode = options.mode ?? Mutation.Mode.Added;
3048
- const parseLinks = typeof options.parseLinks === "function" ? options.parseLinks : Utils.parseLinks;
3049
- const onMatch = typeof options.onMatch === "function" ? options.onMatch : null;
3050
- let matched = false;
3051
- const monitor = await Mutation.useMonitor(page, selectors, {
3052
- mode,
3053
- onMutation: (context = {}) => {
3054
- if (matched) return;
3055
- const html = String(context.html || "");
3056
- const text = String(context.text || "");
3057
- const rawDom = `${html}
3058
- ${text}`;
3059
- const [candidate] = parseLinks(rawDom, { identities }) || [];
3060
- if (!candidate) return;
3061
- matched = true;
3062
- if (onMatch) {
3063
- onMatch({
3064
- link: candidate,
3065
- rawDom,
3066
- mutationCount: context.mutationCount || 0,
3067
- html,
3068
- text,
3069
- mutationNodes: Array.isArray(context.mutationNodes) ? context.mutationNodes : []
3070
- });
3071
- }
3072
- }
3073
- });
3074
- return {
3075
- stop: async () => {
3076
- return await monitor.stop();
3077
- }
3078
- };
3079
- }
3080
- };
3081
-
3082
- // src/share.js
3083
3054
  var DEFAULT_TIMEOUT_AFTER_ACTION_MS = 10 * 1e3;
3084
3055
  var DEFAULT_PAYLOAD_SNAPSHOT_MAX_LEN = 500;
3056
+ var DEFAULT_POLL_INTERVAL_MS = 120;
3057
+ var createRuntimeKey = (prefix) => `__${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
3085
3058
  var Share = {
3086
3059
  /**
3087
- * 捕获分享链接(接口响应 + DOM 监听 双通道)
3060
+ * 捕获分享链接(仅被动剪切板写入通道)
3061
+ * 不做 clipboard-read,避免权限弹窗
3088
3062
  *
3089
3063
  * @param {import('playwright').Page} page
3090
3064
  * @param {Object} [options]
3091
- * @param {string[]} [options.identities]
3065
+ * @param {(string | RegExp)[]} [options.identities]
3092
3066
  * @param {number} [options.timeoutAfterActionMs]
3093
3067
  * @param {number} [options.payloadSnapshotMaxLen]
3094
- * @param {boolean} [options.enableResponse=true]
3095
- * @param {boolean} [options.enableDom=true]
3096
- * @param {string | string[]} [options.domSelectors='html']
3097
- * @param {'added' | 'changed' | 'all'} [options.domMode='added']
3098
- * @param {(response: import('playwright').Response) => boolean | Promise<boolean>} [options.responseFilter]
3099
- * @param {(text: string, options?: { identities?: string[] }) => string[]} [options.parseLinks]
3100
3068
  * @param {() => Promise<void>} [options.performActions]
3101
- * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'response' | 'dom' | 'none' }>}
3069
+ * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'clipboard' | 'none' }>}
3102
3070
  */
3103
3071
  async captureLink(page, options = {}) {
3104
3072
  const identities = Array.isArray(options.identities) ? options.identities : [];
3105
3073
  const timeoutAfterActionMs = options.timeoutAfterActionMs ?? DEFAULT_TIMEOUT_AFTER_ACTION_MS;
3074
+ const totalTimeoutMs = Math.max(0, Number(timeoutAfterActionMs) || 0);
3106
3075
  const payloadSnapshotMaxLen = options.payloadSnapshotMaxLen ?? DEFAULT_PAYLOAD_SNAPSHOT_MAX_LEN;
3107
- const enableResponse = options.enableResponse !== false;
3108
- const enableDom = options.enableDom !== false;
3109
- const domSelectors = options.domSelectors ?? "html";
3110
- const domMode = options.domMode ?? Mutation.Mode.All;
3111
- const responseFilter = typeof options.responseFilter === "function" ? options.responseFilter : null;
3112
- const parseLinks = typeof options.parseLinks === "function" ? options.parseLinks : Utils.parseLinks;
3113
3076
  const performActions = typeof options.performActions === "function" ? options.performActions : async () => {
3114
3077
  };
3115
- if (!enableResponse && !enableDom) {
3116
- throw new Error("Share.captureLink requires at least one channel: response or dom");
3117
- }
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);
3132
- return true;
3078
+ const clipboardKeys = {
3079
+ state: createRuntimeKey("pk_clip_state"),
3080
+ hookReady: createRuntimeKey("pk_clip_hook_ready"),
3081
+ copyListener: createRuntimeKey("pk_clip_copy_listener"),
3082
+ execWrapped: createRuntimeKey("pk_clip_exec_wrapped"),
3083
+ writeTextWrapped: createRuntimeKey("pk_clip_write_text_wrapped"),
3084
+ writeWrapped: createRuntimeKey("pk_clip_write_wrapped")
3133
3085
  };
3134
- const stopDomMonitor = async () => {
3135
- if (!domMonitor) return;
3136
- const monitor = domMonitor;
3137
- domMonitor = null;
3086
+ const ensureClipboardHooks = async () => {
3138
3087
  try {
3139
- await monitor.stop();
3088
+ await page.evaluate(({ keys }) => {
3089
+ if (document[keys.hookReady]) return;
3090
+ const state = document[keys.state] || { captured: "" };
3091
+ document[keys.state] = state;
3092
+ const normalize = (value) => String(value || "").trim();
3093
+ const setCaptured = (value) => {
3094
+ const safe = normalize(value);
3095
+ if (!safe) return;
3096
+ state.captured = safe;
3097
+ };
3098
+ try {
3099
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
3100
+ const clipboard = navigator.clipboard;
3101
+ if (!clipboard[keys.writeTextWrapped]) {
3102
+ const originalWriteText = clipboard.writeText.bind(clipboard);
3103
+ clipboard.writeText = async (text) => {
3104
+ setCaptured(text);
3105
+ return await originalWriteText(text);
3106
+ };
3107
+ clipboard[keys.writeTextWrapped] = true;
3108
+ }
3109
+ }
3110
+ } catch {
3111
+ }
3112
+ try {
3113
+ if (navigator.clipboard && typeof navigator.clipboard.write === "function") {
3114
+ const clipboard = navigator.clipboard;
3115
+ if (!clipboard[keys.writeWrapped]) {
3116
+ const originalWrite = clipboard.write.bind(clipboard);
3117
+ clipboard.write = async (data) => {
3118
+ try {
3119
+ if (Array.isArray(data) && data.length > 0) {
3120
+ for (const item of data) {
3121
+ if (!item?.types?.length) continue;
3122
+ if (!item.types.includes("text/plain")) continue;
3123
+ const blob = await item.getType("text/plain");
3124
+ const text = await blob.text();
3125
+ setCaptured(text);
3126
+ break;
3127
+ }
3128
+ }
3129
+ } catch {
3130
+ }
3131
+ return await originalWrite(data);
3132
+ };
3133
+ clipboard[keys.writeWrapped] = true;
3134
+ }
3135
+ }
3136
+ } catch {
3137
+ }
3138
+ try {
3139
+ if (!document[keys.copyListener]) {
3140
+ document.addEventListener("copy", (event) => {
3141
+ try {
3142
+ const copied = event?.clipboardData?.getData?.("text/plain") || "";
3143
+ setCaptured(copied);
3144
+ } catch {
3145
+ }
3146
+ }, true);
3147
+ document[keys.copyListener] = true;
3148
+ }
3149
+ } catch {
3150
+ }
3151
+ try {
3152
+ if (typeof document.execCommand === "function" && !document[keys.execWrapped]) {
3153
+ const originalExecCommand = document.execCommand.bind(document);
3154
+ document.execCommand = function(command, ...args) {
3155
+ try {
3156
+ if (String(command || "").toLowerCase() === "copy") {
3157
+ const selected = globalThis.getSelection?.().toString?.() || "";
3158
+ setCaptured(selected);
3159
+ }
3160
+ } catch {
3161
+ }
3162
+ return originalExecCommand(command, ...args);
3163
+ };
3164
+ document[keys.execWrapped] = true;
3165
+ }
3166
+ } catch {
3167
+ }
3168
+ document[keys.hookReady] = true;
3169
+ }, { keys: clipboardKeys });
3140
3170
  } catch {
3141
3171
  }
3142
3172
  };
3143
- const onResponse = async (response) => {
3144
- if (link) return;
3173
+ const probeCapturedLink = async () => {
3145
3174
  try {
3146
- if (responseFilter) {
3147
- const accepted = await responseFilter(response);
3148
- if (!accepted) return;
3149
- }
3150
- const text = await response.text();
3151
- if (!text) return;
3152
- const [candidate] = parseLinks(text, { identities }) || [];
3153
- if (!candidate) return;
3154
- const matched = finalizeMatch(candidate, "response", text);
3155
- if (!matched) return;
3156
- page.off("response", onResponse);
3157
- void stopDomMonitor();
3175
+ const capturedText = await page.evaluate(({ keys }) => {
3176
+ return String(document[keys.state]?.captured || "").trim();
3177
+ }, { keys: clipboardKeys });
3178
+ const [capturedLink] = Utils.parseLinks(capturedText || "", { identities }) || [];
3179
+ if (!capturedLink) return null;
3180
+ return {
3181
+ link: capturedLink,
3182
+ payloadText: capturedText || ""
3183
+ };
3158
3184
  } catch {
3185
+ return null;
3159
3186
  }
3160
3187
  };
3161
- if (enableDom) {
3162
- domMonitor = await Monitor.useShareLinkMonitor(page, {
3163
- identities,
3164
- selectors: domSelectors,
3165
- mode: domMode,
3166
- parseLinks,
3167
- onMatch: ({ link: domLink, rawDom }) => {
3168
- const matched = finalizeMatch(domLink, "dom", rawDom);
3169
- if (!matched) return;
3170
- if (enableResponse) {
3171
- page.off("response", onResponse);
3172
- }
3173
- }
3174
- });
3175
- }
3176
- if (enableResponse) {
3177
- page.on("response", onResponse);
3188
+ await ensureClipboardHooks();
3189
+ const deadline = Date.now() + totalTimeoutMs;
3190
+ const getRemainingMs = () => Math.max(0, deadline - Date.now());
3191
+ let timer = null;
3192
+ let actionError = null;
3193
+ const actionPromise = Promise.resolve().then(() => performActions()).then(() => "__ACTION_DONE__").catch((error) => {
3194
+ actionError = error;
3195
+ return "__ACTION_ERROR__";
3196
+ });
3197
+ const timeoutPromise = new Promise((resolve) => {
3198
+ timer = setTimeout(() => resolve("__ACTION_TIMEOUT__"), Math.max(1, getRemainingMs()));
3199
+ });
3200
+ const actionResult = await Promise.race([actionPromise, timeoutPromise]);
3201
+ if (timer) clearTimeout(timer);
3202
+ if (actionResult === "__ACTION_ERROR__") {
3203
+ throw actionError;
3178
3204
  }
3179
- try {
3180
- await performActions();
3181
- if (!link && timeoutAfterActionMs > 0) {
3182
- await Promise.race([
3183
- matchedPromise,
3184
- new Promise((resolve) => setTimeout(resolve, timeoutAfterActionMs))
3185
- ]);
3186
- }
3187
- const payloadSnapshot = payloadText ? payloadText.replace(/\s+/g, " ").trim().slice(0, payloadSnapshotMaxLen) : "";
3188
- return {
3189
- link,
3190
- payloadText,
3191
- payloadSnapshot,
3192
- source
3193
- };
3194
- } finally {
3195
- if (enableResponse) {
3196
- page.off("response", onResponse);
3205
+ while (true) {
3206
+ const captured = await probeCapturedLink();
3207
+ if (captured?.link) {
3208
+ const payloadSnapshot = captured.payloadText ? captured.payloadText.replace(/\s+/g, " ").trim().slice(0, payloadSnapshotMaxLen) : "";
3209
+ return {
3210
+ link: captured.link,
3211
+ payloadText: captured.payloadText,
3212
+ payloadSnapshot,
3213
+ source: "clipboard"
3214
+ };
3197
3215
  }
3198
- await stopDomMonitor();
3216
+ const remaining = getRemainingMs();
3217
+ if (remaining <= 0) break;
3218
+ await (0, import_delay3.default)(Math.max(0, Math.min(DEFAULT_POLL_INTERVAL_MS, remaining)));
3199
3219
  }
3220
+ return {
3221
+ link: null,
3222
+ payloadText: "",
3223
+ payloadSnapshot: "",
3224
+ source: "none"
3225
+ };
3200
3226
  },
3201
3227
  /**
3202
3228
  * 全页面滚动截图(基础能力)