page-action-cache 1.0.1

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.
Files changed (73) hide show
  1. package/dist/actions-executor.d.ts +62 -0
  2. package/dist/actions-executor.d.ts.map +1 -0
  3. package/dist/actions-executor.js +339 -0
  4. package/dist/actions-executor.js.map +1 -0
  5. package/dist/cache-invalidator.d.ts +70 -0
  6. package/dist/cache-invalidator.d.ts.map +1 -0
  7. package/dist/cache-invalidator.js +212 -0
  8. package/dist/cache-invalidator.js.map +1 -0
  9. package/dist/cache-store.d.ts +80 -0
  10. package/dist/cache-store.d.ts.map +1 -0
  11. package/dist/cache-store.js +361 -0
  12. package/dist/cache-store.js.map +1 -0
  13. package/dist/cache-strategy.d.ts +65 -0
  14. package/dist/cache-strategy.d.ts.map +1 -0
  15. package/dist/cache-strategy.js +237 -0
  16. package/dist/cache-strategy.js.map +1 -0
  17. package/dist/hooks-entry.d.ts +18 -0
  18. package/dist/hooks-entry.d.ts.map +1 -0
  19. package/dist/hooks-entry.js +27 -0
  20. package/dist/hooks-entry.js.map +1 -0
  21. package/dist/hooks.d.ts +10 -0
  22. package/dist/hooks.d.ts.map +1 -0
  23. package/dist/hooks.js +277 -0
  24. package/dist/hooks.js.map +1 -0
  25. package/dist/index.d.ts +24 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +34 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/scenario-recognizer.d.ts +45 -0
  30. package/dist/scenario-recognizer.d.ts.map +1 -0
  31. package/dist/scenario-recognizer.js +213 -0
  32. package/dist/scenario-recognizer.js.map +1 -0
  33. package/dist/security-policy.d.ts +62 -0
  34. package/dist/security-policy.d.ts.map +1 -0
  35. package/dist/security-policy.js +219 -0
  36. package/dist/security-policy.js.map +1 -0
  37. package/dist/tools.d.ts +209 -0
  38. package/dist/tools.d.ts.map +1 -0
  39. package/dist/tools.js +383 -0
  40. package/dist/tools.js.map +1 -0
  41. package/dist/types.d.ts +336 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +8 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/ux-enhancer.d.ts +60 -0
  46. package/dist/ux-enhancer.d.ts.map +1 -0
  47. package/dist/ux-enhancer.js +218 -0
  48. package/dist/ux-enhancer.js.map +1 -0
  49. package/dist/variable-resolver.d.ts +28 -0
  50. package/dist/variable-resolver.d.ts.map +1 -0
  51. package/dist/variable-resolver.js +201 -0
  52. package/dist/variable-resolver.js.map +1 -0
  53. package/docs/API.md +555 -0
  54. package/docs/IMPLEMENTATION.md +1792 -0
  55. package/docs/INTEGRATION.md +387 -0
  56. package/docs/README.md +183 -0
  57. package/index.ts +118 -0
  58. package/openclaw.plugin.json +208 -0
  59. package/package.json +76 -0
  60. package/skills/page-action-cache/SKILL.md +216 -0
  61. package/src/actions-executor.ts +441 -0
  62. package/src/cache-invalidator.ts +271 -0
  63. package/src/cache-store.ts +457 -0
  64. package/src/cache-strategy.ts +327 -0
  65. package/src/hooks-entry.ts +114 -0
  66. package/src/hooks.ts +332 -0
  67. package/src/index.ts +104 -0
  68. package/src/scenario-recognizer.ts +259 -0
  69. package/src/security-policy.ts +268 -0
  70. package/src/tools.ts +437 -0
  71. package/src/types.ts +482 -0
  72. package/src/ux-enhancer.ts +266 -0
  73. package/src/variable-resolver.ts +258 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Actions Executor
