openclaw-multi-auto 1.4.5 → 1.4.7

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 (100) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  3. package/dist/plugin-sdk/mattermost.js +3 -3
  4. package/dist/plugin-sdk/signal.js +2 -2
  5. package/docs/browser-architecture.md +602 -0
  6. package/extensions/googlechat/node_modules/.bin/openclaw +2 -2
  7. package/extensions/memory-core/node_modules/.bin/openclaw +2 -2
  8. package/extensions/memory-lancedb/node_modules/.bin/openai +2 -2
  9. package/extensions/page-action-cache/dist/actions-executor.d.ts +62 -0
  10. package/extensions/page-action-cache/dist/actions-executor.d.ts.map +1 -0
  11. package/extensions/page-action-cache/dist/actions-executor.js +339 -0
  12. package/extensions/page-action-cache/dist/actions-executor.js.map +1 -0
  13. package/extensions/page-action-cache/dist/cache-invalidator.d.ts +70 -0
  14. package/extensions/page-action-cache/dist/cache-invalidator.d.ts.map +1 -0
  15. package/extensions/page-action-cache/dist/cache-invalidator.js +212 -0
  16. package/extensions/page-action-cache/dist/cache-invalidator.js.map +1 -0
  17. package/extensions/page-action-cache/dist/cache-store.d.ts +80 -0
  18. package/extensions/page-action-cache/dist/cache-store.d.ts.map +1 -0
  19. package/extensions/page-action-cache/dist/cache-store.js +361 -0
  20. package/extensions/page-action-cache/dist/cache-store.js.map +1 -0
  21. package/extensions/page-action-cache/dist/cache-strategy.d.ts +65 -0
  22. package/extensions/page-action-cache/dist/cache-strategy.d.ts.map +1 -0
  23. package/extensions/page-action-cache/dist/cache-strategy.js +237 -0
  24. package/extensions/page-action-cache/dist/cache-strategy.js.map +1 -0
  25. package/extensions/page-action-cache/dist/hooks-entry.d.ts +18 -0
  26. package/extensions/page-action-cache/dist/hooks-entry.d.ts.map +1 -0
  27. package/extensions/page-action-cache/dist/hooks-entry.js +27 -0
  28. package/extensions/page-action-cache/dist/hooks-entry.js.map +1 -0
  29. package/extensions/page-action-cache/dist/hooks.d.ts +10 -0
  30. package/extensions/page-action-cache/dist/hooks.d.ts.map +1 -0
  31. package/extensions/page-action-cache/dist/hooks.js +277 -0
  32. package/extensions/page-action-cache/dist/hooks.js.map +1 -0
  33. package/extensions/page-action-cache/dist/index.d.ts +24 -0
  34. package/extensions/page-action-cache/dist/index.d.ts.map +1 -0
  35. package/extensions/page-action-cache/dist/index.js +34 -0
  36. package/extensions/page-action-cache/dist/index.js.map +1 -0
  37. package/extensions/page-action-cache/dist/scenario-recognizer.d.ts +45 -0
  38. package/extensions/page-action-cache/dist/scenario-recognizer.d.ts.map +1 -0
  39. package/extensions/page-action-cache/dist/scenario-recognizer.js +213 -0
  40. package/extensions/page-action-cache/dist/scenario-recognizer.js.map +1 -0
  41. package/extensions/page-action-cache/dist/security-policy.d.ts +62 -0
  42. package/extensions/page-action-cache/dist/security-policy.d.ts.map +1 -0
  43. package/extensions/page-action-cache/dist/security-policy.js +219 -0
  44. package/extensions/page-action-cache/dist/security-policy.js.map +1 -0
  45. package/extensions/page-action-cache/dist/tools.d.ts +209 -0
  46. package/extensions/page-action-cache/dist/tools.d.ts.map +1 -0
  47. package/extensions/page-action-cache/dist/tools.js +383 -0
  48. package/extensions/page-action-cache/dist/tools.js.map +1 -0
  49. package/extensions/page-action-cache/dist/types.d.ts +336 -0
  50. package/extensions/page-action-cache/dist/types.d.ts.map +1 -0
  51. package/extensions/page-action-cache/dist/types.js +8 -0
  52. package/extensions/page-action-cache/dist/types.js.map +1 -0
  53. package/extensions/page-action-cache/dist/ux-enhancer.d.ts +60 -0
  54. package/extensions/page-action-cache/dist/ux-enhancer.d.ts.map +1 -0
  55. package/extensions/page-action-cache/dist/ux-enhancer.js +218 -0
  56. package/extensions/page-action-cache/dist/ux-enhancer.js.map +1 -0
  57. package/extensions/page-action-cache/dist/variable-resolver.d.ts +28 -0
  58. package/extensions/page-action-cache/dist/variable-resolver.d.ts.map +1 -0
  59. package/extensions/page-action-cache/dist/variable-resolver.js +201 -0
  60. package/extensions/page-action-cache/dist/variable-resolver.js.map +1 -0
  61. package/extensions/page-action-cache/docs/API.md +555 -0
  62. package/extensions/page-action-cache/docs/IMPLEMENTATION.md +1792 -0
  63. package/extensions/page-action-cache/docs/INTEGRATION.md +387 -0
  64. package/extensions/page-action-cache/docs/README.md +183 -0
  65. package/extensions/page-action-cache/index.ts +118 -0
  66. package/extensions/page-action-cache/node_modules/.bin/nlc +21 -0
  67. package/extensions/page-action-cache/node_modules/.bin/node-llama-cpp +21 -0
  68. package/extensions/page-action-cache/node_modules/.bin/openclaw +21 -0
  69. package/extensions/page-action-cache/node_modules/.bin/tsc +21 -0
  70. package/extensions/page-action-cache/node_modules/.bin/tsserver +21 -0
  71. package/extensions/page-action-cache/node_modules/.bin/vitest +21 -0
  72. package/extensions/page-action-cache/openclaw.plugin.json +208 -0
  73. package/extensions/page-action-cache/package.json +76 -0
  74. package/extensions/page-action-cache/scripts/npm_publish.sh +80 -0
  75. package/extensions/page-action-cache/skills/page-action-cache/SKILL.md +216 -0
  76. package/extensions/page-action-cache/src/actions-executor.ts +441 -0
  77. package/extensions/page-action-cache/src/cache-invalidator.ts +271 -0
  78. package/extensions/page-action-cache/src/cache-store.ts +457 -0
  79. package/extensions/page-action-cache/src/cache-strategy.ts +327 -0
  80. package/extensions/page-action-cache/src/hooks-entry.ts +114 -0
  81. package/extensions/page-action-cache/src/hooks.ts +332 -0
  82. package/extensions/page-action-cache/src/index.ts +104 -0
  83. package/extensions/page-action-cache/src/scenario-recognizer.ts +259 -0
  84. package/extensions/page-action-cache/src/security-policy.ts +268 -0
  85. package/extensions/page-action-cache/src/tools.ts +437 -0
  86. package/extensions/page-action-cache/src/types.ts +482 -0
  87. package/extensions/page-action-cache/src/ux-enhancer.ts +266 -0
  88. package/extensions/page-action-cache/src/variable-resolver.ts +258 -0
  89. package/extensions/page-action-cache/tests/actions-executor.test.ts +424 -0
  90. package/extensions/page-action-cache/tests/cache-store.test.ts +267 -0
  91. package/extensions/page-action-cache/tests/integration-test.ts +62 -0
  92. package/extensions/page-action-cache/tests/scenario-recognizer.test.ts +140 -0
  93. package/extensions/page-action-cache/tests/variable-resolver.test.ts +187 -0
  94. package/extensions/page-action-cache/tsconfig.json +39 -0
  95. package/package.json +1 -1
  96. package/scripts/create-instance.sh +26 -8
  97. package/scripts/npm_publish.sh +59 -1
  98. package/scripts/publish-extension.sh +343 -0
  99. package/ui/node_modules/.bin/vite +2 -2
  100. package/ui/node_modules/.bin/vitest +2 -2
@@ -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
+ }
@@ -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
+ }