@tomflow/proflow-execution-browser-extension 0.1.11 → 0.1.13
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/CHANGELOG.md +12 -0
- package/DOCS.md +12 -4
- package/SETUP.md +10 -14
- package/dist/deployment/adapter.d.ts +49 -50
- package/dist/deployment/adapter.js +139 -103
- package/dist/deployment/descriptor.d.ts +4 -1
- package/dist/deployment/descriptor.js +5 -1
- package/dist/extension/background.js +189 -0
- package/dist/extension/provisioning-content.d.ts +1 -0
- package/dist/extension/provisioning-content.js +936 -0
- package/dist/src/configure-args.d.ts +3 -0
- package/dist/src/configure-args.js +23 -0
- package/dist/src/configure.js +8 -71
- package/dist/src/custom-gpt-editor-driver.d.ts +51 -0
- package/dist/src/custom-gpt-editor-driver.js +116 -0
- package/dist/src/custom-gpt-knowledge.d.ts +21 -0
- package/dist/src/custom-gpt-knowledge.js +148 -0
- package/dist/src/custom-gpt-provisioner.d.ts +78 -0
- package/dist/src/custom-gpt-provisioner.js +225 -0
- package/dist/src/custom-gpt-role.d.ts +66 -0
- package/dist/src/custom-gpt-role.js +98 -0
- package/dist/src/install-workflow.d.ts +29 -0
- package/dist/src/install-workflow.js +64 -0
- package/dist/src/pairing.d.ts +20 -0
- package/dist/src/pairing.js +176 -0
- package/dist/src/provisioning-bridge.d.ts +46 -0
- package/dist/src/provisioning-bridge.js +314 -0
- package/extension/background.ts +264 -3
- package/extension/provisioning-content.ts +1033 -0
- package/manifest.json +9 -1
- package/package.json +7 -5
- package/proflow.module.json +5 -1
|
@@ -3,6 +3,7 @@ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { deterministicLoopbackPort, ensureModuleSecretFile, moduleWorkspaceStateDirectory, readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
|
|
6
|
+
import { createBrowserExtensionPairingServer } from "../src/pairing.js";
|
|
6
7
|
import { descriptor } from "./descriptor.js";
|
|
7
8
|
const base = {
|
|
8
9
|
contract: "deployment.result.v1",
|
|
@@ -15,48 +16,27 @@ const setupPlan = {
|
|
|
15
16
|
steps: [
|
|
16
17
|
{
|
|
17
18
|
id: "STEP-EXECUTION-BROWSER-EXTENSION-01",
|
|
18
|
-
title: "
|
|
19
|
-
description: "
|
|
19
|
+
title: "加载扩展并自动配对",
|
|
20
|
+
description: "准备 unpacked 目录和本地 Bridge,等待 Chrome Extension 的真实 hello + heartbeat。",
|
|
20
21
|
state: "TODO",
|
|
21
22
|
responsible: "USER",
|
|
22
23
|
execution: {
|
|
23
|
-
interactive: "pnpm exec -- proflow-execution-browser-extension setup
|
|
24
|
-
nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup
|
|
24
|
+
interactive: "pnpm exec -- proflow-execution-browser-extension setup",
|
|
25
|
+
nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup",
|
|
25
26
|
},
|
|
26
27
|
requiredInputs: [],
|
|
27
|
-
verify: "pnpm exec -- proflow-execution-browser-extension setup 01",
|
|
28
|
-
successCondition: "扩展目录已生成并可选择",
|
|
29
|
-
humanAction: "启用开发者模式并选择脚本复制的目录。",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: "STEP-EXECUTION-BROWSER-EXTENSION-02",
|
|
33
|
-
title: "登记 Extension ID 并生成配置",
|
|
34
|
-
description: "保存扩展 ID,生成 Bridge 配置并提示 Reload。",
|
|
35
|
-
state: "TODO",
|
|
36
|
-
responsible: "USER",
|
|
37
|
-
execution: {
|
|
38
|
-
interactive: "pnpm exec -- proflow-execution-browser-extension setup 02",
|
|
39
|
-
nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 02 --extension-id <id>",
|
|
40
|
-
},
|
|
41
|
-
requiredInputs: [
|
|
42
|
-
{
|
|
43
|
-
name: "extensionId",
|
|
44
|
-
description: "Chrome Extension ID",
|
|
45
|
-
sensitive: false,
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
28
|
verify: "pnpm exec -- proflow-execution-browser-extension verify",
|
|
49
|
-
successCondition: "Extension
|
|
50
|
-
humanAction: "
|
|
29
|
+
successCondition: "Extension hello + heartbeat 已被本地 pairing listener 验证并持久化",
|
|
30
|
+
humanAction: "启用开发者模式并加载脚本准备的 unpacked 目录;其余身份发现与验证自动完成。",
|
|
51
31
|
},
|
|
52
32
|
{
|
|
53
|
-
id: "STEP-EXECUTION-BROWSER-EXTENSION-
|
|
54
|
-
title: "
|
|
55
|
-
description: "
|
|
33
|
+
id: "STEP-EXECUTION-BROWSER-EXTENSION-02",
|
|
34
|
+
title: "验证扩展部署状态",
|
|
35
|
+
description: "读取 heartbeat 形成的持久化部署证据。",
|
|
56
36
|
state: "TODO",
|
|
57
37
|
responsible: "AI",
|
|
58
38
|
execution: {
|
|
59
|
-
interactive: "pnpm exec -- proflow-execution-browser-extension
|
|
39
|
+
interactive: "pnpm exec -- proflow-execution-browser-extension verify",
|
|
60
40
|
nonInteractive: "pnpm exec -- proflow-execution-browser-extension verify",
|
|
61
41
|
},
|
|
62
42
|
requiredInputs: [],
|
|
@@ -68,18 +48,18 @@ const setupPlan = {
|
|
|
68
48
|
const blockedSetupPlan = {
|
|
69
49
|
steps: [
|
|
70
50
|
{
|
|
71
|
-
id: "STEP-EXECUTION-BROWSER-EXTENSION-
|
|
72
|
-
title: "
|
|
51
|
+
id: "STEP-EXECUTION-BROWSER-EXTENSION-RECOVER",
|
|
52
|
+
title: "重新加载并自动配对扩展",
|
|
73
53
|
state: "BLOCKED",
|
|
74
54
|
responsible: "EXTERNAL",
|
|
75
55
|
execution: {
|
|
76
56
|
interactive: "pnpm exec -- proflow-execution-browser-extension setup",
|
|
77
|
-
nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup
|
|
57
|
+
nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup",
|
|
78
58
|
},
|
|
79
59
|
requiredInputs: [],
|
|
80
60
|
verify: "pnpm exec -- proflow-execution-browser-extension verify",
|
|
81
61
|
successCondition: "配置状态变为“已就绪”",
|
|
82
|
-
blockedReason: "Extension
|
|
62
|
+
blockedReason: "真实 Extension heartbeat 未到达或本地 pairing 失败",
|
|
83
63
|
},
|
|
84
64
|
],
|
|
85
65
|
};
|
|
@@ -176,7 +156,10 @@ async function readEvidence(context, loadDir) {
|
|
|
176
156
|
raw.loadDir === loadDir &&
|
|
177
157
|
typeof raw.extensionId === "string" &&
|
|
178
158
|
/^[a-z]{32}$/.test(raw.extensionId) &&
|
|
159
|
+
typeof raw.extensionInstanceId === "string" &&
|
|
160
|
+
raw.extensionInstanceId.length > 0 &&
|
|
179
161
|
raw.serviceWorker === "RUNNING" &&
|
|
162
|
+
raw.evidenceSource === "PAIRING_HEARTBEAT" &&
|
|
180
163
|
typeof raw.observedAt === "string" &&
|
|
181
164
|
!Number.isNaN(Date.parse(raw.observedAt))
|
|
182
165
|
? raw
|
|
@@ -186,28 +169,19 @@ async function readEvidence(context, loadDir) {
|
|
|
186
169
|
return undefined;
|
|
187
170
|
}
|
|
188
171
|
}
|
|
189
|
-
function input(context) {
|
|
190
|
-
const value = context.input;
|
|
191
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
192
|
-
return {};
|
|
193
|
-
const extensionId = Reflect.get(value, "extensionId"), serviceWorker = Reflect.get(value, "serviceWorker"), observedAt = Reflect.get(value, "observedAt");
|
|
194
|
-
return {
|
|
195
|
-
...(typeof extensionId === "string" ? { extensionId } : {}),
|
|
196
|
-
...(serviceWorker === "RUNNING"
|
|
197
|
-
? { serviceWorker: "RUNNING" }
|
|
198
|
-
: {}),
|
|
199
|
-
...(typeof observedAt === "string" ? { observedAt } : {}),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
172
|
async function ownFacts(context) {
|
|
203
173
|
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
204
174
|
const loadDir = browserExtensionLoadDir(context.workspaceRoot);
|
|
205
175
|
const bridgeTokenFile = await ensureModuleSecretFile(context, descriptor.moduleRef, "bridge");
|
|
206
176
|
const bridgePort = deterministicLoopbackPort(context, descriptor.moduleRef, "bridge");
|
|
177
|
+
const provisioningBridgeTokenFile = await ensureModuleSecretFile(context, descriptor.moduleRef, "provisioning");
|
|
178
|
+
const provisioningBridgePort = deterministicLoopbackPort(context, descriptor.moduleRef, "provisioning");
|
|
207
179
|
const facts = {
|
|
208
180
|
loadDir,
|
|
209
181
|
bridgeTokenFile,
|
|
210
182
|
bridgeEndpoint: `http://127.0.0.1:${bridgePort}`,
|
|
183
|
+
provisioningBridgeTokenFile,
|
|
184
|
+
provisioningBridgeEndpoint: `http://127.0.0.1:${provisioningBridgePort}`,
|
|
211
185
|
verificationEvidenceFile: verificationFile(context),
|
|
212
186
|
};
|
|
213
187
|
const setup = await readSetup(context);
|
|
@@ -221,7 +195,7 @@ async function ownFacts(context) {
|
|
|
221
195
|
await writeModuleSharedFacts(context, descriptor.moduleRef, facts);
|
|
222
196
|
return facts;
|
|
223
197
|
}
|
|
224
|
-
async function
|
|
198
|
+
async function materializeRuntimeConfig(context) {
|
|
225
199
|
const host = await readModuleSharedFacts(context, "platform-host");
|
|
226
200
|
const endpoint = factString(host, "endpoint"), taskTokenFile = factString(host, "taskApplicationTokenFile"), approvalTokenFile = factString(host, "approvalApplicationTokenFile");
|
|
227
201
|
if (!endpoint || !taskTokenFile || !approvalTokenFile)
|
|
@@ -230,11 +204,17 @@ async function materialize(context, setup) {
|
|
|
230
204
|
const loadDir = String(facts.loadDir);
|
|
231
205
|
const bridgeTokenFile = String(facts.bridgeTokenFile);
|
|
232
206
|
const bridgeEndpoint = String(facts.bridgeEndpoint);
|
|
233
|
-
const
|
|
207
|
+
const provisioningBridgeTokenFile = String(facts.provisioningBridgeTokenFile);
|
|
208
|
+
const provisioningBridgeEndpoint = String(facts.provisioningBridgeEndpoint);
|
|
209
|
+
await writeFile(join(loadDir, "runtime-config.json"), `${JSON.stringify({
|
|
234
210
|
proflowRuntimeBridge: {
|
|
235
211
|
endpoint: bridgeEndpoint,
|
|
236
212
|
token: await credential(bridgeTokenFile),
|
|
237
213
|
},
|
|
214
|
+
proflowProvisioningBridge: {
|
|
215
|
+
endpoint: provisioningBridgeEndpoint,
|
|
216
|
+
token: await credential(provisioningBridgeTokenFile),
|
|
217
|
+
},
|
|
238
218
|
proflowTaskApplication: {
|
|
239
219
|
endpoint,
|
|
240
220
|
token: await credential(taskTokenFile),
|
|
@@ -243,15 +223,91 @@ async function materialize(context, setup) {
|
|
|
243
223
|
endpoint,
|
|
244
224
|
token: await credential(approvalTokenFile),
|
|
245
225
|
},
|
|
226
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
227
|
+
return {
|
|
228
|
+
facts,
|
|
229
|
+
loadDir,
|
|
230
|
+
bridgeTokenFile,
|
|
231
|
+
bridgeEndpoint,
|
|
232
|
+
provisioningBridgeTokenFile,
|
|
233
|
+
provisioningBridgeEndpoint,
|
|
234
|
+
endpoint,
|
|
235
|
+
taskTokenFile,
|
|
246
236
|
};
|
|
247
|
-
|
|
248
|
-
|
|
237
|
+
}
|
|
238
|
+
async function materializeExecutorConfig(context, setup, prepared) {
|
|
239
|
+
const resolved = prepared ?? (await materializeRuntimeConfig(context));
|
|
240
|
+
await writeFile(executorConfigFile(context), `${JSON.stringify({ platformHost: { endpoint: resolved.endpoint, tokenFile: resolved.taskTokenFile }, bridge: { extensionId: setup.extensionId, tokenFile: resolved.bridgeTokenFile, host: "127.0.0.1", port: Number(new URL(resolved.bridgeEndpoint).port) } }, null, 2)}\n`, { mode: 0o600 });
|
|
249
241
|
await writeModuleSharedFacts(context, descriptor.moduleRef, {
|
|
250
|
-
...facts,
|
|
242
|
+
...resolved.facts,
|
|
251
243
|
extensionId: setup.extensionId,
|
|
252
244
|
browserExecutorConfigPath: executorConfigFile(context),
|
|
253
245
|
});
|
|
254
246
|
}
|
|
247
|
+
async function materialize(context, setup) {
|
|
248
|
+
const prepared = await materializeRuntimeConfig(context);
|
|
249
|
+
await materializeExecutorConfig(context, setup, prepared);
|
|
250
|
+
}
|
|
251
|
+
async function persistPairedBrowserExtension(context, prepared, identity) {
|
|
252
|
+
const setup = { extensionId: identity.extensionId };
|
|
253
|
+
await writeFile(setupFile(context), `${JSON.stringify(setup, null, 2)}\n`, {
|
|
254
|
+
mode: 0o600,
|
|
255
|
+
});
|
|
256
|
+
await materializeExecutorConfig(context, setup, prepared);
|
|
257
|
+
const evidence = {
|
|
258
|
+
contract: "proflow.browser-extension-verification.v1",
|
|
259
|
+
moduleVersion: descriptor.moduleVersion,
|
|
260
|
+
loadDir: prepared.loadDir,
|
|
261
|
+
extensionId: identity.extensionId,
|
|
262
|
+
extensionInstanceId: identity.extensionInstanceId,
|
|
263
|
+
serviceWorker: "RUNNING",
|
|
264
|
+
evidenceSource: "PAIRING_HEARTBEAT",
|
|
265
|
+
observedAt: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
await writeFile(verificationFile(context), `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
|
|
268
|
+
return identity;
|
|
269
|
+
}
|
|
270
|
+
export async function pairBrowserExtensionSetup(context, options = {}) {
|
|
271
|
+
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
272
|
+
await installPackage(context);
|
|
273
|
+
const existingSetup = await readSetup(context);
|
|
274
|
+
const existingEvidence = await readEvidence(context, browserExtensionLoadDir(context.workspaceRoot));
|
|
275
|
+
const canRevalidateExisting = Boolean(existingSetup &&
|
|
276
|
+
existingEvidence &&
|
|
277
|
+
existingSetup.extensionId === existingEvidence.extensionId);
|
|
278
|
+
const prepared = await materializeRuntimeConfig(context);
|
|
279
|
+
const pairing = await createBrowserExtensionPairingServer({
|
|
280
|
+
token: await credential(prepared.bridgeTokenFile),
|
|
281
|
+
host: "127.0.0.1",
|
|
282
|
+
port: Number(new URL(prepared.bridgeEndpoint).port),
|
|
283
|
+
pairingTimeoutMs: options.timeoutMs ?? 120_000,
|
|
284
|
+
});
|
|
285
|
+
try {
|
|
286
|
+
const pairingPromise = pairing.waitForPairing();
|
|
287
|
+
if (canRevalidateExisting) {
|
|
288
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
289
|
+
const revalidationGraceMs = Math.min(2_000, Math.max(25, Math.floor(timeoutMs / 4)));
|
|
290
|
+
const revalidated = await Promise.race([
|
|
291
|
+
pairingPromise.then((identity) => ({
|
|
292
|
+
kind: "PAIRED",
|
|
293
|
+
identity,
|
|
294
|
+
})),
|
|
295
|
+
new Promise((resolve) => setTimeout(() => resolve({ kind: "WAITING" }), revalidationGraceMs)),
|
|
296
|
+
]);
|
|
297
|
+
if (revalidated.kind === "PAIRED") {
|
|
298
|
+
return persistPairedBrowserExtension(context, prepared, revalidated.identity);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await options.onWaiting?.({
|
|
302
|
+
loadDir: prepared.loadDir,
|
|
303
|
+
endpoint: pairing.endpoint,
|
|
304
|
+
});
|
|
305
|
+
return persistPairedBrowserExtension(context, prepared, await pairingPromise);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await pairing.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
255
311
|
const failed = (code, message, retryable = true) => ({
|
|
256
312
|
...base,
|
|
257
313
|
ok: false,
|
|
@@ -274,14 +330,28 @@ export const behaviorAdapter = {
|
|
|
274
330
|
const loadDir = browserExtensionLoadDir(context.workspaceRoot);
|
|
275
331
|
const setup = await readSetup(context);
|
|
276
332
|
const evidence = await readEvidence(context, loadDir);
|
|
333
|
+
const setupReady = Boolean(setup && evidence && setup.extensionId === evidence.extensionId);
|
|
277
334
|
return {
|
|
278
335
|
result: {
|
|
279
336
|
...base,
|
|
280
337
|
data: {
|
|
281
|
-
setupStatus:
|
|
338
|
+
setupStatus: setupReady
|
|
282
339
|
? "READY"
|
|
283
340
|
: "ACTION_REQUIRED",
|
|
284
341
|
runtimeStatus: evidence ? "RUNNING" : "STOPPED",
|
|
342
|
+
...(setupReady
|
|
343
|
+
? {}
|
|
344
|
+
: {
|
|
345
|
+
issues: [
|
|
346
|
+
{
|
|
347
|
+
scope: "SETUP",
|
|
348
|
+
code: "EXTENSION_LOAD_REQUIRED",
|
|
349
|
+
message: "Chrome 扩展尚未加载或缺少可验证的运行证据",
|
|
350
|
+
relatedModuleRefs: ["chrome-runtime"],
|
|
351
|
+
nextCommand: "platform setup --module execution-browser-extension",
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
}),
|
|
285
355
|
},
|
|
286
356
|
},
|
|
287
357
|
observedEffects: evidence
|
|
@@ -291,61 +361,27 @@ export const behaviorAdapter = {
|
|
|
291
361
|
},
|
|
292
362
|
setup: async (context) => {
|
|
293
363
|
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
364
|
+
await installPackage(context);
|
|
365
|
+
const setup = await readSetup(context);
|
|
366
|
+
const evidence = await readEvidence(context, browserExtensionLoadDir(context.workspaceRoot));
|
|
367
|
+
if (setup && evidence && setup.extensionId === evidence.extensionId) {
|
|
368
|
+
try {
|
|
369
|
+
await materialize(context, setup);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
298
372
|
return {
|
|
299
373
|
result: {
|
|
300
|
-
...failed("SETUP_FAILED",
|
|
374
|
+
...failed("SETUP_FAILED", error instanceof Error
|
|
375
|
+
? error.message
|
|
376
|
+
: "browser extension config materialization failed"),
|
|
301
377
|
data: blockedSetupPlan,
|
|
302
378
|
},
|
|
303
379
|
observedEffects: [],
|
|
304
380
|
};
|
|
305
|
-
|
|
306
|
-
await writeFile(setupFile(context), `${JSON.stringify(setup, null, 2)}\n`, { mode: 0o600 });
|
|
307
|
-
}
|
|
308
|
-
if (!setup)
|
|
309
|
-
return {
|
|
310
|
-
result: {
|
|
311
|
-
...base,
|
|
312
|
-
ok: false,
|
|
313
|
-
status: "ACTION_REQUIRED",
|
|
314
|
-
data: setupPlan,
|
|
315
|
-
actionRequired: {
|
|
316
|
-
action: "load-unpacked-extension",
|
|
317
|
-
description: `Load ${browserExtensionLoadDir(context.workspaceRoot)} in Chrome, copy its extensionId, then run proflow-execution-browser-extension setup --extension-id <id>.`,
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
observedEffects: [],
|
|
321
|
-
};
|
|
322
|
-
try {
|
|
323
|
-
await materialize(context, setup);
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
return {
|
|
327
|
-
result: {
|
|
328
|
-
...failed("SETUP_FAILED", error instanceof Error
|
|
329
|
-
? error.message
|
|
330
|
-
: "browser extension config materialization failed"),
|
|
331
|
-
data: blockedSetupPlan,
|
|
332
|
-
},
|
|
333
|
-
observedEffects: [],
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (supplied.serviceWorker === "RUNNING") {
|
|
337
|
-
const evidence = {
|
|
338
|
-
contract: "proflow.browser-extension-verification.v1",
|
|
339
|
-
moduleVersion: descriptor.moduleVersion,
|
|
340
|
-
loadDir: browserExtensionLoadDir(context.workspaceRoot),
|
|
341
|
-
extensionId: setup.extensionId,
|
|
342
|
-
serviceWorker: "RUNNING",
|
|
343
|
-
observedAt: supplied.observedAt ?? new Date().toISOString(),
|
|
344
|
-
};
|
|
345
|
-
await writeFile(verificationFile(context), `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
|
|
381
|
+
}
|
|
346
382
|
return {
|
|
347
383
|
result: base,
|
|
348
|
-
observedEffects: ["
|
|
384
|
+
observedEffects: ["Uses heartbeat-proven Chrome MV3 pairing evidence"],
|
|
349
385
|
};
|
|
350
386
|
}
|
|
351
387
|
return {
|
|
@@ -355,8 +391,8 @@ export const behaviorAdapter = {
|
|
|
355
391
|
status: "ACTION_REQUIRED",
|
|
356
392
|
data: setupPlan,
|
|
357
393
|
actionRequired: {
|
|
358
|
-
action: "
|
|
359
|
-
description:
|
|
394
|
+
action: "load-unpacked-extension",
|
|
395
|
+
description: `Run pnpm exec -- proflow-execution-browser-extension setup --workspace ${context.workspaceRoot}; then enable Developer Mode and load ${browserExtensionLoadDir(context.workspaceRoot)}. The package CLI materializes bootstrap config, discovers the extension and verifies its heartbeat automatically.`,
|
|
360
396
|
},
|
|
361
397
|
},
|
|
362
398
|
observedEffects: [],
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "execution-browser-extension";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-execution-browser-extension";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.13";
|
|
7
7
|
readonly kind: "browser-extension";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -14,6 +14,9 @@ export declare const descriptor: {
|
|
|
14
14
|
readonly provides: readonly [{
|
|
15
15
|
readonly contractRef: "execution-browser-executor";
|
|
16
16
|
readonly version: "1.0.0";
|
|
17
|
+
}, {
|
|
18
|
+
readonly contractRef: "custom-gpt-web-provisioning";
|
|
19
|
+
readonly version: "1.0.0";
|
|
17
20
|
}];
|
|
18
21
|
readonly requires: readonly [{
|
|
19
22
|
readonly contractRef: "execution";
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "execution-browser-extension",
|
|
5
5
|
packageName: "@tomflow/proflow-execution-browser-extension",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.13",
|
|
7
7
|
kind: "browser-extension",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -16,6 +16,10 @@ export const descriptor = {
|
|
|
16
16
|
contractRef: "execution-browser-executor",
|
|
17
17
|
version: "1.0.0",
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
contractRef: "custom-gpt-web-provisioning",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
},
|
|
19
23
|
],
|
|
20
24
|
requires: [
|
|
21
25
|
{
|
|
@@ -13912,6 +13912,7 @@ async function bootstrapManagedRuntimeConfig() {
|
|
|
13912
13912
|
if (!isRecord(raw)) return;
|
|
13913
13913
|
const managed = raw;
|
|
13914
13914
|
const bridge = parseConfig(managed.proflowRuntimeBridge);
|
|
13915
|
+
const provisioning = parseConfig(managed.proflowProvisioningBridge);
|
|
13915
13916
|
const task = parseConfig(managed.proflowTaskApplication);
|
|
13916
13917
|
const approval = parseConfig(managed.proflowApprovalApplication);
|
|
13917
13918
|
if (!bridge || !task || !approval) {
|
|
@@ -13919,6 +13920,7 @@ async function bootstrapManagedRuntimeConfig() {
|
|
|
13919
13920
|
}
|
|
13920
13921
|
await chrome.storage.local.set({
|
|
13921
13922
|
proflowRuntimeBridge: bridge,
|
|
13923
|
+
...provisioning ? { proflowProvisioningBridge: provisioning } : {},
|
|
13922
13924
|
proflowTaskApplication: task,
|
|
13923
13925
|
proflowApprovalApplication: approval
|
|
13924
13926
|
});
|
|
@@ -13927,6 +13929,10 @@ async function bridgeConfig() {
|
|
|
13927
13929
|
const stored = await chrome.storage.local.get("proflowRuntimeBridge");
|
|
13928
13930
|
return parseConfig(stored.proflowRuntimeBridge);
|
|
13929
13931
|
}
|
|
13932
|
+
async function provisioningBridgeConfig() {
|
|
13933
|
+
const stored = await chrome.storage.local.get("proflowProvisioningBridge");
|
|
13934
|
+
return parseConfig(stored.proflowProvisioningBridge);
|
|
13935
|
+
}
|
|
13930
13936
|
async function taskApplicationConfig() {
|
|
13931
13937
|
const stored = await chrome.storage.local.get("proflowTaskApplication");
|
|
13932
13938
|
return parseConfig(stored.proflowTaskApplication);
|
|
@@ -14379,6 +14385,68 @@ async function executeCommand(command) {
|
|
|
14379
14385
|
}
|
|
14380
14386
|
throw new Error("BROWSER_PRIMITIVE_UNAVAILABLE");
|
|
14381
14387
|
}
|
|
14388
|
+
function missingProvisioningReceiver(error46) {
|
|
14389
|
+
const message = error46 instanceof Error ? error46.message : String(error46);
|
|
14390
|
+
return message.includes("Could not establish connection") || message.includes("Receiving end does not exist");
|
|
14391
|
+
}
|
|
14392
|
+
function isEditorUrl(url2) {
|
|
14393
|
+
return url2 === "https://chatgpt.com/gpts/editor" || Boolean(url2?.startsWith("https://chatgpt.com/gpts/editor/"));
|
|
14394
|
+
}
|
|
14395
|
+
async function provisioningContentCommand(tabId, operation, request) {
|
|
14396
|
+
let receiverReloaded = false;
|
|
14397
|
+
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
14398
|
+
let response;
|
|
14399
|
+
try {
|
|
14400
|
+
response = await chrome.tabs.sendMessage(tabId, {
|
|
14401
|
+
type: "PROFLOW_PROVISIONING_COMMAND",
|
|
14402
|
+
operation,
|
|
14403
|
+
request
|
|
14404
|
+
});
|
|
14405
|
+
} catch (error46) {
|
|
14406
|
+
if (!receiverReloaded && missingProvisioningReceiver(error46)) {
|
|
14407
|
+
try {
|
|
14408
|
+
const tab = await chrome.tabs.get(tabId);
|
|
14409
|
+
if (tab.status === "complete" && isEditorUrl(tab.url)) {
|
|
14410
|
+
await chrome.tabs.reload(tabId);
|
|
14411
|
+
receiverReloaded = true;
|
|
14412
|
+
}
|
|
14413
|
+
} catch {
|
|
14414
|
+
}
|
|
14415
|
+
}
|
|
14416
|
+
if (attempt === 59) throw error46;
|
|
14417
|
+
await sleep(250);
|
|
14418
|
+
continue;
|
|
14419
|
+
}
|
|
14420
|
+
if (!isRecord(response) || response.ok !== true) {
|
|
14421
|
+
const detail = isRecord(response) && typeof response.error === "string" ? response.error : "PROVISIONING_CONTENT_FAILED";
|
|
14422
|
+
if ((detail === "PROVISIONING_SURFACE_NOT_READY" || detail === "GPT_EDITOR_CONFIGURE_SURFACE_NOT_READY") && attempt < 59) {
|
|
14423
|
+
await sleep(250);
|
|
14424
|
+
continue;
|
|
14425
|
+
}
|
|
14426
|
+
throw new Error(detail);
|
|
14427
|
+
}
|
|
14428
|
+
return response.value;
|
|
14429
|
+
}
|
|
14430
|
+
throw new Error("PROVISIONING_CONTENT_TIMEOUT");
|
|
14431
|
+
}
|
|
14432
|
+
async function executeProvisioningCommand(command) {
|
|
14433
|
+
if (!isRecord(command.request))
|
|
14434
|
+
throw new Error("PROVISIONING_COMMAND_INVALID");
|
|
14435
|
+
let editorUrl = "https://chatgpt.com/gpts/editor";
|
|
14436
|
+
if (command.type === "FINALIZE_CUSTOM_GPT_AUTH") {
|
|
14437
|
+
const carrierUrl = new URL(text(command.request.carrierUrl, "CARRIER_URL"));
|
|
14438
|
+
const match = /^\/g\/(g-[A-Za-z0-9_-]+)$/.exec(carrierUrl.pathname);
|
|
14439
|
+
if (carrierUrl.origin !== "https://chatgpt.com" || carrierUrl.username !== "" || carrierUrl.password !== "" || carrierUrl.search !== "" || carrierUrl.hash !== "" || !match?.[1])
|
|
14440
|
+
throw new Error("PROVISIONING_CARRIER_URL_INVALID");
|
|
14441
|
+
editorUrl = `https://chatgpt.com/gpts/editor/${match[1]}`;
|
|
14442
|
+
}
|
|
14443
|
+
const tab = await chrome.tabs.create({ url: editorUrl, active: true });
|
|
14444
|
+
return provisioningContentCommand(
|
|
14445
|
+
numeric(tab.id, "TAB_ID"),
|
|
14446
|
+
command.type,
|
|
14447
|
+
command.request
|
|
14448
|
+
);
|
|
14449
|
+
}
|
|
14382
14450
|
async function bridgeFetch(config2, path, init = {}) {
|
|
14383
14451
|
return fetch(`${config2.endpoint}${path}`, {
|
|
14384
14452
|
...init,
|
|
@@ -14471,7 +14539,127 @@ async function runBridgeLoop() {
|
|
|
14471
14539
|
}
|
|
14472
14540
|
}
|
|
14473
14541
|
}
|
|
14542
|
+
var provisioningBridgeLoopStarted = false;
|
|
14543
|
+
async function runProvisioningBridgeLoop() {
|
|
14544
|
+
if (provisioningBridgeLoopStarted) return;
|
|
14545
|
+
provisioningBridgeLoopStarted = true;
|
|
14546
|
+
while (true) {
|
|
14547
|
+
const config2 = await provisioningBridgeConfig();
|
|
14548
|
+
if (!config2) {
|
|
14549
|
+
await sleep(1e3);
|
|
14550
|
+
continue;
|
|
14551
|
+
}
|
|
14552
|
+
const query = `?extensionInstanceId=${encodeURIComponent(extensionInstanceId)}`;
|
|
14553
|
+
try {
|
|
14554
|
+
const hello = await bridgeFetch(
|
|
14555
|
+
config2,
|
|
14556
|
+
"/v1/provisioning/session/hello",
|
|
14557
|
+
{
|
|
14558
|
+
method: "POST",
|
|
14559
|
+
body: JSON.stringify({
|
|
14560
|
+
extensionId: chrome.runtime.id,
|
|
14561
|
+
extensionInstanceId
|
|
14562
|
+
})
|
|
14563
|
+
}
|
|
14564
|
+
);
|
|
14565
|
+
if (!hello.ok) throw new Error("PROVISIONING_BRIDGE_HELLO_REJECTED");
|
|
14566
|
+
let lastHeartbeatAt = 0;
|
|
14567
|
+
while (true) {
|
|
14568
|
+
if (Date.now() - lastHeartbeatAt >= 5e3) {
|
|
14569
|
+
const heartbeat = await bridgeFetch(
|
|
14570
|
+
config2,
|
|
14571
|
+
`/v1/provisioning/session/heartbeat${query}`,
|
|
14572
|
+
{ method: "POST", body: "{}" }
|
|
14573
|
+
);
|
|
14574
|
+
if (!heartbeat.ok)
|
|
14575
|
+
throw new Error("PROVISIONING_BRIDGE_HEARTBEAT_REJECTED");
|
|
14576
|
+
lastHeartbeatAt = Date.now();
|
|
14577
|
+
}
|
|
14578
|
+
const response = await bridgeFetch(
|
|
14579
|
+
config2,
|
|
14580
|
+
`/v1/provisioning/commands/next${query}`
|
|
14581
|
+
);
|
|
14582
|
+
if (response.status === 204) {
|
|
14583
|
+
await sleep(250);
|
|
14584
|
+
continue;
|
|
14585
|
+
}
|
|
14586
|
+
if (!response.ok) throw new Error("PROVISIONING_BRIDGE_POLL_REJECTED");
|
|
14587
|
+
const command = await response.json();
|
|
14588
|
+
const commandHeartbeat = setInterval(() => {
|
|
14589
|
+
void bridgeFetch(
|
|
14590
|
+
config2,
|
|
14591
|
+
`/v1/provisioning/session/heartbeat${query}`,
|
|
14592
|
+
{ method: "POST", body: "{}" }
|
|
14593
|
+
).catch(() => void 0);
|
|
14594
|
+
}, 2e3);
|
|
14595
|
+
let result;
|
|
14596
|
+
try {
|
|
14597
|
+
result = {
|
|
14598
|
+
commandId: command.commandId,
|
|
14599
|
+
ok: true,
|
|
14600
|
+
value: await executeProvisioningCommand(command)
|
|
14601
|
+
};
|
|
14602
|
+
} catch (error46) {
|
|
14603
|
+
result = {
|
|
14604
|
+
commandId: command.commandId,
|
|
14605
|
+
ok: false,
|
|
14606
|
+
error: error46 instanceof Error ? error46.message : "PROVISIONING_EXTENSION_COMMAND_FAILED"
|
|
14607
|
+
};
|
|
14608
|
+
} finally {
|
|
14609
|
+
clearInterval(commandHeartbeat);
|
|
14610
|
+
}
|
|
14611
|
+
const reported = await bridgeFetch(
|
|
14612
|
+
config2,
|
|
14613
|
+
`/v1/provisioning/commands/result${query}`,
|
|
14614
|
+
{ method: "POST", body: JSON.stringify(result) }
|
|
14615
|
+
);
|
|
14616
|
+
if (!reported.ok)
|
|
14617
|
+
throw new Error("PROVISIONING_BRIDGE_RESULT_REJECTED");
|
|
14618
|
+
}
|
|
14619
|
+
} catch {
|
|
14620
|
+
await sleep(1e3);
|
|
14621
|
+
}
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
function provisioningRelayBase64(bytes) {
|
|
14625
|
+
let binary = "";
|
|
14626
|
+
for (let offset = 0; offset < bytes.length; offset += 32768)
|
|
14627
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
14628
|
+
return btoa(binary);
|
|
14629
|
+
}
|
|
14474
14630
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
14631
|
+
if (message.type === "PROFLOW_PROVISIONING_RELAY_FETCH") {
|
|
14632
|
+
const rawUrl = typeof message.url === "string" ? message.url : "";
|
|
14633
|
+
let parsed;
|
|
14634
|
+
try {
|
|
14635
|
+
parsed = new URL(rawUrl);
|
|
14636
|
+
} catch {
|
|
14637
|
+
sendResponse({ ok: false, error: "KNOWLEDGE_RELAY_URL_INVALID" });
|
|
14638
|
+
return;
|
|
14639
|
+
}
|
|
14640
|
+
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1" || !parsed.pathname.startsWith("/v1/provisioning/files/") || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") {
|
|
14641
|
+
sendResponse({ ok: false, error: "KNOWLEDGE_RELAY_URL_INVALID" });
|
|
14642
|
+
return;
|
|
14643
|
+
}
|
|
14644
|
+
void fetch(parsed.toString(), { cache: "no-store" }).then(
|
|
14645
|
+
async (response) => {
|
|
14646
|
+
if (!response.ok) {
|
|
14647
|
+
sendResponse({
|
|
14648
|
+
ok: false,
|
|
14649
|
+
error: `KNOWLEDGE_RELAY_FETCH_FAILED:${response.status}`
|
|
14650
|
+
});
|
|
14651
|
+
return;
|
|
14652
|
+
}
|
|
14653
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
14654
|
+
sendResponse({ ok: true, base64: provisioningRelayBase64(bytes) });
|
|
14655
|
+
},
|
|
14656
|
+
(error46) => sendResponse({
|
|
14657
|
+
ok: false,
|
|
14658
|
+
error: error46 instanceof Error ? `KNOWLEDGE_RELAY_FETCH_FAILED:${error46.message}` : "KNOWLEDGE_RELAY_FETCH_FAILED"
|
|
14659
|
+
})
|
|
14660
|
+
);
|
|
14661
|
+
return true;
|
|
14662
|
+
}
|
|
14475
14663
|
if (message.type === "PROFLOW_CONTENT_OBSERVATION" && message.observation && sender.tab?.id !== void 0 && sender.tab.windowId !== void 0) {
|
|
14476
14664
|
const observed = {
|
|
14477
14665
|
...message.observation,
|
|
@@ -14554,6 +14742,7 @@ async function startBackgroundRuntime() {
|
|
|
14554
14742
|
await bootstrapManagedRuntimeConfig();
|
|
14555
14743
|
await persistSnapshot();
|
|
14556
14744
|
void runBridgeLoop();
|
|
14745
|
+
void runProvisioningBridgeLoop();
|
|
14557
14746
|
void runObserverRecovery();
|
|
14558
14747
|
}
|
|
14559
14748
|
chrome.runtime.onInstalled.addListener(() => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|