3
+ * 操作执行器 - 执行缓存的原子化操作
4
+ */
5
+
6
+ import type {
7
+ AtomicAction,
8
+ ExecutionResult,
9
+ PwAi,
10
+ VariableMap,
11
+ NavigateAction,
12
+ ClickAction,
13
+ TypeAction,
14
+ PressAction,
15
+ HoverAction,
16
+ ScreenshotAction,
17
+ EvaluateAction,
18
+ } from "./types.js";
19
+
20
+ // ============================================================================
21
+ // Actions Executor 类
22
+ // ============================================================================
23
+
24
+ /**
25
+ * 操作执行器
26
+ */
27
+ export class ActionsExecutor {
28
+ private pw: PwAi | null = null;
29
+ private cdpUrl: string = "";
30
+
31
+ constructor(cdpUrl?: string) {
32
+ if (cdpUrl) {
33
+ this.cdpUrl = cdpUrl;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 设置 Playwright 客户端
39
+ */
40
+ setPlaywrightClient(pw: PwAi): void {
41
+ this.pw = pw;
42
+ }
43
+
44
+ /**
45
+ * 设置 CDP URL
46
+ */
47
+ setCdpUrl(cdpUrl: string): void {
48
+ this.cdpUrl = cdpUrl;
49
+ }
50
+
51
+ /**
52
+ * 执行单个操作
53
+ */
54
+ async execute(
55
+ action: AtomicAction,
56
+ variables?: VariableMap
57
+ ): Promise<ExecutionResult> {
58
+ const startTime = Date.now();
59
+
60
+ try {
61
+ // 应用变量
62
+ const resolvedAction = this.applyVariables(action, variables);
63
+
64
+ // 根据操作类型执行
65
+ switch (resolvedAction.type) {
66
+ case "navigate":
67
+ await this.executeNavigate(resolvedAction as NavigateAction);
68
+ break;
69
+
70
+ case "click":
71
+ await this.executeClick(resolvedAction as ClickAction);
72
+ break;
73
+
74
+ case "type":
75
+ await this.executeType(resolvedAction as TypeAction);
76
+ break;
77
+
78
+ case "press":
79
+ await this.executePress(resolvedAction as PressAction);
80
+ break;
81
+
82
+ case "hover":
83
+ await this.executeHover(resolvedAction as HoverAction);
84
+ break;
85
+
86
+ case "screenshot":
87
+ await this.executeScreenshot(resolvedAction as ScreenshotAction);
88
+ break;
89
+
90
+ case "scroll":
91
+ case "wait":
92
+ case "select":
93
+ case "focus":
94
+ case "drag":
95
+ case "upload":
96
+ await this.executeEvaluate(resolvedAction as EvaluateAction);
97
+ break;
98
+
99
+ case "composite":
100
+ // 复合操作暂不实现
101
+ throw new Error("Composite actions not yet implemented");
102
+
103
+ default:
104
+ throw new Error(`Unsupported action type: ${action.type}`);
105
+ }
106
+
107
+ return {
108
+ action: action.type,
109
+ success: true,
110
+ duration: Date.now() - startTime,
111
+ };
112
+ } catch (error) {
113
+ return {
114
+ action: action.type,
115
+ success: false,
116
+ error: error instanceof Error ? error.message : String(error),
117
+ duration: Date.now() - startTime,
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 批量执行操作
124
+ */
125
+ async executeBatch(
126
+ actions: AtomicAction[],
127
+ variables?: VariableMap,
128
+ options?: {
129
+ fromIndex?: number;
130
+ toIndex?: number;
131
+ atomic?: boolean; // 是否原子执行(全部成功或全部失败)
132
+ }
133
+ ): Promise<ExecutionResult[]> {
134
+ const from = options?.fromIndex ?? 0;
135
+ const to =
136
+ options?.toIndex !== undefined ? options.toIndex + 1 : actions.length;
137
+ const atomic = options?.atomic ?? false;
138
+
139
+ const results: ExecutionResult[] = [];
140
+ const actionsToExecute = actions.slice(from, to);
141
+
142
+ for (const action of actionsToExecute) {
143
+ const result = await this.execute(action, variables);
144
+ results.push(result);
145
+
146
+ // 原子执行:失败则停止
147
+ if (atomic && !result.success) {
148
+ break;
149
+ }
150
+ }
151
+
152
+ return results;
153
+ }
154
+
155
+ // -------------------------------------------------------------------------
156
+ // 具体操作实现
157
+ // -------------------------------------------------------------------------
158
+
159
+ private async executeNavigate(action: NavigateAction): Promise<void> {
160
+ if (!this.pw) {
161
+ throw new Error("Playwright client not set");
162
+ }
163
+
164
+ await this.pw.navigateViaPlaywright({
165
+ cdpUrl: action.cdpUrl || this.cdpUrl,
166
+ targetId: action.targetId,
167
+ url: action.url,
168
+ waitUntil: action.navigationPolicy?.waitUntil || "domcontentloaded",
169
+ timeout: action.navigationPolicy?.timeout,
170
+ });
171
+ }
172
+
173
+ private async executeClick(action: ClickAction): Promise<void> {
174
+ if (!this.pw) {
175
+ throw new Error("Playwright client not set");
176
+ }
177
+
178
+ await this.pw.clickViaPlaywright({
179
+ cdpUrl: action.cdpUrl || this.cdpUrl,
180
+ targetId: action.targetId,
181
+ ref: action.ref,
182
+ doubleClick: action.doubleClick,
183
+ button: action.button || "left",
184
+ modifiers: action.modifiers,
185
+ timeoutMs: action.timeoutMs,
186
+ });
187
+ }
188
+
189
+ private async executeType(action: TypeAction): Promise<void> {
190
+ if (!this.pw) {
191
+ throw new Error("Playwright client not set");
192
+ }
193
+
194
+ await this.pw.typeViaPlaywright({
195
+ cdpUrl: action.cdpUrl || this.cdpUrl,
196
+ targetId: action.targetId,
197
+ ref: action.ref,
198
+ text: action.text,
199
+ submit: action.submit,
200
+ slowly: action.slowly,
201
+ timeoutMs: action.timeoutMs,
202
+ });
203
+ }
204
+
205
+ private async executePress(action: PressAction): Promise<void> {
206
+ if (!this.pw) {
207
+ throw new Error("Playwright client not set");
208
+ }
209
+
210
+ await this.pw.pressKeyViaPlaywright({
211
+ cdpUrl: action.cdpUrl || this.cdpUrl,
212
+ targetId: action.targetId,
213
+ key: action.key,
214
+ delayMs: action.delayMs,
215
+ });
216
+ }
217
+
218
+ private async executeHover(action: HoverAction): Promise<void> {
219
+ if (!this.pw) {
220
+ throw new Error("Playwright client not set");
221
+ }
222
+
223
+ await this.pw.hoverViaPlaywright({
224
+ cdpUrl: action.cdpUrl || this.cdpUrl,
225
+ targetId: action.targetId,
226
+ ref: action.ref,
227
+ timeoutMs: action.timeoutMs,
228
+ });
229
+ }
230
+
231
+ private async executeScreenshot(action: ScreenshotAction): Promise<string> {
232
+ if (!this.pw) {
233
+ throw new Error("Playwright client not set");
234
+ }
235
+
236
+ return this.pw.screenshotViaPlaywright({
237
+ cdpUrl: action.cdpUrl || this.cdpUrl,
238
+ targetId: action.targetId,
239
+ });
240
+ }
241
+
242
+ private async executeEvaluate(action: EvaluateAction): Promise<any> {
243
+ if (!this.pw) {
244
+ throw new Error("Playwright client not set");
245
+ }
246
+
247
+ return this.pw.evaluateViaPlaywright({
248
+ cdpUrl: action.cdpUrl || this.cdpUrl,
249
+ targetId: action.targetId,
250
+ code: action.evaluate.code,
251
+ args: action.evaluate.args,
252
+ });
253
+ }
254
+
255
+ // -------------------------------------------------------------------------
256
+ // 变量替换
257
+ // -------------------------------------------------------------------------
258
+
259
+ /**
260
+ * 应用变量到操作
261
+ */
262
+ private applyVariables(
263
+ action: AtomicAction,
264
+ variables?: VariableMap
265
+ ): AtomicAction {
266
+ if (!variables) {
267
+ return action;
268
+ }
269
+
270
+ // 检查是否需要变量替换
271
+ if (!action.variable) {
272
+ return action;
273
+ }
274
+
275
+ // 获取变量值
276
+ const value = variables[action.variable];
277
+ if (value === undefined) {
278
+ // 变量未定义,返回原操作
279
+ return action;
280
+ }
281
+
282
+ // 根据操作类型设置正确的参数
283
+ const actionType = action.type;
284
+ if (actionType === "type") {
285
+ return { ...action, text: value as string };
286
+ } else if (actionType === "press") {
287
+ return { ...action, key: value as string };
288
+ } else if (actionType === "navigate") {
289
+ return { ...action, url: value as string };
290
+ } else if (actionType === "scroll" || actionType === "wait" ||
291
+ actionType === "select" || actionType === "focus" ||
292
+ actionType === "drag" || actionType === "upload") {
293
+ // evaluate 操作替换模板变量
294
+ return this.replaceTemplateVariables(action, variables);
295
+ } else {
296
+ // 其他操作替换所有字符串参数中的模板变量
297
+ return this.replaceTemplateVariables(action, variables);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * 替换操作中的模板变量
303
+ */
304
+ private replaceTemplateVariables(
305
+ action: AtomicAction,
306
+ variables: VariableMap
307
+ ): AtomicAction {
308
+ const result: any = { ...action };
309
+
310
+ // 替换字符串类型参数中的 ${variable}
311
+ for (const key of Object.keys(action)) {
312
+ const value = (action as any)[key];
313
+ if (typeof value === "string") {
314
+ result[key] = value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
315
+ const replacement = variables[varName];
316
+ return replacement !== undefined ? replacement : match;
317
+ });
318
+ }
319
+ }
320
+
321
+ // 特殊处理 evaluate.code(如果存在)
322
+ if (result.evaluate?.code && typeof result.evaluate.code === "string") {
323
+ result.evaluate.code = result.evaluate.code.replace(
324
+ /\$\{([^}]+)\}/g,
325
+ (match: string, varName: string) => {
326
+ const replacement = variables[varName];
327
+ return replacement !== undefined ? replacement : match;
328
+ }
329
+ );
330
+ }
331
+
332
+ return result;
333
+ }
334
+
335
+ // -------------------------------------------------------------------------
336
+ // 格式化输出
337
+ // -------------------------------------------------------------------------
338
+
339
+ /**
340
+ * 格式化操作列表
341
+ */
342
+ formatActions(actions: AtomicAction[]): string {
343
+ return actions
344
+ .map((action, i) => this.formatAction(action, i + 1))
345
+ .join("\n");
346
+ }
347
+
348
+ /**
349
+ * 格式化单个操作
350
+ */
351
+ private formatAction(action: AtomicAction, index: number): string {
352
+ switch (action.type) {
353
+ case "navigate":
354
+ return `${index}. 导航到:${(action as NavigateAction).url}`;
355
+
356
+ case "screenshot":
357
+ return `${index}. 截图`;
358
+
359
+ case "click":
360
+ const btn = (action as ClickAction).button || "left";
361
+ const dbl = (action as ClickAction).doubleClick ? "双击" : "";
362
+ const mod = (action as ClickAction).modifiers?.length
363
+ ? `+${(action as ClickAction).modifiers!.join("+")}`
364
+ : "";
365
+ return `${index}. ${dbl}点击(${btn}${mod}):${action.ref}`;
366
+
367
+ case "type":
368
+ const txt = (action as TypeAction).text
369
+ ? `="${(action as TypeAction).text}"`
370
+ : "";
371
+ const sub = (action as TypeAction).submit ? " [提交]" : "";
372
+ const slow = (action as TypeAction).slowly ? " [慢速]" : "";
373
+ return `${index}. 输入:${action.ref}${txt}${sub}${slow}`;
374
+
375
+ case "press":
376
+ const delay = (action as PressAction).delayMs
377
+ ? `(${(action as PressAction).delayMs}ms)`
378
+ : "";
379
+ return `${index}. 按键:${(action as PressAction).key}${delay}${
380
+ action.ref ? ` @ ${action.ref}` : ""
381
+ }`;
382
+
383
+ case "hover":
384
+ return `${index}. 悬停:${action.ref}`;
385
+
386
+ case "scroll":
387
+ return `${index}. 滚动:${
388
+ (action as EvaluateAction).evaluate?.code || "向下滚动"
389
+ }`;
390
+
391
+ case "wait":
392
+ const waitDelay = action.delay ? `(${action.delay}ms)` : "";
393
+ return `${index}. 等待:${
394
+ (action as EvaluateAction).evaluate?.code || "默认等待"
395
+ }${waitDelay}`;
396
+
397
+ case "select":
398
+ return `${index}. 选择:${action.ref}`;
399
+
400
+ case "focus":
401
+ return `${index}. 聚焦:${action.ref}`;
402
+
403
+ case "drag":
404
+ return `${index}. 拖拽:${action.ref}`;
405
+
406
+ case "upload":
407
+ return `${index}. 上传:${action.ref}`;
408
+
409
+ default:
410
+ return `${index}. ${action.type}:${action.ref || ""}`;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * 格式化执行结果
416
+ */
417
+ formatExecutionResults(results: ExecutionResult[]): string {
418
+ return results
419
+ .map((r, i) => {
420
+ const icon = r.success ? "✅" : "❌";
421
+ const time = r.duration ? `(${r.duration}ms)` : "";
422
+ return `${icon} 操作 ${i + 1}: ${r.action} ${time}${
423
+ r.error ? ` - ${r.error}` : ""
424
+ }`;
425
+ })
426
+ .join("\n");
427
+ }
428
+ }
429
+
430
+ // ============================================================================
431
+ // 单例
432
+ // ============================================================================
433
+
434
+ let actionsExecutorInstance: ActionsExecutor | null = null;
435
+
436
+ export function getActionsExecutor(cdpUrl?: string): ActionsExecutor {
437
+ if (!actionsExecutorInstance) {
438
+ actionsExecutorInstance = new ActionsExecutor(cdpUrl);
439
+ }
440
+ return actionsExecutorInstance;
441
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Cache Invalidator
3
+ * 缓存失效检测器 - 检测页面变化并失效缓存
4
+ */
5
+
6
+ import type {
7
+ PageViewport,
8
+ PageChangeDetection,
9
+ PageActionCacheEntry,
10
+ DOMHashFeatures,
11
+ } from "./types.js";
12
+
13
+ // ============================================================================
14
+ // Cache Invalidator 类
15
+ // ============================================================================
16
+
17
+ /**
18
+ * 缓存失效检测器
19
+ */
20
+ export class CacheInvalidator {
21
+ /**
22
+ * 检测页面是否发生变化
23
+ */
24
+ async detectPageChange(
25
+ html: string,
26
+ _url: string,
27
+ _viewport: PageViewport,
28
+ cachedEntry?: PageActionCacheEntry
29
+ ): Promise<PageChangeDetection> {
30
+ // 计算当前 DOM hash
31
+ const currentHash = this.calculateDOMHash(html);
32
+
33
+ if (!cachedEntry || !cachedEntry.pageChangeDetection) {
34
+ return {
35
+ hasChanged: true,
36
+ changeType: "none",
37
+ confidence: 0,
38
+ domHash: currentHash,
39
+ lastCheckedAt: Date.now(),
40
+ };
41
+ }
42
+
43
+ const cachedHash = cachedEntry.pageChangeDetection.domHash;
44
+
45
+ // 比较 hash
46
+ if (currentHash === cachedHash) {
47
+ return {
48
+ hasChanged: false,
49
+ changeType: "none",
50
+ confidence: 100,
51
+ domHash: currentHash,
52
+ lastCheckedAt: Date.now(),
53
+ };
54
+ }
55
+
56
+ // 检测变化类型
57
+ const structureChanged = this.detectStructureChange(html);
58
+ const contentChanged = this.detectContentChange(html, cachedEntry);
59
+
60
+ if (structureChanged || contentChanged) {
61
+ return {
62
+ hasChanged: true,
63
+ changeType: structureChanged ? "structure" : "content",
64
+ confidence: Math.max(
65
+ structureChanged ? 80 : 0,
66
+ contentChanged ? 80 : 0
67
+ ),
68
+ domHash: currentHash,
69
+ lastCheckedAt: Date.now(),
70
+ details: structureChanged
71
+ ? "Form/Button count changed"
72
+ : "Content hash changed",
73
+ };
74
+ }
75
+
76
+ // hash 不同但结构未变,可能是动态内容
77
+ return {
78
+ hasChanged: true,
79
+ changeType: "content",
80
+ confidence: 50, // 低置信度,可能是动态内容
81
+ domHash: currentHash,
82
+ lastCheckedAt: Date.now(),
83
+ details: "Hash changed but structure is similar",
84
+ };
85
+ }
86
+
87
+ /**
88
+ * 计算 DOM hash
89
+ */
90
+ calculateDOMHash(html: string): string {
91
+ const features = this.extractDOMFeatures(html);
92
+ return this.combineFeaturesHash(features);
93
+ }
94
+
95
+ /**
96
+ * 提取 DOM 特征
97
+ */
98
+ private extractDOMFeatures(html: string): DOMHashFeatures {
99
+ return {
100
+ formCount: this.countElements(html, "form"),
101
+ buttonCount: this.countElements(html, "button"),
102
+ linkCount: this.countElements(html, "a"),
103
+ scriptCount: this.countElements(html, "script"),
104
+ headingCount:
105
+ this.countElements(html, "h1") +
106
+ this.countElements(html, "h2") +
107
+ this.countElements(html, "h3"),
108
+ };
109
+ }
110
+
111
+ /**
112
+ * 计算元素数量
113
+ */
114
+ private countElements(html: string, tagName: string): number {
115
+ const regex = new RegExp(`<${tagName}\\b`, "gi");
116
+ return (html.match(regex) || []).length;
117
+ }
118
+
119
+ /**
120
+ * 组合特征为 hash
121
+ */
122
+ private combineFeaturesHash(features: DOMHashFeatures): string {
123
+ const values = [
124
+ features.formCount,
125
+ features.buttonCount,
126
+ features.linkCount,
127
+ features.scriptCount,
128
+ features.headingCount,
129
+ ];
130
+
131
+ // 简单 hash 算法
132
+ let hash = 0;
133
+ for (const value of values) {
134
+ hash = ((hash << 5) - hash + value) | 0;
135
+ }
136
+ return hash.toString(16);
137
+ }
138
+
139
+ /**
140
+ * 检测结构变化
141
+ */
142
+ private detectStructureChange(html: string): boolean {
143
+ // 检查表单和按钮数量是否发生显著变化
144
+ const features = this.extractDOMFeatures(html);
145
+
146
+ // 如果表单数量变化,认为是结构变化
147
+ // 这里简化处理,实际应该与缓存条目比较
148
+ const hasForm = features.formCount > 0;
149
+
150
+ return !hasForm;
151
+ }
152
+
153
+ /**
154
+ * 检测内容变化
155
+ */
156
+ private detectContentChange(
157
+ html: string,
158
+ cachedEntry: PageActionCacheEntry
159
+ ): boolean {
160
+ // 简化处理:检查 HTML 长度变化
161
+ const lengthChanged = Math.abs(html.length - (cachedEntry.actions.length * 100)) > 500;
162
+
163
+ return lengthChanged;
164
+ }
165
+
166
+ /**
167
+ * 判断是否应该失效缓存
168
+ */
169
+ shouldInvalidate(
170
+ entry: PageActionCacheEntry,
171
+ changeResult: PageChangeDetection,
172
+ threshold: number = 80
173
+ ): boolean {
174
+ // 根据页面类型和置信度决定是否失效
175
+ const pageType = entry.pageType || "mixed";
176
+
177
+ if (pageType === "static") {
178
+ // 静态页面:高置信度时失效
179
+ return changeResult.hasChanged && changeResult.confidence >= threshold;
180
+ } else if (pageType === "dynamic") {
181
+ // 动态页面:只在结构变化时失效
182
+ return (
183
+ changeResult.hasChanged && changeResult.changeType === "structure"
184
+ );
185
+ } else {
186
+ // 混合页面:中等置信度时失效
187
+ return (
188
+ changeResult.hasChanged && changeResult.confidence >= threshold - 10
189
+ );
190
+ }
191
+ }
192
+
193
+ /**
194
+ * 创建渐进式失效器
195
+ */
196
+ createProgressiveInvalidator(): ProgressiveInvalidator {
197
+ return new ProgressiveInvalidator();
198
+ }
199
+ }
200
+
201
+ // ============================================================================
202
+ // Progressive Invalidator 类
203
+ // ============================================================================
204
+
205
+ /**
206
+ * 渐进式失效:保留多版本
207
+ */
208
+ export class ProgressiveInvalidator {
209
+ private versions: Map<string, string[]> = new Map();
210
+
211
+ /**
212
+ * 添加版本
213
+ */
214
+ addVersion(entry: PageActionCacheEntry, versionId: string): void {
215
+ const key = entry.key;
216
+
217
+ if (!this.versions.has(key)) {
218
+ this.versions.set(key, []);
219
+ }
220
+
221
+ const versions = this.versions.get(key)!;
222
+ versions.push(versionId);
223
+
224
+ // 只保留最近的 N 个版本(默认 3 个)
225
+ if (versions.length > 3) {
226
+ versions.shift();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * 获取版本列表
232
+ */
233
+ getVersions(key: string): string[] {
234
+ return this.versions.get(key) || [];
235
+ }
236
+
237
+ /**
238
+ * 生成版本标识
239
+ */
240
+ generateVersion(entry: PageActionCacheEntry): string {
241
+ const time = new Date(entry.createdAt).toISOString().slice(0, 10); // YYYY-MM-DD
242
+ const pageType = entry.pageType || "unknown";
243
+
244
+ return `${pageType}-${entry.scenario}-${time}`;
245
+ }
246
+
247
+ /**
248
+ * 清理旧版本
249
+ */
250
+ cleanupOldVersions(key: string, maxVersions: number = 3): void {
251
+ const versions = this.getVersions(key);
252
+
253
+ if (versions.length > maxVersions) {
254
+ const toKeep = versions.slice(-maxVersions);
255
+ this.versions.set(key, toKeep);
256
+ }
257
+ }
258
+ }
259
+
260
+ // ============================================================================
261
+ // 单例
262
+ // ============================================================================
263
+
264
+ let cacheInvalidatorInstance: CacheInvalidator | null = null;
265
+
266
+ export function getCacheInvalidator(): CacheInvalidator {
267
+ if (!cacheInvalidatorInstance) {
268
+ cacheInvalidatorInstance = new CacheInvalidator();
269
+ }
270
+ return cacheInvalidatorInstance;
271
+ }