agent-browser-stealth 0.17.0-fork.2 → 0.24.0-fork.2
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/README.md +1256 -240
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser-darwin-x64 +0 -0
- package/bin/agent-browser-linux-arm64 +0 -0
- package/bin/agent-browser-linux-x64 +0 -0
- package/bin/agent-browser-win32-x64.exe +0 -0
- package/bin/agent-browser.js +13 -2
- package/extensions/tab-group-cdp/content-script.js +425 -0
- package/extensions/tab-group-cdp/icons/icon.svg +7 -0
- package/extensions/tab-group-cdp/manifest.json +34 -0
- package/extensions/tab-group-cdp/page-bridge.js +133 -0
- package/extensions/tab-group-cdp/service-worker.js +2249 -0
- package/extensions/tab-group-cdp/sidepanel.css +258 -0
- package/extensions/tab-group-cdp/sidepanel.html +28 -0
- package/extensions/tab-group-cdp/sidepanel.js +1225 -0
- package/package.json +17 -69
- package/scripts/build-all-platforms.sh +6 -0
- package/scripts/check-version-sync.js +14 -2
- package/scripts/copy-native.js +8 -50
- package/scripts/postinstall.js +149 -165
- package/scripts/windows-debug/provision.sh +220 -0
- package/scripts/windows-debug/run.sh +92 -0
- package/scripts/windows-debug/start.sh +43 -0
- package/scripts/windows-debug/stop.sh +28 -0
- package/scripts/windows-debug/sync.sh +27 -0
- package/skills/agent-browser/SKILL.md +256 -159
- package/skills/agent-browser/references/authentication.md +101 -0
- package/skills/agent-browser/references/commands.md +34 -2
- package/skills/agent-browser/references/snapshot-refs.md +25 -0
- package/skills/agentcore/SKILL.md +115 -0
- package/skills/dogfood/SKILL.md +4 -2
- package/skills/electron/SKILL.md +26 -2
- package/skills/slack/SKILL.md +0 -9
- package/skills/slack/references/slack-tasks.md +2 -8
- package/skills/vercel-sandbox/SKILL.md +280 -0
- package/bin/agent-browser-local +0 -0
- package/bin/agent-browser-stealth +0 -0
- package/bin/agent-browser-stealth.d +0 -1
- package/dist/action-policy.d.ts +0 -14
- package/dist/action-policy.d.ts.map +0 -1
- package/dist/action-policy.js +0 -253
- package/dist/action-policy.js.map +0 -1
- package/dist/actions.d.ts +0 -21
- package/dist/actions.d.ts.map +0 -1
- package/dist/actions.js +0 -2139
- package/dist/actions.js.map +0 -1
- package/dist/auth-cli.d.ts +0 -2
- package/dist/auth-cli.d.ts.map +0 -1
- package/dist/auth-cli.js +0 -97
- package/dist/auth-cli.js.map +0 -1
- package/dist/auth-vault.d.ts +0 -36
- package/dist/auth-vault.d.ts.map +0 -1
- package/dist/auth-vault.js +0 -125
- package/dist/auth-vault.js.map +0 -1
- package/dist/browser.d.ts +0 -665
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js +0 -3210
- package/dist/browser.js.map +0 -1
- package/dist/confirmation.d.ts +0 -8
- package/dist/confirmation.d.ts.map +0 -1
- package/dist/confirmation.js +0 -30
- package/dist/confirmation.js.map +0 -1
- package/dist/daemon.d.ts +0 -78
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -744
- package/dist/daemon.js.map +0 -1
- package/dist/diff.d.ts +0 -18
- package/dist/diff.d.ts.map +0 -1
- package/dist/diff.js +0 -271
- package/dist/diff.js.map +0 -1
- package/dist/domain-filter.d.ts +0 -28
- package/dist/domain-filter.d.ts.map +0 -1
- package/dist/domain-filter.js +0 -149
- package/dist/domain-filter.js.map +0 -1
- package/dist/encryption.d.ts +0 -73
- package/dist/encryption.d.ts.map +0 -1
- package/dist/encryption.js +0 -171
- package/dist/encryption.js.map +0 -1
- package/dist/ios-actions.d.ts +0 -11
- package/dist/ios-actions.d.ts.map +0 -1
- package/dist/ios-actions.js +0 -228
- package/dist/ios-actions.js.map +0 -1
- package/dist/ios-manager.d.ts +0 -266
- package/dist/ios-manager.d.ts.map +0 -1
- package/dist/ios-manager.js +0 -1073
- package/dist/ios-manager.js.map +0 -1
- package/dist/protocol.d.ts +0 -26
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -990
- package/dist/protocol.js.map +0 -1
- package/dist/snapshot.d.ts +0 -67
- package/dist/snapshot.d.ts.map +0 -1
- package/dist/snapshot.js +0 -514
- package/dist/snapshot.js.map +0 -1
- package/dist/state-utils.d.ts +0 -77
- package/dist/state-utils.d.ts.map +0 -1
- package/dist/state-utils.js +0 -178
- package/dist/state-utils.js.map +0 -1
- package/dist/stealth.d.ts +0 -41
- package/dist/stealth.d.ts.map +0 -1
- package/dist/stealth.js +0 -1743
- package/dist/stealth.js.map +0 -1
- package/dist/stream-server.d.ts +0 -117
- package/dist/stream-server.d.ts.map +0 -1
- package/dist/stream-server.js +0 -309
- package/dist/stream-server.js.map +0 -1
- package/dist/types.d.ts +0 -973
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/scripts/check-creepjs-headless.js +0 -137
- package/scripts/check-daemon-pid-recovery.js +0 -148
- package/scripts/check-sannysoft-webdriver.js +0 -112
- package/scripts/check-stealth-regression.js +0 -199
- package/scripts/check-turnstile-testkey.ts +0 -125
- package/scripts/clawhub-sync.sh +0 -27
- package/scripts/sync-upstream.sh +0 -142
- package/scripts/verify-bundled-binaries.js +0 -71
- package/scripts/verify-native-version.js +0 -48
- package/scripts/verify-packed-host-binary.js +0 -88
- package/scripts/verify-registry-host-binary.js +0 -120
- package/skills/agent-browser-stealth/SKILL.md +0 -127
|
@@ -0,0 +1,2249 @@
|
|
|
1
|
+
const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST';
|
|
2
|
+
const PANEL_GET_STATE = 'AB_PANEL_GET_STATE';
|
|
3
|
+
const PANEL_CLOSE_OTHER_TABS = 'AB_PANEL_CLOSE_OTHER_SESSION_TABS';
|
|
4
|
+
const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION';
|
|
5
|
+
const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS';
|
|
6
|
+
const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY';
|
|
7
|
+
const PANEL_SET_OPTIONS = 'AB_PANEL_SET_OPTIONS';
|
|
8
|
+
|
|
9
|
+
const PANEL_RUN_ACTION = 'AB_PANEL_RUN_ACTION';
|
|
10
|
+
const PANEL_CLEAR_ACTIVITY = 'AB_PANEL_CLEAR_ACTIVITY';
|
|
11
|
+
const PANEL_START_RECORDING = 'AB_PANEL_START_RECORDING';
|
|
12
|
+
const PANEL_STOP_RECORDING = 'AB_PANEL_STOP_RECORDING';
|
|
13
|
+
const PANEL_SAVE_RECORDING = 'AB_PANEL_SAVE_RECORDING';
|
|
14
|
+
const PANEL_RUN_WORKFLOW = 'AB_PANEL_RUN_WORKFLOW';
|
|
15
|
+
const PANEL_DELETE_WORKFLOW = 'AB_PANEL_DELETE_WORKFLOW';
|
|
16
|
+
const PANEL_SET_SHORTCUT = 'AB_PANEL_SET_SHORTCUT';
|
|
17
|
+
const PANEL_DELETE_SHORTCUT = 'AB_PANEL_DELETE_SHORTCUT';
|
|
18
|
+
const PANEL_RUN_SHORTCUT = 'AB_PANEL_RUN_SHORTCUT';
|
|
19
|
+
const PANEL_CREATE_SCHEDULE = 'AB_PANEL_CREATE_SCHEDULE';
|
|
20
|
+
const PANEL_DELETE_SCHEDULE = 'AB_PANEL_DELETE_SCHEDULE';
|
|
21
|
+
const PANEL_TOGGLE_SCHEDULE = 'AB_PANEL_TOGGLE_SCHEDULE';
|
|
22
|
+
|
|
23
|
+
const CONTENT_EVENT_TYPE = 'AB_CONTENT_EVENT';
|
|
24
|
+
const CONTENT_EXECUTE_ACTION = 'AB_CONTENT_EXECUTE_ACTION';
|
|
25
|
+
const CONTENT_GET_DOM_STATE = 'AB_CONTENT_GET_DOM_STATE';
|
|
26
|
+
const CONTENT_PING = 'AB_CONTENT_PING';
|
|
27
|
+
|
|
28
|
+
const DEFAULT_GROUP_TITLE = 'Agent Browser Stealth';
|
|
29
|
+
const DOWNLOAD_ARCHIVE_ROOT = 'agent-browser-stealth';
|
|
30
|
+
const STORAGE_POLICY_KEY = 'abSessionPoliciesV1';
|
|
31
|
+
const STORAGE_OPTIONS_KEY = 'abExtensionOptionsV1';
|
|
32
|
+
const STORAGE_WORKFLOWS_KEY = 'abWorkflowsV1';
|
|
33
|
+
const STORAGE_SHORTCUTS_KEY = 'abShortcutsV1';
|
|
34
|
+
const STORAGE_SCHEDULES_KEY = 'abSchedulesV1';
|
|
35
|
+
|
|
36
|
+
const CLEANUP_ALARM_NAME = 'ab-clean-empty-groups';
|
|
37
|
+
const WORKFLOW_ALARM_PREFIX = 'ab-workflow-schedule:';
|
|
38
|
+
|
|
39
|
+
const GROUP_COLORS = ['blue', 'green', 'pink', 'orange', 'purple', 'cyan', 'red', 'yellow'];
|
|
40
|
+
const RISKY_TLDS = new Set(['zip', 'mov', 'click', 'top', 'gq', 'tk', 'country']);
|
|
41
|
+
const RISKY_HOST_KEYWORDS = ['secure-login', 'account-verify', 'wallet-verify', 'airdrop-claim'];
|
|
42
|
+
|
|
43
|
+
const DEFAULT_EXTENSION_OPTIONS = {
|
|
44
|
+
strictWindowIsolation: true,
|
|
45
|
+
suppressCrossWindowActivation: true,
|
|
46
|
+
autoCleanEmptyGroups: true,
|
|
47
|
+
pageBridgeEnabled: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MAX_ACTIVITY_EVENTS = 500;
|
|
51
|
+
const COMMAND_HISTORY_LIMIT = 120;
|
|
52
|
+
|
|
53
|
+
const sessionGroupCache = new Map();
|
|
54
|
+
const sessionGroupTitleMap = new Map();
|
|
55
|
+
const sessionWindowMap = new Map();
|
|
56
|
+
const tabSessionMap = new Map();
|
|
57
|
+
const tabMetaById = new Map();
|
|
58
|
+
const downloadEvents = [];
|
|
59
|
+
const sessionPolicies = new Map();
|
|
60
|
+
const workflows = new Map();
|
|
61
|
+
const shortcuts = new Map();
|
|
62
|
+
const schedules = new Map();
|
|
63
|
+
|
|
64
|
+
const activityEvents = [];
|
|
65
|
+
const commandHistory = [];
|
|
66
|
+
let latestDomState = null;
|
|
67
|
+
|
|
68
|
+
let extensionOptions = { ...DEFAULT_EXTENSION_OPTIONS };
|
|
69
|
+
// Transient workflow recorder state. Persisted only when saved as a workflow.
|
|
70
|
+
let recordingState = null;
|
|
71
|
+
let bootstrapPromise = bootstrapState();
|
|
72
|
+
let eventCounter = 0;
|
|
73
|
+
|
|
74
|
+
function now() {
|
|
75
|
+
return Date.now();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function uid(prefix) {
|
|
79
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
80
|
+
return `${prefix}-${Date.now().toString(36)}-${rand}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clampInt(value, min, max, fallback) {
|
|
84
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
85
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
86
|
+
if (parsed < min) return min;
|
|
87
|
+
if (parsed > max) return max;
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeSession(session) {
|
|
92
|
+
if (typeof session !== 'string') return 'default';
|
|
93
|
+
const trimmed = session.trim();
|
|
94
|
+
return trimmed.length > 0 ? trimmed.slice(0, 64) : 'default';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeGroupTitle(title) {
|
|
98
|
+
if (typeof title !== 'string') return DEFAULT_GROUP_TITLE;
|
|
99
|
+
const trimmed = title.trim();
|
|
100
|
+
return trimmed.length > 0 ? trimmed.slice(0, 80) : DEFAULT_GROUP_TITLE;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeAllowedDomains(domains) {
|
|
104
|
+
if (!Array.isArray(domains)) return [];
|
|
105
|
+
return domains
|
|
106
|
+
.map((item) => (typeof item === 'string' ? item.trim().toLowerCase() : ''))
|
|
107
|
+
.filter((item) => item.length > 0)
|
|
108
|
+
.slice(0, 256);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeUrl(rawUrl) {
|
|
112
|
+
if (typeof rawUrl !== 'string') return null;
|
|
113
|
+
const trimmed = rawUrl.trim();
|
|
114
|
+
if (!trimmed) return null;
|
|
115
|
+
|
|
116
|
+
if (/^(https?|file|about|data|blob|chrome-extension):/i.test(trimmed)) {
|
|
117
|
+
return trimmed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `https://${trimmed}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeShortcutName(name) {
|
|
124
|
+
if (typeof name !== 'string') return null;
|
|
125
|
+
const trimmed = name.trim().toLowerCase();
|
|
126
|
+
if (!trimmed) return null;
|
|
127
|
+
const normalized = trimmed.replace(/[^a-z0-9:_-]/g, '-').replace(/-+/g, '-').slice(0, 48);
|
|
128
|
+
return normalized || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseHostname(rawUrl) {
|
|
132
|
+
if (typeof rawUrl !== 'string' || rawUrl.length === 0) return null;
|
|
133
|
+
try {
|
|
134
|
+
const parsed = new URL(rawUrl);
|
|
135
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
|
|
136
|
+
return parsed.hostname.toLowerCase();
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function domainMatches(hostname, pattern) {
|
|
143
|
+
if (!hostname || !pattern) return false;
|
|
144
|
+
if (pattern.startsWith('*.')) {
|
|
145
|
+
const suffix = pattern.slice(2);
|
|
146
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
147
|
+
}
|
|
148
|
+
if (pattern.startsWith('.')) {
|
|
149
|
+
const suffix = pattern.slice(1);
|
|
150
|
+
return hostname === suffix || hostname.endsWith(pattern);
|
|
151
|
+
}
|
|
152
|
+
return hostname === pattern || hostname.endsWith(`.${pattern}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isDomainAllowed(hostname, patterns) {
|
|
156
|
+
if (!hostname) return true;
|
|
157
|
+
if (!patterns || patterns.length === 0) return true;
|
|
158
|
+
return patterns.some((pattern) => domainMatches(hostname, pattern));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectRiskHints(rawUrl, allowedDomains) {
|
|
162
|
+
const hints = [];
|
|
163
|
+
const hostname = parseHostname(rawUrl);
|
|
164
|
+
if (!hostname) return hints;
|
|
165
|
+
|
|
166
|
+
if (allowedDomains.length > 0 && !isDomainAllowed(hostname, allowedDomains)) {
|
|
167
|
+
hints.push(`domain-not-allowed:${hostname}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const tld = hostname.split('.').pop();
|
|
171
|
+
if (tld && RISKY_TLDS.has(tld)) {
|
|
172
|
+
hints.push(`high-risk-tld:.${tld}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const keyword of RISKY_HOST_KEYWORDS) {
|
|
176
|
+
if (hostname.includes(keyword)) {
|
|
177
|
+
hints.push(`suspicious-host-keyword:${keyword}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [...new Set(hints)].slice(0, 10);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cacheKey(windowId, session) {
|
|
185
|
+
return `${windowId}:${session}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sanitizeSegment(input, fallback = 'default') {
|
|
189
|
+
const raw = typeof input === 'string' ? input : '';
|
|
190
|
+
const cleaned = raw
|
|
191
|
+
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-')
|
|
192
|
+
.replace(/\s+/g, '_')
|
|
193
|
+
.replace(/\.+/g, '.')
|
|
194
|
+
.trim();
|
|
195
|
+
if (!cleaned) return fallback;
|
|
196
|
+
return cleaned.slice(0, 80);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sanitizeFilename(filename, fallback = 'download.bin') {
|
|
200
|
+
const name = typeof filename === 'string' ? filename.split('/').pop() : '';
|
|
201
|
+
return sanitizeSegment(name, fallback);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function pickColorForSession(session) {
|
|
205
|
+
let hash = 0;
|
|
206
|
+
for (let i = 0; i < session.length; i += 1) {
|
|
207
|
+
hash = (hash * 31 + session.charCodeAt(i)) >>> 0;
|
|
208
|
+
}
|
|
209
|
+
return GROUP_COLORS[hash % GROUP_COLORS.length];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function shouldCollapseGroup(session) {
|
|
213
|
+
return session !== 'default';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function defaultGroupTitleForSession(session) {
|
|
217
|
+
const normalized = normalizeSession(session);
|
|
218
|
+
if (normalized === 'default') {
|
|
219
|
+
return DEFAULT_GROUP_TITLE;
|
|
220
|
+
}
|
|
221
|
+
return normalizeGroupTitle(`${DEFAULT_GROUP_TITLE} • ${normalized}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getGroupTitleForSession(session) {
|
|
225
|
+
const normalized = normalizeSession(session);
|
|
226
|
+
return sessionGroupTitleMap.get(normalized) || defaultGroupTitleForSession(normalized);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function createActivityEvent(kind, payload, meta = {}) {
|
|
230
|
+
return {
|
|
231
|
+
id: ++eventCounter,
|
|
232
|
+
kind,
|
|
233
|
+
payload,
|
|
234
|
+
tabId: typeof meta.tabId === 'number' ? meta.tabId : null,
|
|
235
|
+
session: typeof meta.session === 'string' ? meta.session : null,
|
|
236
|
+
url: typeof meta.url === 'string' ? meta.url : '',
|
|
237
|
+
title: typeof meta.title === 'string' ? meta.title : '',
|
|
238
|
+
source: typeof meta.source === 'string' ? meta.source : 'extension',
|
|
239
|
+
timestamp: now(),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function pushActivityEvent(kind, payload, meta = {}) {
|
|
244
|
+
activityEvents.push(createActivityEvent(kind, payload, meta));
|
|
245
|
+
if (activityEvents.length > MAX_ACTIVITY_EVENTS) {
|
|
246
|
+
activityEvents.splice(0, activityEvents.length - MAX_ACTIVITY_EVENTS);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function pushCommandHistory(entry) {
|
|
251
|
+
commandHistory.push({
|
|
252
|
+
...entry,
|
|
253
|
+
timestamp: now(),
|
|
254
|
+
});
|
|
255
|
+
if (commandHistory.length > COMMAND_HISTORY_LIMIT) {
|
|
256
|
+
commandHistory.splice(0, commandHistory.length - COMMAND_HISTORY_LIMIT);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function loadPolicies() {
|
|
261
|
+
try {
|
|
262
|
+
const result = await chrome.storage.local.get([STORAGE_POLICY_KEY]);
|
|
263
|
+
const entries = result?.[STORAGE_POLICY_KEY];
|
|
264
|
+
if (!entries || typeof entries !== 'object') return;
|
|
265
|
+
|
|
266
|
+
for (const [session, domains] of Object.entries(entries)) {
|
|
267
|
+
const normalizedSession = normalizeSession(session);
|
|
268
|
+
sessionPolicies.set(normalizedSession, normalizeAllowedDomains(domains));
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Ignore storage load failures.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function normalizeOptions(raw) {
|
|
276
|
+
if (!raw || typeof raw !== 'object') {
|
|
277
|
+
return { ...DEFAULT_EXTENSION_OPTIONS };
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
strictWindowIsolation: raw.strictWindowIsolation !== false,
|
|
281
|
+
suppressCrossWindowActivation: raw.suppressCrossWindowActivation !== false,
|
|
282
|
+
autoCleanEmptyGroups: raw.autoCleanEmptyGroups !== false,
|
|
283
|
+
pageBridgeEnabled: raw.pageBridgeEnabled === true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function loadOptions() {
|
|
288
|
+
try {
|
|
289
|
+
const result = await chrome.storage.local.get([STORAGE_OPTIONS_KEY]);
|
|
290
|
+
extensionOptions = normalizeOptions(result?.[STORAGE_OPTIONS_KEY]);
|
|
291
|
+
} catch {
|
|
292
|
+
extensionOptions = { ...DEFAULT_EXTENSION_OPTIONS };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function persistOptions() {
|
|
297
|
+
await chrome.storage.local.set({ [STORAGE_OPTIONS_KEY]: extensionOptions });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function setExtensionOptions(nextOptions) {
|
|
301
|
+
const merged = {
|
|
302
|
+
...extensionOptions,
|
|
303
|
+
...(nextOptions && typeof nextOptions === 'object' ? nextOptions : {}),
|
|
304
|
+
};
|
|
305
|
+
extensionOptions = normalizeOptions(merged);
|
|
306
|
+
await persistOptions();
|
|
307
|
+
await syncCleanupAlarm();
|
|
308
|
+
return extensionOptions;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function normalizeWorkflowStep(rawStep) {
|
|
312
|
+
if (!rawStep || typeof rawStep !== 'object') return null;
|
|
313
|
+
if (typeof rawStep.action !== 'string' || rawStep.action.trim().length === 0) return null;
|
|
314
|
+
|
|
315
|
+
const action = rawStep.action.trim();
|
|
316
|
+
return {
|
|
317
|
+
id: typeof rawStep.id === 'string' ? rawStep.id : uid('step'),
|
|
318
|
+
action,
|
|
319
|
+
args: rawStep.args && typeof rawStep.args === 'object' ? rawStep.args : {},
|
|
320
|
+
timeoutMs: clampInt(rawStep.timeoutMs, 0, 120_000, 0),
|
|
321
|
+
retries: clampInt(rawStep.retries, 0, 5, 0),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeWorkflow(rawWorkflow) {
|
|
326
|
+
if (!rawWorkflow || typeof rawWorkflow !== 'object') return null;
|
|
327
|
+
if (typeof rawWorkflow.id !== 'string' || rawWorkflow.id.length === 0) return null;
|
|
328
|
+
|
|
329
|
+
const steps = Array.isArray(rawWorkflow.steps)
|
|
330
|
+
? rawWorkflow.steps.map((step) => normalizeWorkflowStep(step)).filter(Boolean)
|
|
331
|
+
: [];
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
id: rawWorkflow.id,
|
|
335
|
+
name:
|
|
336
|
+
typeof rawWorkflow.name === 'string' && rawWorkflow.name.trim().length > 0
|
|
337
|
+
? rawWorkflow.name.trim().slice(0, 120)
|
|
338
|
+
: rawWorkflow.id,
|
|
339
|
+
steps,
|
|
340
|
+
createdAt: clampInt(rawWorkflow.createdAt, 0, Number.MAX_SAFE_INTEGER, now()),
|
|
341
|
+
updatedAt: clampInt(rawWorkflow.updatedAt, 0, Number.MAX_SAFE_INTEGER, now()),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizeCadence(rawCadence) {
|
|
346
|
+
if (!rawCadence || typeof rawCadence !== 'object') {
|
|
347
|
+
return {
|
|
348
|
+
kind: 'daily',
|
|
349
|
+
hour: 9,
|
|
350
|
+
minute: 0,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const kind =
|
|
355
|
+
rawCadence.kind === 'weekly' ||
|
|
356
|
+
rawCadence.kind === 'monthly' ||
|
|
357
|
+
rawCadence.kind === 'yearly' ||
|
|
358
|
+
rawCadence.kind === 'daily'
|
|
359
|
+
? rawCadence.kind
|
|
360
|
+
: 'daily';
|
|
361
|
+
|
|
362
|
+
const cadence = {
|
|
363
|
+
kind,
|
|
364
|
+
hour: clampInt(rawCadence.hour, 0, 23, 9),
|
|
365
|
+
minute: clampInt(rawCadence.minute, 0, 59, 0),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
if (kind === 'weekly') {
|
|
369
|
+
const weekdays = Array.isArray(rawCadence.weekdays)
|
|
370
|
+
? rawCadence.weekdays.map((day) => clampInt(day, 0, 6, 1))
|
|
371
|
+
: [clampInt(rawCadence.weekday, 0, 6, 1)];
|
|
372
|
+
cadence.weekdays = [...new Set(weekdays)].slice(0, 7);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (kind === 'monthly') {
|
|
376
|
+
cadence.dayOfMonth = clampInt(rawCadence.dayOfMonth, 1, 31, 1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (kind === 'yearly') {
|
|
380
|
+
cadence.month = clampInt(rawCadence.month, 1, 12, 1);
|
|
381
|
+
cadence.dayOfMonth = clampInt(rawCadence.dayOfMonth, 1, 31, 1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return cadence;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function daysInMonth(year, monthIndex) {
|
|
388
|
+
return new Date(year, monthIndex + 1, 0).getDate();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function computeNextRun(cadence, fromTs = now()) {
|
|
392
|
+
const from = new Date(fromTs + 1000);
|
|
393
|
+
const hour = clampInt(cadence.hour, 0, 23, 9);
|
|
394
|
+
const minute = clampInt(cadence.minute, 0, 59, 0);
|
|
395
|
+
|
|
396
|
+
if (cadence.kind === 'weekly') {
|
|
397
|
+
const weekdays = Array.isArray(cadence.weekdays) && cadence.weekdays.length > 0
|
|
398
|
+
? cadence.weekdays.map((day) => clampInt(day, 0, 6, 1))
|
|
399
|
+
: [1];
|
|
400
|
+
|
|
401
|
+
for (let offset = 0; offset < 14; offset += 1) {
|
|
402
|
+
const candidate = new Date(from);
|
|
403
|
+
candidate.setDate(from.getDate() + offset);
|
|
404
|
+
candidate.setHours(hour, minute, 0, 0);
|
|
405
|
+
if (candidate <= from) continue;
|
|
406
|
+
if (weekdays.includes(candidate.getDay())) {
|
|
407
|
+
return candidate.getTime();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (cadence.kind === 'monthly') {
|
|
413
|
+
const dayOfMonth = clampInt(cadence.dayOfMonth, 1, 31, 1);
|
|
414
|
+
const candidate = new Date(from);
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < 24; i += 1) {
|
|
417
|
+
const monthDate = new Date(candidate.getFullYear(), candidate.getMonth() + i, 1);
|
|
418
|
+
const maxDay = daysInMonth(monthDate.getFullYear(), monthDate.getMonth());
|
|
419
|
+
monthDate.setDate(Math.min(dayOfMonth, maxDay));
|
|
420
|
+
monthDate.setHours(hour, minute, 0, 0);
|
|
421
|
+
if (monthDate > from) {
|
|
422
|
+
return monthDate.getTime();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (cadence.kind === 'yearly') {
|
|
428
|
+
const month = clampInt(cadence.month, 1, 12, 1) - 1;
|
|
429
|
+
const dayOfMonth = clampInt(cadence.dayOfMonth, 1, 31, 1);
|
|
430
|
+
|
|
431
|
+
for (let yearOffset = 0; yearOffset < 5; yearOffset += 1) {
|
|
432
|
+
const year = from.getFullYear() + yearOffset;
|
|
433
|
+
const maxDay = daysInMonth(year, month);
|
|
434
|
+
const candidate = new Date(year, month, Math.min(dayOfMonth, maxDay), hour, minute, 0, 0);
|
|
435
|
+
if (candidate > from) {
|
|
436
|
+
return candidate.getTime();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const dailyCandidate = new Date(from);
|
|
442
|
+
dailyCandidate.setHours(hour, minute, 0, 0);
|
|
443
|
+
if (dailyCandidate <= from) {
|
|
444
|
+
dailyCandidate.setDate(dailyCandidate.getDate() + 1);
|
|
445
|
+
}
|
|
446
|
+
return dailyCandidate.getTime();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeSchedule(rawSchedule) {
|
|
450
|
+
if (!rawSchedule || typeof rawSchedule !== 'object') return null;
|
|
451
|
+
if (typeof rawSchedule.id !== 'string' || rawSchedule.id.length === 0) return null;
|
|
452
|
+
if (typeof rawSchedule.workflowId !== 'string' || rawSchedule.workflowId.length === 0) return null;
|
|
453
|
+
|
|
454
|
+
const cadence = normalizeCadence(rawSchedule.cadence);
|
|
455
|
+
|
|
456
|
+
const nextRunAt =
|
|
457
|
+
typeof rawSchedule.nextRunAt === 'number' && Number.isFinite(rawSchedule.nextRunAt)
|
|
458
|
+
? rawSchedule.nextRunAt
|
|
459
|
+
: computeNextRun(cadence);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
id: rawSchedule.id,
|
|
463
|
+
name:
|
|
464
|
+
typeof rawSchedule.name === 'string' && rawSchedule.name.trim().length > 0
|
|
465
|
+
? rawSchedule.name.trim().slice(0, 120)
|
|
466
|
+
: rawSchedule.id,
|
|
467
|
+
workflowId: rawSchedule.workflowId,
|
|
468
|
+
cadence,
|
|
469
|
+
enabled: rawSchedule.enabled !== false,
|
|
470
|
+
createdAt: clampInt(rawSchedule.createdAt, 0, Number.MAX_SAFE_INTEGER, now()),
|
|
471
|
+
updatedAt: clampInt(rawSchedule.updatedAt, 0, Number.MAX_SAFE_INTEGER, now()),
|
|
472
|
+
lastRunAt:
|
|
473
|
+
typeof rawSchedule.lastRunAt === 'number' && Number.isFinite(rawSchedule.lastRunAt)
|
|
474
|
+
? rawSchedule.lastRunAt
|
|
475
|
+
: null,
|
|
476
|
+
nextRunAt,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function persistWorkflows() {
|
|
481
|
+
const payload = [...workflows.values()];
|
|
482
|
+
await chrome.storage.local.set({ [STORAGE_WORKFLOWS_KEY]: payload });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function persistShortcuts() {
|
|
486
|
+
const payload = Object.fromEntries(shortcuts.entries());
|
|
487
|
+
await chrome.storage.local.set({ [STORAGE_SHORTCUTS_KEY]: payload });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function persistSchedules() {
|
|
491
|
+
const payload = [...schedules.values()];
|
|
492
|
+
await chrome.storage.local.set({ [STORAGE_SCHEDULES_KEY]: payload });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function loadAutomationState() {
|
|
496
|
+
try {
|
|
497
|
+
const result = await chrome.storage.local.get([
|
|
498
|
+
STORAGE_WORKFLOWS_KEY,
|
|
499
|
+
STORAGE_SHORTCUTS_KEY,
|
|
500
|
+
STORAGE_SCHEDULES_KEY,
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
const workflowEntries = Array.isArray(result?.[STORAGE_WORKFLOWS_KEY])
|
|
504
|
+
? result[STORAGE_WORKFLOWS_KEY]
|
|
505
|
+
: [];
|
|
506
|
+
|
|
507
|
+
workflows.clear();
|
|
508
|
+
for (const entry of workflowEntries) {
|
|
509
|
+
const workflow = normalizeWorkflow(entry);
|
|
510
|
+
if (!workflow) continue;
|
|
511
|
+
workflows.set(workflow.id, workflow);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const shortcutEntries =
|
|
515
|
+
result?.[STORAGE_SHORTCUTS_KEY] && typeof result[STORAGE_SHORTCUTS_KEY] === 'object'
|
|
516
|
+
? result[STORAGE_SHORTCUTS_KEY]
|
|
517
|
+
: {};
|
|
518
|
+
|
|
519
|
+
shortcuts.clear();
|
|
520
|
+
for (const [name, workflowId] of Object.entries(shortcutEntries)) {
|
|
521
|
+
const shortcutName = normalizeShortcutName(name);
|
|
522
|
+
if (!shortcutName) continue;
|
|
523
|
+
if (typeof workflowId !== 'string') continue;
|
|
524
|
+
if (!workflows.has(workflowId)) continue;
|
|
525
|
+
shortcuts.set(shortcutName, workflowId);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const scheduleEntries = Array.isArray(result?.[STORAGE_SCHEDULES_KEY])
|
|
529
|
+
? result[STORAGE_SCHEDULES_KEY]
|
|
530
|
+
: [];
|
|
531
|
+
|
|
532
|
+
schedules.clear();
|
|
533
|
+
for (const entry of scheduleEntries) {
|
|
534
|
+
const schedule = normalizeSchedule(entry);
|
|
535
|
+
if (!schedule) continue;
|
|
536
|
+
if (!workflows.has(schedule.workflowId)) continue;
|
|
537
|
+
schedules.set(schedule.id, schedule);
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
workflows.clear();
|
|
541
|
+
shortcuts.clear();
|
|
542
|
+
schedules.clear();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function scheduleWorkflowAlarm(schedule) {
|
|
547
|
+
const alarmName = `${WORKFLOW_ALARM_PREFIX}${schedule.id}`;
|
|
548
|
+
await chrome.alarms.clear(alarmName);
|
|
549
|
+
|
|
550
|
+
if (!schedule.enabled) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (typeof schedule.nextRunAt !== 'number' || !Number.isFinite(schedule.nextRunAt)) {
|
|
555
|
+
schedule.nextRunAt = computeNextRun(schedule.cadence);
|
|
556
|
+
schedule.updatedAt = now();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
await chrome.alarms.create(alarmName, {
|
|
560
|
+
when: Math.max(schedule.nextRunAt, now() + 5_000),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function syncAllWorkflowAlarms() {
|
|
565
|
+
for (const schedule of schedules.values()) {
|
|
566
|
+
await scheduleWorkflowAlarm(schedule);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function bootstrapState() {
|
|
571
|
+
await loadPolicies();
|
|
572
|
+
await loadOptions();
|
|
573
|
+
await loadAutomationState();
|
|
574
|
+
await syncCleanupAlarm();
|
|
575
|
+
await syncAllWorkflowAlarms();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function persistPolicies() {
|
|
579
|
+
const serialized = {};
|
|
580
|
+
for (const [session, domains] of sessionPolicies.entries()) {
|
|
581
|
+
serialized[session] = [...domains];
|
|
582
|
+
}
|
|
583
|
+
await chrome.storage.local.set({ [STORAGE_POLICY_KEY]: serialized });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function setSessionPolicy(session, allowedDomains) {
|
|
587
|
+
const normalizedSession = normalizeSession(session);
|
|
588
|
+
const normalizedDomains = normalizeAllowedDomains(allowedDomains);
|
|
589
|
+
sessionPolicies.set(normalizedSession, normalizedDomains);
|
|
590
|
+
await persistPolicies();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function getSessionPolicy(session) {
|
|
594
|
+
const normalizedSession = normalizeSession(session);
|
|
595
|
+
return sessionPolicies.get(normalizedSession) ?? [];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function updateTabMeta(tab) {
|
|
599
|
+
if (!tab || typeof tab.id !== 'number') return;
|
|
600
|
+
tabMetaById.set(tab.id, {
|
|
601
|
+
id: tab.id,
|
|
602
|
+
windowId: typeof tab.windowId === 'number' ? tab.windowId : -1,
|
|
603
|
+
url: typeof tab.url === 'string' ? tab.url : '',
|
|
604
|
+
title: typeof tab.title === 'string' ? tab.title : '',
|
|
605
|
+
groupId: typeof tab.groupId === 'number' ? tab.groupId : -1,
|
|
606
|
+
active: tab.active === true,
|
|
607
|
+
lastSeenAt: now(),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function pruneDownloadEvents() {
|
|
612
|
+
const maxSize = 100;
|
|
613
|
+
if (downloadEvents.length > maxSize) {
|
|
614
|
+
downloadEvents.splice(0, downloadEvents.length - maxSize);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function recordDownloadEvent(event) {
|
|
619
|
+
downloadEvents.push({ ...event, timestamp: now() });
|
|
620
|
+
pruneDownloadEvents();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function removeWindowCaches(windowId) {
|
|
624
|
+
for (const key of [...sessionGroupCache.keys()]) {
|
|
625
|
+
if (key.startsWith(`${windowId}:`)) {
|
|
626
|
+
sessionGroupCache.delete(key);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const [session, mappedWindowId] of [...sessionWindowMap.entries()]) {
|
|
630
|
+
if (mappedWindowId === windowId) {
|
|
631
|
+
sessionWindowMap.delete(session);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function ensureSessionWindow(tabId, currentWindowId, session) {
|
|
637
|
+
if (!extensionOptions.strictWindowIsolation) {
|
|
638
|
+
sessionWindowMap.set(session, currentWindowId);
|
|
639
|
+
return currentWindowId;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let targetWindowId = sessionWindowMap.get(session);
|
|
643
|
+
|
|
644
|
+
if (typeof targetWindowId === 'number') {
|
|
645
|
+
try {
|
|
646
|
+
await chrome.windows.get(targetWindowId);
|
|
647
|
+
} catch {
|
|
648
|
+
sessionWindowMap.delete(session);
|
|
649
|
+
targetWindowId = undefined;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (typeof targetWindowId !== 'number') {
|
|
654
|
+
sessionWindowMap.set(session, currentWindowId);
|
|
655
|
+
return currentWindowId;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (targetWindowId === currentWindowId) {
|
|
659
|
+
return targetWindowId;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
await chrome.tabs.move(tabId, { windowId: targetWindowId, index: -1 });
|
|
663
|
+
await chrome.tabs.update(tabId, { active: false }).catch(() => {});
|
|
664
|
+
return targetWindowId;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function findExistingGroup(windowId, groupTitle) {
|
|
668
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
669
|
+
const checked = new Set();
|
|
670
|
+
|
|
671
|
+
for (const tab of tabs) {
|
|
672
|
+
if (typeof tab.groupId !== 'number' || tab.groupId < 0 || checked.has(tab.groupId)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
checked.add(tab.groupId);
|
|
677
|
+
try {
|
|
678
|
+
const group = await chrome.tabGroups.get(tab.groupId);
|
|
679
|
+
if (group.title === groupTitle) {
|
|
680
|
+
return tab.groupId;
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
// Ignore stale group references.
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function ensureSessionGroup(tabId, windowId, session, groupTitle) {
|
|
691
|
+
const targetWindowId = await ensureSessionWindow(tabId, windowId, session);
|
|
692
|
+
const key = cacheKey(targetWindowId, session);
|
|
693
|
+
let groupId = sessionGroupCache.get(key);
|
|
694
|
+
|
|
695
|
+
if (typeof groupId === 'number') {
|
|
696
|
+
try {
|
|
697
|
+
await chrome.tabGroups.get(groupId);
|
|
698
|
+
} catch {
|
|
699
|
+
groupId = undefined;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (typeof groupId !== 'number') {
|
|
704
|
+
const existing = await findExistingGroup(targetWindowId, groupTitle);
|
|
705
|
+
if (typeof existing === 'number') {
|
|
706
|
+
groupId = existing;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (typeof groupId === 'number') {
|
|
711
|
+
await chrome.tabs.group({ groupId, tabIds: [tabId] });
|
|
712
|
+
} else {
|
|
713
|
+
groupId = await chrome.tabs.group({
|
|
714
|
+
tabIds: [tabId],
|
|
715
|
+
createProperties: { windowId: targetWindowId },
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const color = pickColorForSession(session);
|
|
720
|
+
const collapsed = shouldCollapseGroup(session);
|
|
721
|
+
await chrome.tabGroups.update(groupId, {
|
|
722
|
+
title: groupTitle,
|
|
723
|
+
color,
|
|
724
|
+
collapsed,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
sessionGroupCache.set(key, groupId);
|
|
728
|
+
sessionWindowMap.set(session, targetWindowId);
|
|
729
|
+
sessionGroupTitleMap.set(session, groupTitle);
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
groupId,
|
|
733
|
+
windowId: targetWindowId,
|
|
734
|
+
color,
|
|
735
|
+
collapsed,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function applySessionDomainFallback(tabId, session) {
|
|
740
|
+
const allowedDomains = getSessionPolicy(session);
|
|
741
|
+
if (allowedDomains.length === 0) {
|
|
742
|
+
return { enforced: false, blocked: false };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let tab;
|
|
746
|
+
try {
|
|
747
|
+
tab = await chrome.tabs.get(tabId);
|
|
748
|
+
} catch {
|
|
749
|
+
return { enforced: true, blocked: false };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const hostname = parseHostname(tab.url);
|
|
753
|
+
if (!hostname) {
|
|
754
|
+
return { enforced: true, blocked: false };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (isDomainAllowed(hostname, allowedDomains)) {
|
|
758
|
+
return { enforced: true, blocked: false };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
await chrome.tabs.update(tabId, { url: 'about:blank' }).catch(() => {});
|
|
762
|
+
return {
|
|
763
|
+
enforced: true,
|
|
764
|
+
blocked: true,
|
|
765
|
+
reason: `${hostname} is not in allowed domains`,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function getManagedSessionForTab(tabId) {
|
|
770
|
+
if (typeof tabId !== 'number') return undefined;
|
|
771
|
+
return tabSessionMap.get(tabId);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function ensureManagedTab(tabId, sessionHint) {
|
|
775
|
+
if (typeof tabId !== 'number') return null;
|
|
776
|
+
|
|
777
|
+
let tab;
|
|
778
|
+
try {
|
|
779
|
+
tab = await chrome.tabs.get(tabId);
|
|
780
|
+
} catch {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (typeof tab.windowId !== 'number') {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const session = normalizeSession(sessionHint || getManagedSessionForTab(tabId) || 'default');
|
|
789
|
+
const groupTitle = getGroupTitleForSession(session);
|
|
790
|
+
tabSessionMap.set(tabId, session);
|
|
791
|
+
updateTabMeta(tab);
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const grouping = await ensureSessionGroup(tabId, tab.windowId, session, groupTitle);
|
|
795
|
+
return { session, groupTitle, grouping };
|
|
796
|
+
} catch {
|
|
797
|
+
// Best-effort grouping: keep session mapping even if group APIs fail.
|
|
798
|
+
return { session, groupTitle, grouping: null };
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function collectSessionTabIds(session) {
|
|
803
|
+
const result = [];
|
|
804
|
+
for (const [tabId, tabSession] of tabSessionMap.entries()) {
|
|
805
|
+
if (tabSession === session) {
|
|
806
|
+
result.push(tabId);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return result;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function closeOtherSessionTabs(session) {
|
|
813
|
+
const normalized = normalizeSession(session);
|
|
814
|
+
const closeIds = [];
|
|
815
|
+
|
|
816
|
+
for (const [tabId, tabSession] of tabSessionMap.entries()) {
|
|
817
|
+
if (tabSession !== normalized) {
|
|
818
|
+
closeIds.push(tabId);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (closeIds.length > 0) {
|
|
823
|
+
await chrome.tabs.remove(closeIds);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return { closed: closeIds.length };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function focusSession(session) {
|
|
830
|
+
const normalized = normalizeSession(session);
|
|
831
|
+
const tabIds = collectSessionTabIds(normalized);
|
|
832
|
+
if (tabIds.length === 0) {
|
|
833
|
+
return { focused: false };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
let tab;
|
|
837
|
+
try {
|
|
838
|
+
tab = await chrome.tabs.get(tabIds[0]);
|
|
839
|
+
} catch {
|
|
840
|
+
return { focused: false };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (typeof tab.windowId === 'number') {
|
|
844
|
+
await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {});
|
|
845
|
+
}
|
|
846
|
+
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
|
|
847
|
+
return { focused: true, tabId: tab.id };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function cleanEmptyGroups() {
|
|
851
|
+
let removedGroups = 0;
|
|
852
|
+
let removedWindows = 0;
|
|
853
|
+
|
|
854
|
+
for (const [key, groupId] of [...sessionGroupCache.entries()]) {
|
|
855
|
+
const [windowIdRaw] = key.split(':');
|
|
856
|
+
const windowId = Number(windowIdRaw);
|
|
857
|
+
|
|
858
|
+
let groupExists = true;
|
|
859
|
+
try {
|
|
860
|
+
await chrome.tabGroups.get(groupId);
|
|
861
|
+
} catch {
|
|
862
|
+
groupExists = false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!groupExists) {
|
|
866
|
+
sessionGroupCache.delete(key);
|
|
867
|
+
removedGroups += 1;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const tabs = await chrome.tabs.query({ windowId }).catch(() => []);
|
|
872
|
+
const hasMembers = tabs.some((tab) => tab.groupId === groupId);
|
|
873
|
+
if (!hasMembers) {
|
|
874
|
+
sessionGroupCache.delete(key);
|
|
875
|
+
removedGroups += 1;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
for (const [session, windowId] of [...sessionWindowMap.entries()]) {
|
|
880
|
+
try {
|
|
881
|
+
await chrome.windows.get(windowId);
|
|
882
|
+
} catch {
|
|
883
|
+
sessionWindowMap.delete(session);
|
|
884
|
+
removedWindows += 1;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return { removedGroups, removedWindows };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function syncCleanupAlarm() {
|
|
892
|
+
try {
|
|
893
|
+
await chrome.alarms.clear(CLEANUP_ALARM_NAME);
|
|
894
|
+
if (extensionOptions.autoCleanEmptyGroups) {
|
|
895
|
+
await chrome.alarms.create(CLEANUP_ALARM_NAME, { periodInMinutes: 1 });
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
// Ignore alarms API failures.
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function enforceSessionWindowAffinity(tabId) {
|
|
903
|
+
if (!extensionOptions.suppressCrossWindowActivation) return { moved: false };
|
|
904
|
+
|
|
905
|
+
const session = getManagedSessionForTab(tabId);
|
|
906
|
+
if (!session) return { moved: false };
|
|
907
|
+
if (!extensionOptions.strictWindowIsolation) return { moved: false };
|
|
908
|
+
|
|
909
|
+
let tab;
|
|
910
|
+
try {
|
|
911
|
+
tab = await chrome.tabs.get(tabId);
|
|
912
|
+
} catch {
|
|
913
|
+
return { moved: false };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const mappedWindowId = sessionWindowMap.get(session);
|
|
917
|
+
if (typeof mappedWindowId !== 'number' || mappedWindowId === tab.windowId) {
|
|
918
|
+
if (typeof tab.windowId === 'number') {
|
|
919
|
+
sessionWindowMap.set(session, tab.windowId);
|
|
920
|
+
}
|
|
921
|
+
return { moved: false };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
try {
|
|
925
|
+
await chrome.tabs.move(tabId, { windowId: mappedWindowId, index: -1 });
|
|
926
|
+
await chrome.tabs.update(tabId, { active: false }).catch(() => {});
|
|
927
|
+
return { moved: true, toWindowId: mappedWindowId };
|
|
928
|
+
} catch {
|
|
929
|
+
return { moved: false };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function updateRiskBadge(tabId) {
|
|
934
|
+
let text = '';
|
|
935
|
+
let title = 'agent-browser-stealth';
|
|
936
|
+
|
|
937
|
+
const session = getManagedSessionForTab(tabId);
|
|
938
|
+
if (session) {
|
|
939
|
+
let tab;
|
|
940
|
+
try {
|
|
941
|
+
tab = await chrome.tabs.get(tabId);
|
|
942
|
+
} catch {
|
|
943
|
+
tab = undefined;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (tab) {
|
|
947
|
+
const hints = collectRiskHints(tab.url, getSessionPolicy(session));
|
|
948
|
+
if (hints.length > 0) {
|
|
949
|
+
text = '!';
|
|
950
|
+
title = `Risk hints (${hints.length}): ${hints.join(', ')}`;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
await chrome.action.setBadgeText({ text }).catch(() => {});
|
|
956
|
+
await chrome.action.setBadgeBackgroundColor({ color: '#dc2626' }).catch(() => {});
|
|
957
|
+
await chrome.action.setTitle({ title }).catch(() => {});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function buildWorkflowSummary() {
|
|
961
|
+
return [...workflows.values()].map((workflow) => ({
|
|
962
|
+
id: workflow.id,
|
|
963
|
+
name: workflow.name,
|
|
964
|
+
stepCount: workflow.steps.length,
|
|
965
|
+
steps: workflow.steps,
|
|
966
|
+
createdAt: workflow.createdAt,
|
|
967
|
+
updatedAt: workflow.updatedAt,
|
|
968
|
+
}));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function buildShortcutSummary() {
|
|
972
|
+
return [...shortcuts.entries()].map(([name, workflowId]) => ({
|
|
973
|
+
name,
|
|
974
|
+
workflowId,
|
|
975
|
+
workflowName: workflows.get(workflowId)?.name || workflowId,
|
|
976
|
+
}));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function buildScheduleSummary() {
|
|
980
|
+
return [...schedules.values()].map((schedule) => ({
|
|
981
|
+
...schedule,
|
|
982
|
+
workflowName: workflows.get(schedule.workflowId)?.name || schedule.workflowId,
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function buildActivitySummary() {
|
|
987
|
+
const recent = activityEvents.slice(-200).reverse();
|
|
988
|
+
return {
|
|
989
|
+
events: recent,
|
|
990
|
+
console: recent.filter((event) => event.kind === 'console').slice(0, 80),
|
|
991
|
+
network: recent.filter((event) => event.kind === 'network').slice(0, 120),
|
|
992
|
+
commandHistory: commandHistory.slice(-80).reverse(),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function getControlState() {
|
|
997
|
+
const tabs = await chrome.tabs.query({ currentWindow: true });
|
|
998
|
+
const activeTab = tabs.find((tab) => tab.active === true) || tabs[0] || null;
|
|
999
|
+
|
|
1000
|
+
return {
|
|
1001
|
+
activeTab:
|
|
1002
|
+
activeTab && typeof activeTab.id === 'number'
|
|
1003
|
+
? {
|
|
1004
|
+
id: activeTab.id,
|
|
1005
|
+
title: activeTab.title || '',
|
|
1006
|
+
url: activeTab.url || '',
|
|
1007
|
+
windowId: activeTab.windowId,
|
|
1008
|
+
}
|
|
1009
|
+
: null,
|
|
1010
|
+
tabs: tabs.map((tab) => ({
|
|
1011
|
+
id: tab.id,
|
|
1012
|
+
index: tab.index,
|
|
1013
|
+
title: tab.title || '(Untitled)',
|
|
1014
|
+
url: tab.url || '',
|
|
1015
|
+
active: tab.active === true,
|
|
1016
|
+
windowId: tab.windowId,
|
|
1017
|
+
session: getManagedSessionForTab(tab.id),
|
|
1018
|
+
})),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function buildPanelState() {
|
|
1023
|
+
const allTabs = await chrome.tabs.query({});
|
|
1024
|
+
for (const tab of allTabs) {
|
|
1025
|
+
updateTabMeta(tab);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const sessionMap = new Map();
|
|
1029
|
+
|
|
1030
|
+
for (const tab of allTabs) {
|
|
1031
|
+
if (typeof tab.id !== 'number') continue;
|
|
1032
|
+
const session = getManagedSessionForTab(tab.id);
|
|
1033
|
+
if (!session) continue;
|
|
1034
|
+
|
|
1035
|
+
if (!sessionMap.has(session)) {
|
|
1036
|
+
sessionMap.set(session, {
|
|
1037
|
+
session,
|
|
1038
|
+
windowId: sessionWindowMap.get(session) ?? tab.windowId,
|
|
1039
|
+
allowedDomains: getSessionPolicy(session),
|
|
1040
|
+
tabs: [],
|
|
1041
|
+
riskHints: [],
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const entry = sessionMap.get(session);
|
|
1046
|
+
entry.tabs.push({
|
|
1047
|
+
id: tab.id,
|
|
1048
|
+
windowId: tab.windowId,
|
|
1049
|
+
title: tab.title ?? '',
|
|
1050
|
+
url: tab.url ?? '',
|
|
1051
|
+
active: tab.active === true,
|
|
1052
|
+
groupId: typeof tab.groupId === 'number' ? tab.groupId : -1,
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const hints = collectRiskHints(tab.url, entry.allowedDomains);
|
|
1056
|
+
for (const hint of hints) {
|
|
1057
|
+
if (!entry.riskHints.includes(hint)) {
|
|
1058
|
+
entry.riskHints.push(hint);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const sessions = [];
|
|
1064
|
+
for (const sessionEntry of sessionMap.values()) {
|
|
1065
|
+
sessionEntry.tabs.sort((a, b) => Number(b.active) - Number(a.active));
|
|
1066
|
+
const key = cacheKey(sessionEntry.windowId, sessionEntry.session);
|
|
1067
|
+
const cachedGroupId = sessionGroupCache.get(key);
|
|
1068
|
+
|
|
1069
|
+
let group;
|
|
1070
|
+
if (typeof cachedGroupId === 'number') {
|
|
1071
|
+
try {
|
|
1072
|
+
const groupInfo = await chrome.tabGroups.get(cachedGroupId);
|
|
1073
|
+
group = {
|
|
1074
|
+
id: cachedGroupId,
|
|
1075
|
+
title: groupInfo.title,
|
|
1076
|
+
color: groupInfo.color,
|
|
1077
|
+
collapsed: groupInfo.collapsed,
|
|
1078
|
+
};
|
|
1079
|
+
} catch {
|
|
1080
|
+
// Group may no longer exist.
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
sessions.push({
|
|
1085
|
+
...sessionEntry,
|
|
1086
|
+
group,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
sessions.sort((a, b) => a.session.localeCompare(b.session));
|
|
1091
|
+
|
|
1092
|
+
const control = await getControlState();
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
extensionId: chrome.runtime.id,
|
|
1096
|
+
options: { ...extensionOptions },
|
|
1097
|
+
totals: {
|
|
1098
|
+
sessions: sessions.length,
|
|
1099
|
+
tabs: sessions.reduce((sum, session) => sum + session.tabs.length, 0),
|
|
1100
|
+
},
|
|
1101
|
+
sessions,
|
|
1102
|
+
downloads: downloadEvents.slice(-25).reverse(),
|
|
1103
|
+
latestDomState,
|
|
1104
|
+
control,
|
|
1105
|
+
activity: buildActivitySummary(),
|
|
1106
|
+
automation: {
|
|
1107
|
+
recording: recordingState
|
|
1108
|
+
? {
|
|
1109
|
+
id: recordingState.id,
|
|
1110
|
+
name: recordingState.name,
|
|
1111
|
+
startedAt: recordingState.startedAt,
|
|
1112
|
+
stoppedAt: recordingState.stoppedAt,
|
|
1113
|
+
stepCount: recordingState.steps.length,
|
|
1114
|
+
steps: recordingState.steps,
|
|
1115
|
+
}
|
|
1116
|
+
: null,
|
|
1117
|
+
workflows: buildWorkflowSummary(),
|
|
1118
|
+
shortcuts: buildShortcutSummary(),
|
|
1119
|
+
schedules: buildScheduleSummary(),
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function waitForTabSettled(tabId, timeoutMs = 15_000) {
|
|
1125
|
+
return new Promise((resolve, reject) => {
|
|
1126
|
+
const deadline = now() + timeoutMs;
|
|
1127
|
+
|
|
1128
|
+
const timer = setTimeout(() => {
|
|
1129
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
1130
|
+
reject(new Error('tab-load-timeout'));
|
|
1131
|
+
}, timeoutMs);
|
|
1132
|
+
|
|
1133
|
+
const onUpdated = (updatedTabId, changeInfo) => {
|
|
1134
|
+
if (updatedTabId !== tabId) return;
|
|
1135
|
+
if (changeInfo.status === 'complete') {
|
|
1136
|
+
clearTimeout(timer);
|
|
1137
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
1138
|
+
resolve(true);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
1143
|
+
|
|
1144
|
+
chrome.tabs
|
|
1145
|
+
.get(tabId)
|
|
1146
|
+
.then((tab) => {
|
|
1147
|
+
if (tab.status === 'complete') {
|
|
1148
|
+
clearTimeout(timer);
|
|
1149
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
1150
|
+
resolve(true);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (now() > deadline) {
|
|
1155
|
+
clearTimeout(timer);
|
|
1156
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
1157
|
+
reject(new Error('tab-load-timeout'));
|
|
1158
|
+
}
|
|
1159
|
+
})
|
|
1160
|
+
.catch(() => {
|
|
1161
|
+
clearTimeout(timer);
|
|
1162
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
1163
|
+
reject(new Error('tab-not-found'));
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function delay(ms) {
|
|
1169
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
async function getOrCreateActionTab(tabId) {
|
|
1173
|
+
if (typeof tabId === 'number') {
|
|
1174
|
+
try {
|
|
1175
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1176
|
+
return tab;
|
|
1177
|
+
} catch {
|
|
1178
|
+
// fall through
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1183
|
+
if (activeTab && typeof activeTab.id === 'number') {
|
|
1184
|
+
return activeTab;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return chrome.tabs.create({ url: 'about:blank', active: true });
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async function sendContentCommand(tabId, message) {
|
|
1191
|
+
try {
|
|
1192
|
+
return await chrome.tabs.sendMessage(tabId, message);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
return {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function shouldRecordAction(action) {
|
|
1202
|
+
return ![
|
|
1203
|
+
'tabs:list',
|
|
1204
|
+
'dom-state',
|
|
1205
|
+
'snapshot',
|
|
1206
|
+
'shortcut:run',
|
|
1207
|
+
'workflow:run',
|
|
1208
|
+
].includes(action);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function recordWorkflowStep(action, args) {
|
|
1212
|
+
if (!recordingState) return;
|
|
1213
|
+
if (!shouldRecordAction(action)) return;
|
|
1214
|
+
|
|
1215
|
+
recordingState.steps.push({
|
|
1216
|
+
id: uid('step'),
|
|
1217
|
+
action,
|
|
1218
|
+
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1219
|
+
retries: 0,
|
|
1220
|
+
timeoutMs: 0,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async function runShortcutByName(name, options = {}) {
|
|
1225
|
+
const shortcutName = normalizeShortcutName(name);
|
|
1226
|
+
if (!shortcutName) {
|
|
1227
|
+
return { ok: false, error: 'shortcut-name-required' };
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const workflowId = shortcuts.get(shortcutName);
|
|
1231
|
+
if (!workflowId) {
|
|
1232
|
+
return { ok: false, error: `shortcut-not-found:${shortcutName}` };
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const runResult = await runWorkflowById(workflowId, {
|
|
1236
|
+
source: options.source || 'shortcut',
|
|
1237
|
+
tabId: options.tabId,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
return {
|
|
1241
|
+
...runResult,
|
|
1242
|
+
shortcut: shortcutName,
|
|
1243
|
+
workflowId,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async function runActionInternal(request, options = {}) {
|
|
1248
|
+
const action = typeof request.action === 'string' ? request.action.trim() : '';
|
|
1249
|
+
const args = request.args && typeof request.args === 'object' ? request.args : {};
|
|
1250
|
+
const source = options.source || 'panel';
|
|
1251
|
+
|
|
1252
|
+
if (!action) {
|
|
1253
|
+
return { ok: false, error: 'action-required' };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (action.startsWith('/')) {
|
|
1257
|
+
return runShortcutByName(action.slice(1), {
|
|
1258
|
+
source,
|
|
1259
|
+
tabId: request.tabId,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
let tab = null;
|
|
1264
|
+
let tabId = typeof request.tabId === 'number' ? request.tabId : undefined;
|
|
1265
|
+
let sourceSession = undefined;
|
|
1266
|
+
|
|
1267
|
+
if (!['tabs:list'].includes(action)) {
|
|
1268
|
+
tab = await getOrCreateActionTab(tabId);
|
|
1269
|
+
tabId = tab.id;
|
|
1270
|
+
const managed = await ensureManagedTab(tabId, getManagedSessionForTab(tabId));
|
|
1271
|
+
sourceSession = managed?.session;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
let result;
|
|
1275
|
+
|
|
1276
|
+
switch (action) {
|
|
1277
|
+
case 'open': {
|
|
1278
|
+
const url = normalizeUrl(args.url || request.url);
|
|
1279
|
+
if (!url) {
|
|
1280
|
+
result = { ok: false, error: 'url-required' };
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
const updatedTab = await chrome.tabs.update(tabId, { url, active: true });
|
|
1284
|
+
await waitForTabSettled(updatedTab.id, clampInt(args.timeoutMs, 1000, 60_000, 12_000)).catch(
|
|
1285
|
+
() => {}
|
|
1286
|
+
);
|
|
1287
|
+
result = {
|
|
1288
|
+
ok: true,
|
|
1289
|
+
action,
|
|
1290
|
+
tabId: updatedTab.id,
|
|
1291
|
+
url,
|
|
1292
|
+
};
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
case 'back': {
|
|
1297
|
+
if (typeof chrome.tabs.goBack === 'function') {
|
|
1298
|
+
await chrome.tabs.goBack(tabId);
|
|
1299
|
+
} else {
|
|
1300
|
+
await sendContentCommand(tabId, {
|
|
1301
|
+
type: CONTENT_EXECUTE_ACTION,
|
|
1302
|
+
command: 'eval',
|
|
1303
|
+
args: { expression: '(() => { history.back(); return true; })()' },
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
result = { ok: true, action, tabId };
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
case 'forward': {
|
|
1311
|
+
if (typeof chrome.tabs.goForward === 'function') {
|
|
1312
|
+
await chrome.tabs.goForward(tabId);
|
|
1313
|
+
} else {
|
|
1314
|
+
await sendContentCommand(tabId, {
|
|
1315
|
+
type: CONTENT_EXECUTE_ACTION,
|
|
1316
|
+
command: 'eval',
|
|
1317
|
+
args: { expression: '(() => { history.forward(); return true; })()' },
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
result = { ok: true, action, tabId };
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
case 'reload': {
|
|
1325
|
+
await chrome.tabs.reload(tabId);
|
|
1326
|
+
await waitForTabSettled(tabId, clampInt(args.timeoutMs, 1000, 60_000, 10_000)).catch(() => {});
|
|
1327
|
+
result = { ok: true, action, tabId };
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
case 'wait': {
|
|
1332
|
+
const ms = clampInt(args.ms, 0, 120_000, 1000);
|
|
1333
|
+
await delay(ms);
|
|
1334
|
+
result = { ok: true, action, waitedMs: ms, tabId };
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
case 'click':
|
|
1339
|
+
case 'fill':
|
|
1340
|
+
case 'press':
|
|
1341
|
+
case 'eval':
|
|
1342
|
+
case 'snapshot': {
|
|
1343
|
+
// DOM-level commands are delegated to the page content-script.
|
|
1344
|
+
const commandArgs =
|
|
1345
|
+
action === 'eval'
|
|
1346
|
+
? { expression: args.expression }
|
|
1347
|
+
: {
|
|
1348
|
+
selector: args.selector,
|
|
1349
|
+
value: args.value,
|
|
1350
|
+
key: args.key,
|
|
1351
|
+
interactiveOnly: args.interactiveOnly === true,
|
|
1352
|
+
maxNodes: args.maxNodes,
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
const command = action === 'eval' ? 'eval' : action;
|
|
1356
|
+
const response = await sendContentCommand(tabId, {
|
|
1357
|
+
type: CONTENT_EXECUTE_ACTION,
|
|
1358
|
+
command,
|
|
1359
|
+
args: commandArgs,
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
result = {
|
|
1363
|
+
...(response && typeof response === 'object' ? response : { ok: false, error: 'invalid-response' }),
|
|
1364
|
+
action,
|
|
1365
|
+
tabId,
|
|
1366
|
+
};
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
case 'dom-state': {
|
|
1371
|
+
const response = await sendContentCommand(tabId, {
|
|
1372
|
+
type: CONTENT_GET_DOM_STATE,
|
|
1373
|
+
options: {
|
|
1374
|
+
selector: args.selector,
|
|
1375
|
+
interactiveOnly: args.interactiveOnly === true,
|
|
1376
|
+
maxNodes: args.maxNodes,
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
result = {
|
|
1381
|
+
...(response && typeof response === 'object' ? response : { ok: false, error: 'invalid-response' }),
|
|
1382
|
+
action,
|
|
1383
|
+
tabId,
|
|
1384
|
+
};
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
case 'tabs:list': {
|
|
1389
|
+
const tabs = await chrome.tabs.query({ currentWindow: true });
|
|
1390
|
+
result = {
|
|
1391
|
+
ok: true,
|
|
1392
|
+
action,
|
|
1393
|
+
tabs: tabs.map((entry) => ({
|
|
1394
|
+
id: entry.id,
|
|
1395
|
+
index: entry.index,
|
|
1396
|
+
title: entry.title,
|
|
1397
|
+
url: entry.url,
|
|
1398
|
+
active: entry.active === true,
|
|
1399
|
+
session: getManagedSessionForTab(entry.id),
|
|
1400
|
+
})),
|
|
1401
|
+
};
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
case 'tabs:new': {
|
|
1406
|
+
const url = normalizeUrl(args.url) || 'about:blank';
|
|
1407
|
+
const created = await chrome.tabs.create({ url, active: true });
|
|
1408
|
+
const managed = await ensureManagedTab(created.id, sourceSession || 'default');
|
|
1409
|
+
result = {
|
|
1410
|
+
ok: true,
|
|
1411
|
+
action,
|
|
1412
|
+
tabId: created.id,
|
|
1413
|
+
url,
|
|
1414
|
+
session: managed?.session || null,
|
|
1415
|
+
groupId: managed?.grouping?.groupId ?? null,
|
|
1416
|
+
};
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
case 'tabs:switch': {
|
|
1421
|
+
if (typeof args.tabId === 'number') {
|
|
1422
|
+
const switched = await chrome.tabs.update(args.tabId, { active: true });
|
|
1423
|
+
if (typeof switched.windowId === 'number') {
|
|
1424
|
+
await chrome.windows.update(switched.windowId, { focused: true }).catch(() => {});
|
|
1425
|
+
}
|
|
1426
|
+
const managed = await ensureManagedTab(switched.id, sourceSession || 'default');
|
|
1427
|
+
result = {
|
|
1428
|
+
ok: true,
|
|
1429
|
+
action,
|
|
1430
|
+
tabId: switched.id,
|
|
1431
|
+
session: managed?.session || null,
|
|
1432
|
+
};
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const index = clampInt(args.index, 0, 500, 0);
|
|
1437
|
+
const tabs = await chrome.tabs.query({ currentWindow: true });
|
|
1438
|
+
const target = tabs.find((item) => item.index === index);
|
|
1439
|
+
if (!target || typeof target.id !== 'number') {
|
|
1440
|
+
result = { ok: false, error: `tab-index-not-found:${index}` };
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const switched = await chrome.tabs.update(target.id, { active: true });
|
|
1445
|
+
const managed = await ensureManagedTab(switched.id, sourceSession || 'default');
|
|
1446
|
+
result = {
|
|
1447
|
+
ok: true,
|
|
1448
|
+
action,
|
|
1449
|
+
tabId: switched.id,
|
|
1450
|
+
session: managed?.session || null,
|
|
1451
|
+
};
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
case 'tabs:close': {
|
|
1456
|
+
await chrome.tabs.remove(tabId);
|
|
1457
|
+
result = { ok: true, action, tabId };
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
case 'shortcut:run': {
|
|
1462
|
+
result = await runShortcutByName(args.name, {
|
|
1463
|
+
source,
|
|
1464
|
+
tabId,
|
|
1465
|
+
});
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
default:
|
|
1470
|
+
result = { ok: false, error: `unknown-action:${action}` };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
pushCommandHistory({
|
|
1474
|
+
action,
|
|
1475
|
+
args,
|
|
1476
|
+
ok: result?.ok === true,
|
|
1477
|
+
source,
|
|
1478
|
+
error: result?.ok === true ? null : result?.error || 'unknown',
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
if (options.record !== false && result?.ok === true) {
|
|
1482
|
+
recordWorkflowStep(action, args);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (
|
|
1486
|
+
(action === 'dom-state' || action === 'snapshot') &&
|
|
1487
|
+
result?.ok === true &&
|
|
1488
|
+
result?.state &&
|
|
1489
|
+
typeof result.state === 'object'
|
|
1490
|
+
) {
|
|
1491
|
+
latestDomState = result.state;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
pushActivityEvent('command', {
|
|
1495
|
+
action,
|
|
1496
|
+
ok: result?.ok === true,
|
|
1497
|
+
error: result?.ok === true ? null : result?.error || 'unknown',
|
|
1498
|
+
}, {
|
|
1499
|
+
source,
|
|
1500
|
+
tabId,
|
|
1501
|
+
session: typeof tabId === 'number' ? getManagedSessionForTab(tabId) : null,
|
|
1502
|
+
url: tab?.url || '',
|
|
1503
|
+
title: tab?.title || '',
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
return result;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function runWorkflowById(workflowId, options = {}) {
|
|
1510
|
+
const workflow = workflows.get(workflowId);
|
|
1511
|
+
if (!workflow) {
|
|
1512
|
+
return { ok: false, error: `workflow-not-found:${workflowId}` };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
let currentTabId = typeof options.tabId === 'number' ? options.tabId : undefined;
|
|
1516
|
+
const results = [];
|
|
1517
|
+
|
|
1518
|
+
for (const step of workflow.steps) {
|
|
1519
|
+
// Retry each step with bounded backoff for transient page timing issues.
|
|
1520
|
+
let stepResult = null;
|
|
1521
|
+
const retries = clampInt(step.retries, 0, 5, 0);
|
|
1522
|
+
|
|
1523
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
1524
|
+
stepResult = await runActionInternal(
|
|
1525
|
+
{
|
|
1526
|
+
action: step.action,
|
|
1527
|
+
args: step.args,
|
|
1528
|
+
tabId: currentTabId,
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
source: options.source || 'workflow',
|
|
1532
|
+
record: false,
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
|
|
1536
|
+
if (stepResult?.ok === true) {
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (attempt < retries) {
|
|
1541
|
+
await delay(300 * (attempt + 1));
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
results.push({
|
|
1546
|
+
action: step.action,
|
|
1547
|
+
ok: stepResult?.ok === true,
|
|
1548
|
+
error: stepResult?.ok === true ? null : stepResult?.error || 'unknown',
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
if (stepResult?.ok !== true) {
|
|
1552
|
+
pushActivityEvent('workflow', {
|
|
1553
|
+
workflowId: workflow.id,
|
|
1554
|
+
workflowName: workflow.name,
|
|
1555
|
+
ok: false,
|
|
1556
|
+
failedAction: step.action,
|
|
1557
|
+
error: stepResult?.error || 'unknown',
|
|
1558
|
+
}, {
|
|
1559
|
+
source: options.source || 'workflow',
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
return {
|
|
1563
|
+
ok: false,
|
|
1564
|
+
workflowId: workflow.id,
|
|
1565
|
+
workflowName: workflow.name,
|
|
1566
|
+
error: `workflow-step-failed:${step.action}`,
|
|
1567
|
+
results,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (typeof stepResult.tabId === 'number') {
|
|
1572
|
+
currentTabId = stepResult.tabId;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (step.timeoutMs > 0) {
|
|
1576
|
+
await delay(step.timeoutMs);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
pushActivityEvent('workflow', {
|
|
1581
|
+
workflowId: workflow.id,
|
|
1582
|
+
workflowName: workflow.name,
|
|
1583
|
+
ok: true,
|
|
1584
|
+
steps: workflow.steps.length,
|
|
1585
|
+
}, {
|
|
1586
|
+
source: options.source || 'workflow',
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
ok: true,
|
|
1591
|
+
workflowId: workflow.id,
|
|
1592
|
+
workflowName: workflow.name,
|
|
1593
|
+
results,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async function startRecording(name) {
|
|
1598
|
+
const normalizedName =
|
|
1599
|
+
typeof name === 'string' && name.trim().length > 0 ? name.trim().slice(0, 120) : 'Recorded Workflow';
|
|
1600
|
+
|
|
1601
|
+
recordingState = {
|
|
1602
|
+
id: uid('recording'),
|
|
1603
|
+
name: normalizedName,
|
|
1604
|
+
startedAt: now(),
|
|
1605
|
+
stoppedAt: null,
|
|
1606
|
+
steps: [],
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
pushActivityEvent('recording', {
|
|
1610
|
+
event: 'start',
|
|
1611
|
+
name: normalizedName,
|
|
1612
|
+
}, {
|
|
1613
|
+
source: 'panel',
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
return { ok: true, recording: recordingState };
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async function stopRecording() {
|
|
1620
|
+
if (!recordingState) {
|
|
1621
|
+
return { ok: false, error: 'recording-not-active' };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
recordingState.stoppedAt = now();
|
|
1625
|
+
|
|
1626
|
+
pushActivityEvent('recording', {
|
|
1627
|
+
event: 'stop',
|
|
1628
|
+
steps: recordingState.steps.length,
|
|
1629
|
+
name: recordingState.name,
|
|
1630
|
+
}, {
|
|
1631
|
+
source: 'panel',
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
return { ok: true, recording: recordingState };
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function saveRecordingAsWorkflow(name) {
|
|
1638
|
+
if (!recordingState) {
|
|
1639
|
+
return { ok: false, error: 'recording-not-active' };
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (recordingState.steps.length === 0) {
|
|
1643
|
+
return { ok: false, error: 'recording-has-no-steps' };
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const workflowName =
|
|
1647
|
+
typeof name === 'string' && name.trim().length > 0
|
|
1648
|
+
? name.trim().slice(0, 120)
|
|
1649
|
+
: recordingState.name || 'Recorded Workflow';
|
|
1650
|
+
|
|
1651
|
+
const workflow = {
|
|
1652
|
+
id: uid('workflow'),
|
|
1653
|
+
name: workflowName,
|
|
1654
|
+
steps: recordingState.steps.map((step) => normalizeWorkflowStep(step)).filter(Boolean),
|
|
1655
|
+
createdAt: now(),
|
|
1656
|
+
updatedAt: now(),
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
workflows.set(workflow.id, workflow);
|
|
1660
|
+
await persistWorkflows();
|
|
1661
|
+
|
|
1662
|
+
pushActivityEvent('recording', {
|
|
1663
|
+
event: 'saved',
|
|
1664
|
+
workflowId: workflow.id,
|
|
1665
|
+
workflowName: workflow.name,
|
|
1666
|
+
steps: workflow.steps.length,
|
|
1667
|
+
}, {
|
|
1668
|
+
source: 'panel',
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
recordingState = null;
|
|
1672
|
+
return { ok: true, workflow };
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
async function deleteWorkflow(workflowId) {
|
|
1676
|
+
if (!workflows.has(workflowId)) {
|
|
1677
|
+
return { ok: false, error: `workflow-not-found:${workflowId}` };
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
workflows.delete(workflowId);
|
|
1681
|
+
|
|
1682
|
+
for (const [name, mappedWorkflowId] of [...shortcuts.entries()]) {
|
|
1683
|
+
if (mappedWorkflowId === workflowId) {
|
|
1684
|
+
shortcuts.delete(name);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
for (const [scheduleId, schedule] of [...schedules.entries()]) {
|
|
1689
|
+
if (schedule.workflowId === workflowId) {
|
|
1690
|
+
schedules.delete(scheduleId);
|
|
1691
|
+
await chrome.alarms.clear(`${WORKFLOW_ALARM_PREFIX}${scheduleId}`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
await persistWorkflows();
|
|
1696
|
+
await persistShortcuts();
|
|
1697
|
+
await persistSchedules();
|
|
1698
|
+
|
|
1699
|
+
return { ok: true };
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
async function setShortcut(name, workflowId) {
|
|
1703
|
+
const shortcutName = normalizeShortcutName(name);
|
|
1704
|
+
if (!shortcutName) {
|
|
1705
|
+
return { ok: false, error: 'invalid-shortcut-name' };
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
if (!workflows.has(workflowId)) {
|
|
1709
|
+
return { ok: false, error: `workflow-not-found:${workflowId}` };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
shortcuts.set(shortcutName, workflowId);
|
|
1713
|
+
await persistShortcuts();
|
|
1714
|
+
|
|
1715
|
+
return {
|
|
1716
|
+
ok: true,
|
|
1717
|
+
shortcut: {
|
|
1718
|
+
name: shortcutName,
|
|
1719
|
+
workflowId,
|
|
1720
|
+
workflowName: workflows.get(workflowId)?.name || workflowId,
|
|
1721
|
+
},
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
async function deleteShortcut(name) {
|
|
1726
|
+
const shortcutName = normalizeShortcutName(name);
|
|
1727
|
+
if (!shortcutName) {
|
|
1728
|
+
return { ok: false, error: 'invalid-shortcut-name' };
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
shortcuts.delete(shortcutName);
|
|
1732
|
+
await persistShortcuts();
|
|
1733
|
+
return { ok: true };
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
async function createSchedule(input) {
|
|
1737
|
+
if (!input || typeof input !== 'object') {
|
|
1738
|
+
return { ok: false, error: 'schedule-input-required' };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
if (typeof input.workflowId !== 'string' || !workflows.has(input.workflowId)) {
|
|
1742
|
+
return { ok: false, error: 'invalid-workflow-id' };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const cadence = normalizeCadence(input.cadence);
|
|
1746
|
+
const schedule = {
|
|
1747
|
+
id: uid('schedule'),
|
|
1748
|
+
name:
|
|
1749
|
+
typeof input.name === 'string' && input.name.trim().length > 0
|
|
1750
|
+
? input.name.trim().slice(0, 120)
|
|
1751
|
+
: `Schedule ${workflows.get(input.workflowId)?.name || input.workflowId}`,
|
|
1752
|
+
workflowId: input.workflowId,
|
|
1753
|
+
cadence,
|
|
1754
|
+
enabled: input.enabled !== false,
|
|
1755
|
+
createdAt: now(),
|
|
1756
|
+
updatedAt: now(),
|
|
1757
|
+
lastRunAt: null,
|
|
1758
|
+
nextRunAt: computeNextRun(cadence),
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
schedules.set(schedule.id, schedule);
|
|
1762
|
+
await persistSchedules();
|
|
1763
|
+
await scheduleWorkflowAlarm(schedule);
|
|
1764
|
+
|
|
1765
|
+
return { ok: true, schedule };
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
async function deleteSchedule(scheduleId) {
|
|
1769
|
+
if (!schedules.has(scheduleId)) {
|
|
1770
|
+
return { ok: false, error: `schedule-not-found:${scheduleId}` };
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
schedules.delete(scheduleId);
|
|
1774
|
+
await chrome.alarms.clear(`${WORKFLOW_ALARM_PREFIX}${scheduleId}`);
|
|
1775
|
+
await persistSchedules();
|
|
1776
|
+
return { ok: true };
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
async function toggleSchedule(scheduleId, enabled) {
|
|
1780
|
+
const schedule = schedules.get(scheduleId);
|
|
1781
|
+
if (!schedule) {
|
|
1782
|
+
return { ok: false, error: `schedule-not-found:${scheduleId}` };
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
schedule.enabled = enabled !== false;
|
|
1786
|
+
schedule.updatedAt = now();
|
|
1787
|
+
if (schedule.enabled && (!schedule.nextRunAt || schedule.nextRunAt <= now())) {
|
|
1788
|
+
schedule.nextRunAt = computeNextRun(schedule.cadence);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
schedules.set(schedule.id, schedule);
|
|
1792
|
+
await persistSchedules();
|
|
1793
|
+
await scheduleWorkflowAlarm(schedule);
|
|
1794
|
+
|
|
1795
|
+
return { ok: true, schedule };
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
async function runSchedule(scheduleId) {
|
|
1799
|
+
const schedule = schedules.get(scheduleId);
|
|
1800
|
+
if (!schedule) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
if (!schedule.enabled) {
|
|
1805
|
+
await scheduleWorkflowAlarm(schedule);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const runResult = await runWorkflowById(schedule.workflowId, {
|
|
1810
|
+
source: `schedule:${schedule.id}`,
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
schedule.lastRunAt = now();
|
|
1814
|
+
schedule.updatedAt = now();
|
|
1815
|
+
schedule.nextRunAt = computeNextRun(schedule.cadence, schedule.lastRunAt);
|
|
1816
|
+
schedules.set(schedule.id, schedule);
|
|
1817
|
+
|
|
1818
|
+
await persistSchedules();
|
|
1819
|
+
await scheduleWorkflowAlarm(schedule);
|
|
1820
|
+
|
|
1821
|
+
pushActivityEvent('schedule', {
|
|
1822
|
+
scheduleId: schedule.id,
|
|
1823
|
+
scheduleName: schedule.name,
|
|
1824
|
+
workflowId: schedule.workflowId,
|
|
1825
|
+
ok: runResult.ok === true,
|
|
1826
|
+
error: runResult.ok === true ? null : runResult.error || 'unknown',
|
|
1827
|
+
}, {
|
|
1828
|
+
source: 'alarm',
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
async function handleTabGroupRequest(message, sender) {
|
|
1833
|
+
await bootstrapPromise;
|
|
1834
|
+
|
|
1835
|
+
const tabId = sender.tab?.id;
|
|
1836
|
+
const windowId = sender.tab?.windowId;
|
|
1837
|
+
const nonce = typeof message.nonce === 'string' ? message.nonce : undefined;
|
|
1838
|
+
|
|
1839
|
+
if (typeof tabId !== 'number' || typeof windowId !== 'number') {
|
|
1840
|
+
return {
|
|
1841
|
+
ok: false,
|
|
1842
|
+
error: 'missing-tab-context',
|
|
1843
|
+
extensionId: chrome.runtime.id,
|
|
1844
|
+
nonce,
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
if (typeof message.pluginId === 'string' && message.pluginId !== chrome.runtime.id) {
|
|
1849
|
+
return {
|
|
1850
|
+
ok: false,
|
|
1851
|
+
error: 'plugin-id-mismatch',
|
|
1852
|
+
extensionId: chrome.runtime.id,
|
|
1853
|
+
nonce,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const session = normalizeSession(message.session);
|
|
1858
|
+
const groupTitle = normalizeGroupTitle(message.groupTitle);
|
|
1859
|
+
const allowedDomains = normalizeAllowedDomains(message.allowedDomains);
|
|
1860
|
+
if (allowedDomains.length > 0) {
|
|
1861
|
+
await setSessionPolicy(session, allowedDomains);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
tabSessionMap.set(tabId, session);
|
|
1865
|
+
updateTabMeta(sender.tab);
|
|
1866
|
+
|
|
1867
|
+
const grouping = await ensureSessionGroup(tabId, windowId, session, groupTitle);
|
|
1868
|
+
const policy = await applySessionDomainFallback(tabId, session);
|
|
1869
|
+
const riskHints = collectRiskHints(sender.tab?.url, getSessionPolicy(session));
|
|
1870
|
+
if (policy.blocked && policy.reason) {
|
|
1871
|
+
riskHints.push(`policy-blocked:${policy.reason}`);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
return {
|
|
1875
|
+
ok: true,
|
|
1876
|
+
extensionId: chrome.runtime.id,
|
|
1877
|
+
nonce,
|
|
1878
|
+
...grouping,
|
|
1879
|
+
policy,
|
|
1880
|
+
riskHints: [...new Set(riskHints)].slice(0, 10),
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1885
|
+
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
|
1886
|
+
bootstrapPromise = bootstrapState();
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1890
|
+
bootstrapPromise = bootstrapState();
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1894
|
+
if (!message || typeof message !== 'object') {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const type = message.type;
|
|
1899
|
+
|
|
1900
|
+
if (type === REQUEST_TYPE) {
|
|
1901
|
+
handleTabGroupRequest(message, sender)
|
|
1902
|
+
.then((response) => sendResponse(response))
|
|
1903
|
+
.catch((error) => {
|
|
1904
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1905
|
+
sendResponse({
|
|
1906
|
+
ok: false,
|
|
1907
|
+
error: errorMessage,
|
|
1908
|
+
extensionId: chrome.runtime.id,
|
|
1909
|
+
nonce: typeof message.nonce === 'string' ? message.nonce : undefined,
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
return true;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
if (type === CONTENT_EVENT_TYPE) {
|
|
1916
|
+
const tabId = sender.tab?.id;
|
|
1917
|
+
const session = typeof tabId === 'number' ? getManagedSessionForTab(tabId) : null;
|
|
1918
|
+
const kind = typeof message.kind === 'string' ? message.kind : 'unknown';
|
|
1919
|
+
|
|
1920
|
+
pushActivityEvent(kind, message.payload || {}, {
|
|
1921
|
+
source: 'content-script',
|
|
1922
|
+
tabId,
|
|
1923
|
+
session,
|
|
1924
|
+
url: sender.tab?.url || message.url || '',
|
|
1925
|
+
title: sender.tab?.title || message.title || '',
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
sendResponse({ ok: true });
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (type === PANEL_GET_STATE) {
|
|
1933
|
+
bootstrapPromise
|
|
1934
|
+
.then(() => buildPanelState())
|
|
1935
|
+
.then((state) => sendResponse({ ok: true, state }))
|
|
1936
|
+
.catch((error) => {
|
|
1937
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1938
|
+
sendResponse({ ok: false, error: errorMessage });
|
|
1939
|
+
});
|
|
1940
|
+
return true;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if (type === PANEL_RUN_ACTION) {
|
|
1944
|
+
bootstrapPromise
|
|
1945
|
+
.then(() =>
|
|
1946
|
+
runActionInternal(
|
|
1947
|
+
{
|
|
1948
|
+
action: message.action,
|
|
1949
|
+
args: message.args || {},
|
|
1950
|
+
tabId: message.tabId,
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
source: 'panel',
|
|
1954
|
+
record: true,
|
|
1955
|
+
}
|
|
1956
|
+
)
|
|
1957
|
+
)
|
|
1958
|
+
.then((result) => sendResponse(result))
|
|
1959
|
+
.catch((error) => {
|
|
1960
|
+
sendResponse({
|
|
1961
|
+
ok: false,
|
|
1962
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1963
|
+
});
|
|
1964
|
+
});
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (type === PANEL_CLEAR_ACTIVITY) {
|
|
1969
|
+
activityEvents.length = 0;
|
|
1970
|
+
sendResponse({ ok: true });
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
if (type === PANEL_START_RECORDING) {
|
|
1975
|
+
startRecording(message.name)
|
|
1976
|
+
.then((result) => sendResponse(result))
|
|
1977
|
+
.catch((error) => {
|
|
1978
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1979
|
+
});
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (type === PANEL_STOP_RECORDING) {
|
|
1984
|
+
stopRecording()
|
|
1985
|
+
.then((result) => sendResponse(result))
|
|
1986
|
+
.catch((error) => {
|
|
1987
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1988
|
+
});
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
if (type === PANEL_SAVE_RECORDING) {
|
|
1993
|
+
saveRecordingAsWorkflow(message.name)
|
|
1994
|
+
.then((result) => sendResponse(result))
|
|
1995
|
+
.catch((error) => {
|
|
1996
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1997
|
+
});
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (type === PANEL_RUN_WORKFLOW) {
|
|
2002
|
+
runWorkflowById(message.workflowId, {
|
|
2003
|
+
source: 'panel-workflow',
|
|
2004
|
+
tabId: message.tabId,
|
|
2005
|
+
})
|
|
2006
|
+
.then((result) => sendResponse(result))
|
|
2007
|
+
.catch((error) => {
|
|
2008
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2009
|
+
});
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
if (type === PANEL_DELETE_WORKFLOW) {
|
|
2014
|
+
deleteWorkflow(message.workflowId)
|
|
2015
|
+
.then((result) => sendResponse(result))
|
|
2016
|
+
.catch((error) => {
|
|
2017
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2018
|
+
});
|
|
2019
|
+
return true;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
if (type === PANEL_SET_SHORTCUT) {
|
|
2023
|
+
setShortcut(message.name, message.workflowId)
|
|
2024
|
+
.then((result) => sendResponse(result))
|
|
2025
|
+
.catch((error) => {
|
|
2026
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2027
|
+
});
|
|
2028
|
+
return true;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
if (type === PANEL_DELETE_SHORTCUT) {
|
|
2032
|
+
deleteShortcut(message.name)
|
|
2033
|
+
.then((result) => sendResponse(result))
|
|
2034
|
+
.catch((error) => {
|
|
2035
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2036
|
+
});
|
|
2037
|
+
return true;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
if (type === PANEL_RUN_SHORTCUT) {
|
|
2041
|
+
runShortcutByName(message.name, {
|
|
2042
|
+
source: 'panel-shortcut',
|
|
2043
|
+
tabId: message.tabId,
|
|
2044
|
+
})
|
|
2045
|
+
.then((result) => sendResponse(result))
|
|
2046
|
+
.catch((error) => {
|
|
2047
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2048
|
+
});
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (type === PANEL_CREATE_SCHEDULE) {
|
|
2053
|
+
createSchedule(message.schedule)
|
|
2054
|
+
.then((result) => sendResponse(result))
|
|
2055
|
+
.catch((error) => {
|
|
2056
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2057
|
+
});
|
|
2058
|
+
return true;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
if (type === PANEL_DELETE_SCHEDULE) {
|
|
2062
|
+
deleteSchedule(message.scheduleId)
|
|
2063
|
+
.then((result) => sendResponse(result))
|
|
2064
|
+
.catch((error) => {
|
|
2065
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2066
|
+
});
|
|
2067
|
+
return true;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
if (type === PANEL_TOGGLE_SCHEDULE) {
|
|
2071
|
+
toggleSchedule(message.scheduleId, message.enabled)
|
|
2072
|
+
.then((result) => sendResponse(result))
|
|
2073
|
+
.catch((error) => {
|
|
2074
|
+
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
2075
|
+
});
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
if (type === PANEL_CLOSE_OTHER_TABS) {
|
|
2080
|
+
closeOtherSessionTabs(message.session)
|
|
2081
|
+
.then((result) => sendResponse({ ok: true, result }))
|
|
2082
|
+
.catch((error) => {
|
|
2083
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2084
|
+
sendResponse({ ok: false, error: errorMessage });
|
|
2085
|
+
});
|
|
2086
|
+
return true;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
if (type === PANEL_FOCUS_SESSION) {
|
|
2090
|
+
focusSession(message.session)
|
|
2091
|
+
.then((result) => sendResponse({ ok: true, result }))
|
|
2092
|
+
.catch((error) => {
|
|
2093
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2094
|
+
sendResponse({ ok: false, error: errorMessage });
|
|
2095
|
+
});
|
|
2096
|
+
return true;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
if (type === PANEL_CLEAN_EMPTY_GROUPS) {
|
|
2100
|
+
cleanEmptyGroups()
|
|
2101
|
+
.then((result) => sendResponse({ ok: true, result }))
|
|
2102
|
+
.catch((error) => {
|
|
2103
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2104
|
+
sendResponse({ ok: false, error: errorMessage });
|
|
2105
|
+
});
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (type === PANEL_SET_POLICY) {
|
|
2110
|
+
bootstrapPromise
|
|
2111
|
+
.then(() => setSessionPolicy(message.session, message.allowedDomains))
|
|
2112
|
+
.then(() => sendResponse({ ok: true }))
|
|
2113
|
+
.catch((error) => {
|
|
2114
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2115
|
+
sendResponse({ ok: false, error: errorMessage });
|
|
2116
|
+
});
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (type === PANEL_SET_OPTIONS) {
|
|
2121
|
+
bootstrapPromise
|
|
2122
|
+
.then(() => setExtensionOptions(message.options))
|
|
2123
|
+
.then((options) => sendResponse({ ok: true, options }))
|
|
2124
|
+
.catch((error) => {
|
|
2125
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2126
|
+
sendResponse({ ok: false, error: errorMessage });
|
|
2127
|
+
});
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
if (type === CONTENT_PING) {
|
|
2132
|
+
sendResponse({ ok: true, extensionId: chrome.runtime.id });
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
2137
|
+
updateTabMeta(tab);
|
|
2138
|
+
const session = getManagedSessionForTab(tabId);
|
|
2139
|
+
if (!session) {
|
|
2140
|
+
if (changeInfo.status === 'complete' && tab.active === true) {
|
|
2141
|
+
updateRiskBadge(tabId).catch(() => {});
|
|
2142
|
+
}
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (typeof tab.windowId === 'number') {
|
|
2147
|
+
sessionWindowMap.set(session, tab.windowId);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (changeInfo.status === 'complete') {
|
|
2151
|
+
applySessionDomainFallback(tabId, session).catch(() => {});
|
|
2152
|
+
if (tab.active === true) {
|
|
2153
|
+
updateRiskBadge(tabId).catch(() => {});
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
2159
|
+
enforceSessionWindowAffinity(activeInfo.tabId).catch(() => {});
|
|
2160
|
+
updateRiskBadge(activeInfo.tabId).catch(() => {});
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
|
2164
|
+
const session = getManagedSessionForTab(tabId);
|
|
2165
|
+
tabSessionMap.delete(tabId);
|
|
2166
|
+
tabMetaById.delete(tabId);
|
|
2167
|
+
|
|
2168
|
+
if (removeInfo.isWindowClosing) {
|
|
2169
|
+
removeWindowCaches(removeInfo.windowId);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
if (!session) return;
|
|
2174
|
+
const remaining = collectSessionTabIds(session);
|
|
2175
|
+
if (remaining.length === 0) {
|
|
2176
|
+
sessionWindowMap.delete(session);
|
|
2177
|
+
sessionGroupTitleMap.delete(session);
|
|
2178
|
+
}
|
|
2179
|
+
cleanEmptyGroups().catch(() => {});
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
chrome.tabs.onDetached.addListener((tabId) => {
|
|
2183
|
+
const session = getManagedSessionForTab(tabId);
|
|
2184
|
+
if (!session) return;
|
|
2185
|
+
tabMetaById.delete(tabId);
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
chrome.tabs.onAttached.addListener((tabId, attachInfo) => {
|
|
2189
|
+
const session = getManagedSessionForTab(tabId);
|
|
2190
|
+
if (!session) return;
|
|
2191
|
+
sessionWindowMap.set(session, attachInfo.newWindowId);
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
2195
|
+
removeWindowCaches(windowId);
|
|
2196
|
+
cleanEmptyGroups().catch(() => {});
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
2200
|
+
if (!alarm || typeof alarm.name !== 'string') return;
|
|
2201
|
+
|
|
2202
|
+
if (alarm.name === CLEANUP_ALARM_NAME) {
|
|
2203
|
+
cleanEmptyGroups().catch(() => {});
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
if (alarm.name.startsWith(WORKFLOW_ALARM_PREFIX)) {
|
|
2208
|
+
const scheduleId = alarm.name.slice(WORKFLOW_ALARM_PREFIX.length);
|
|
2209
|
+
runSchedule(scheduleId).catch(() => {});
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
2214
|
+
const session = getManagedSessionForTab(item.tabId);
|
|
2215
|
+
if (!session) {
|
|
2216
|
+
suggest();
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const safeSession = sanitizeSegment(session, 'default');
|
|
2221
|
+
const safeFilename = sanitizeFilename(item.filename, `download-${item.id}.bin`);
|
|
2222
|
+
const filename = `${DOWNLOAD_ARCHIVE_ROOT}/${safeSession}/${safeFilename}`;
|
|
2223
|
+
|
|
2224
|
+
recordDownloadEvent({
|
|
2225
|
+
id: item.id,
|
|
2226
|
+
tabId: item.tabId,
|
|
2227
|
+
session,
|
|
2228
|
+
state: 'routing',
|
|
2229
|
+
filename,
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
suggest({
|
|
2233
|
+
filename,
|
|
2234
|
+
conflictAction: 'uniquify',
|
|
2235
|
+
});
|
|
2236
|
+
});
|
|
2237
|
+
|
|
2238
|
+
chrome.downloads.onChanged.addListener((delta) => {
|
|
2239
|
+
if (!delta || typeof delta.id !== 'number') return;
|
|
2240
|
+
|
|
2241
|
+
const state = delta.state?.current;
|
|
2242
|
+
if (!state) return;
|
|
2243
|
+
|
|
2244
|
+
recordDownloadEvent({
|
|
2245
|
+
id: delta.id,
|
|
2246
|
+
state,
|
|
2247
|
+
filename: delta.filename?.current,
|
|
2248
|
+
});
|
|
2249
|
+
});
|