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,457 @@
1
+ /**
2
+ * Page Action Cache Store
3
+ * 管理页面操作缓存的存储
4
+ */
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { createHash } from "node:crypto";
9
+ import type {
10
+ PageActionCacheEntry,
11
+ PageActionCacheStore,
12
+ PageViewport,
13
+ CacheStats,
14
+ ScenarioMatch,
15
+ } from "./types.js";
16
+
17
+ // ============================================================================
18
+ // 配置
19
+ // ============================================================================
20
+
21
+ const CACHE_DIR = path.join(
22
+ process.env.HOME || process.env.USERPROFILE || ".",
23
+ ".openclaw",
24
+ "cache",
25
+ "page-actions"
26
+ );
27
+
28
+ const CACHE_FILE = path.join(CACHE_DIR, "cache.json");
29
+ const CACHE_VERSION = 1;
30
+
31
+ const DEFAULT_TTL = {
32
+ L3: 7 * 24 * 60 * 60 * 1000, // 7 天
33
+ L2: 3 * 24 * 60 * 60 * 1000, // 3 天
34
+ L1: 30 * 60 * 1000, // 30 分钟
35
+ };
36
+
37
+ // ============================================================================
38
+ // CacheStore 类
39
+ // ============================================================================
40
+
41
+ /**
42
+ * 缓存存储管理器
43
+ */
44
+ export class CacheStore {
45
+ private store: PageActionCacheStore;
46
+
47
+ constructor() {
48
+ this.store = this.load();
49
+ }
50
+
51
+ // -------------------------------------------------------------------------
52
+ // 初始化和加载
53
+ // -------------------------------------------------------------------------
54
+
55
+ private load(): PageActionCacheStore {
56
+ try {
57
+ if (fs.existsSync(CACHE_FILE)) {
58
+ const data = fs.readFileSync(CACHE_FILE, "utf-8");
59
+ const loaded = JSON.parse(data) as PageActionCacheStore;
60
+
61
+ // 版本检查
62
+ if (loaded.version !== CACHE_VERSION) {
63
+ console.warn(
64
+ `[CacheStore] Cache version mismatch (expected ${CACHE_VERSION}, got ${loaded.version}), clearing cache`
65
+ );
66
+ return this.createEmptyStore();
67
+ }
68
+
69
+ return loaded;
70
+ }
71
+ } catch (error) {
72
+ console.error(`[CacheStore] Failed to load cache:`, error);
73
+ }
74
+
75
+ return this.createEmptyStore();
76
+ }
77
+
78
+ private createEmptyStore(): PageActionCacheStore {
79
+ return {
80
+ version: CACHE_VERSION,
81
+ entries: {},
82
+ scenarios: new Map<string, ScenarioMatch>(),
83
+ stats: {
84
+ totalEntries: 0,
85
+ totalHits: 0,
86
+ totalMisses: 0,
87
+ hitRate: 0,
88
+ l3Hits: 0,
89
+ l2Hits: 0,
90
+ l1Hits: 0,
91
+ scenarioMatches: 0,
92
+ llmClassifications: 0,
93
+ learnedAssociations: 0,
94
+ savedTokens: 0,
95
+ savedTime: 0,
96
+ avgExecutionTime: 0,
97
+ userConfirmations: 0,
98
+ userForcedRefreshes: 0,
99
+ cacheErrors: 0,
100
+ },
101
+ };
102
+ }
103
+
104
+ private ensureCacheDir(): void {
105
+ if (!fs.existsSync(CACHE_DIR)) {
106
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
107
+ }
108
+ }
109
+
110
+ // -------------------------------------------------------------------------
111
+ // 保存
112
+ // -------------------------------------------------------------------------
113
+
114
+ private save(): void {
115
+ try {
116
+ this.ensureCacheDir();
117
+
118
+ // 序列化(处理 Map)
119
+ const serialized = {
120
+ ...this.store,
121
+ scenarios: Array.from(this.store.scenarios.entries()),
122
+ };
123
+
124
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(serialized, null, 2));
125
+ } catch (error) {
126
+ console.error(`[CacheStore] Failed to save cache:`, error);
127
+ }
128
+ }
129
+
130
+ private scheduleSave(): void {
131
+ // 防抖保存(100ms)
132
+ if (this._saveTimeout) {
133
+ clearTimeout(this._saveTimeout);
134
+ }
135
+ this._saveTimeout = setTimeout(() => this.save(), 100);
136
+ }
137
+
138
+ private _saveTimeout: NodeJS.Timeout | null = null;
139
+
140
+ // -------------------------------------------------------------------------
141
+ // 缓存键生成
142
+ // -------------------------------------------------------------------------
143
+
144
+ private generateKey(url: string, viewport: PageViewport): string {
145
+ const normalizedUrl = this.normalizeUrl(url);
146
+ const viewportStr = `${viewport.width}x${viewport.height}`;
147
+ const hash = createHash("md5")
148
+ .update(`${normalizedUrl}:${viewportStr}`)
149
+ .digest("hex");
150
+ return hash || `fallback-${Date.now()}`; // 确保 hash 不为空
151
+ }
152
+
153
+ private normalizeUrl(url: string): string {
154
+ try {
155
+ const parsed = new URL(url);
156
+ // 移除查询参数和哈希(可根据配置调整)
157
+ parsed.search = "";
158
+ parsed.hash = "";
159
+ return parsed.toString();
160
+ } catch {
161
+ return url;
162
+ }
163
+ }
164
+
165
+ // -------------------------------------------------------------------------
166
+ // 基本操作
167
+ // -------------------------------------------------------------------------
168
+
169
+ /**
170
+ * 设置缓存条目
171
+ */
172
+ set(
173
+ url: string,
174
+ viewport: PageViewport,
175
+ actions: any[],
176
+ metadata: {
177
+ scenario: string;
178
+ cacheLevel: "L3" | "L2" | "L1";
179
+ variables?: Record<string, string>;
180
+ description?: string;
181
+ pageType?: "static" | "dynamic" | "mixed";
182
+ }
183
+ ): string {
184
+ const key = this.generateKey(url, viewport);
185
+ const now = Date.now();
186
+ const ttl = DEFAULT_TTL[metadata.cacheLevel];
187
+
188
+ const entry: PageActionCacheEntry = {
189
+ key,
190
+ url: this.normalizeUrl(url),
191
+ viewport,
192
+ cacheLevel: metadata.cacheLevel,
193
+ scenario: metadata.scenario,
194
+ description: metadata.description || `Cached ${metadata.scenario} actions`,
195
+ variables: metadata.variables,
196
+ actions,
197
+ createdAt: now,
198
+ lastAccessTime: now,
199
+ accessCount: 0,
200
+ expiresAt: now + ttl,
201
+ pageChangeDetection: {
202
+ hasChanged: false,
203
+ changeType: "none",
204
+ confidence: 100,
205
+ domHash: "",
206
+ lastCheckedAt: now,
207
+ },
208
+ source: "llm",
209
+ version: 1,
210
+ successCount: 0,
211
+ failCount: 0,
212
+ avgExecutionTime: 0,
213
+ pageType: metadata.pageType || "mixed",
214
+ };
215
+
216
+ this.store.entries[key] = entry;
217
+ this.store.stats.totalEntries++;
218
+
219
+ this.scheduleSave();
220
+ return key;
221
+ }
222
+
223
+ /**
224
+ * 获取缓存条目
225
+ */
226
+ get(url: string, viewport: PageViewport): PageActionCacheEntry | null {
227
+ const key = this.generateKey(url, viewport);
228
+ const entry = this.store.entries[key];
229
+
230
+ if (!entry) {
231
+ this.store.stats.totalMisses++;
232
+ this.updateHitRate();
233
+ return null;
234
+ }
235
+
236
+ // 检查过期
237
+ if (Date.now() > entry.expiresAt) {
238
+ delete this.store.entries[key];
239
+ this.store.stats.totalEntries--;
240
+ this.store.stats.totalMisses++;
241
+ this.updateHitRate();
242
+ this.scheduleSave();
243
+ return null;
244
+ }
245
+
246
+ // 更新访问统计
247
+ entry.lastAccessTime = Date.now();
248
+ entry.accessCount++;
249
+
250
+ // 更新层级统计
251
+ this.store.stats.totalHits++;
252
+ switch (entry.cacheLevel) {
253
+ case "L3":
254
+ this.store.stats.l3Hits++;
255
+ break;
256
+ case "L2":
257
+ this.store.stats.l2Hits++;
258
+ break;
259
+ case "L1":
260
+ this.store.stats.l1Hits++;
261
+ break;
262
+ }
263
+
264
+ this.updateHitRate();
265
+ this.scheduleSave();
266
+
267
+ return entry;
268
+ }
269
+
270
+ /**
271
+ * 删除缓存条目
272
+ */
273
+ delete(url: string, viewport: PageViewport): boolean {
274
+ const key = this.generateKey(url, viewport);
275
+ const existed = !!this.store.entries[key];
276
+
277
+ if (existed) {
278
+ delete this.store.entries[key];
279
+ this.store.stats.totalEntries--;
280
+ this.scheduleSave();
281
+ }
282
+
283
+ return existed;
284
+ }
285
+
286
+ /**
287
+ * 按模式删除缓存条目
288
+ */
289
+ deleteByPattern(urlPattern: string): number {
290
+ let deleted = 0;
291
+
292
+ for (const [key, entry] of Object.entries(this.store.entries)) {
293
+ if (entry.url.includes(urlPattern)) {
294
+ delete this.store.entries[key];
295
+ this.store.stats.totalEntries--;
296
+ deleted++;
297
+ }
298
+ }
299
+
300
+ if (deleted > 0) {
301
+ this.scheduleSave();
302
+ }
303
+
304
+ return deleted;
305
+ }
306
+
307
+ /**
308
+ * 清空所有缓存
309
+ */
310
+ clear(): void {
311
+ this.store.entries = {};
312
+ this.store.scenarios.clear();
313
+ this.store.stats = this.createEmptyStore().stats;
314
+ this.scheduleSave();
315
+ }
316
+
317
+ // -------------------------------------------------------------------------
318
+ // 统计更新
319
+ // -------------------------------------------------------------------------
320
+
321
+ private updateHitRate(): void {
322
+ const total = this.store.stats.totalHits + this.store.stats.totalMisses;
323
+ this.store.stats.hitRate =
324
+ total > 0 ? (this.store.stats.totalHits / total) * 100 : 0;
325
+ }
326
+
327
+ /**
328
+ * 更新执行统计
329
+ */
330
+ updateExecutionStats(
331
+ key: string,
332
+ results: Array<{ success: boolean; duration: number }>
333
+ ): void {
334
+ const entry = this.store.entries[key];
335
+ if (!entry) return;
336
+
337
+ const successCount = results.filter((r) => r.success).length;
338
+ const failCount = results.length - successCount;
339
+ const avgDuration =
340
+ results.reduce((sum, r) => sum + r.duration, 0) / results.length;
341
+
342
+ // 更新统计(加权平均)
343
+ const oldCount = entry.successCount + entry.failCount;
344
+ const newCount = oldCount + results.length;
345
+
346
+ entry.successCount += successCount;
347
+ entry.failCount += failCount;
348
+ entry.avgExecutionTime =
349
+ (entry.avgExecutionTime * oldCount + avgDuration * results.length) /
350
+ newCount;
351
+
352
+ // 估算节省的 token 和时间
353
+ this.store.stats.savedTokens += 5000; // 估算值
354
+ this.store.stats.savedTime += 2000; // 估算值(毫秒)
355
+
356
+ this.scheduleSave();
357
+ }
358
+
359
+ // -------------------------------------------------------------------------
360
+ // 场景匹配管理
361
+ // -------------------------------------------------------------------------
362
+
363
+ /**
364
+ * 保存场景匹配
365
+ */
366
+ saveScenarioMatch(match: ScenarioMatch): void {
367
+ this.store.scenarios.set(match.scenario, match);
368
+ this.store.stats.scenarioMatches++;
369
+ this.scheduleSave();
370
+ }
371
+
372
+ /**
373
+ * 获取场景匹配
374
+ */
375
+ getScenarioMatch(scenario: string): ScenarioMatch | undefined {
376
+ return this.store.scenarios.get(scenario);
377
+ }
378
+
379
+ // -------------------------------------------------------------------------
380
+ // 查询方法
381
+ // -------------------------------------------------------------------------
382
+
383
+ /**
384
+ * 获取统计信息
385
+ */
386
+ getStats(): CacheStats {
387
+ return { ...this.store.stats };
388
+ }
389
+
390
+ /**
391
+ * 列出所有缓存条目
392
+ */
393
+ listEntries(): PageActionCacheEntry[] {
394
+ return Object.values(this.store.entries).sort(
395
+ (a, b) => b.lastAccessTime - a.lastAccessTime
396
+ );
397
+ }
398
+
399
+ /**
400
+ * 获取热门场景
401
+ */
402
+ getTopScenarios(limit: number = 5): string {
403
+ const scenarioCounts = new Map<string, number>();
404
+
405
+ for (const entry of Object.values(this.store.entries)) {
406
+ const count = scenarioCounts.get(entry.scenario) || 0;
407
+ scenarioCounts.set(entry.scenario, count + entry.accessCount);
408
+ }
409
+
410
+ const sorted = Array.from(scenarioCounts.entries())
411
+ .sort((a, b) => b[1] - a[1])
412
+ .slice(0, limit);
413
+
414
+ return sorted
415
+ .map(([scenario, count]) => `- ${scenario}: ${count} 次`)
416
+ .join("\n");
417
+ }
418
+
419
+ // -------------------------------------------------------------------------
420
+ // 清理
421
+ // -------------------------------------------------------------------------
422
+
423
+ /**
424
+ * 清理过期缓存
425
+ */
426
+ cleanupExpired(): number {
427
+ const now = Date.now();
428
+ let cleaned = 0;
429
+
430
+ for (const [key, entry] of Object.entries(this.store.entries)) {
431
+ if (now > entry.expiresAt) {
432
+ delete this.store.entries[key];
433
+ this.store.stats.totalEntries--;
434
+ cleaned++;
435
+ }
436
+ }
437
+
438
+ if (cleaned > 0) {
439
+ this.scheduleSave();
440
+ }
441
+
442
+ return cleaned;
443
+ }
444
+ }
445
+
446
+ // ============================================================================
447
+ // 单例
448
+ // ============================================================================
449
+
450
+ let cacheStoreInstance: CacheStore | null = null;
451
+
452
+ export function getCacheStore(): CacheStore {
453
+ if (!cacheStoreInstance) {
454
+ cacheStoreInstance = new CacheStore();
455
+ }
456
+ return cacheStoreInstance;
457
+ }