@tomflow/proflow-execution-browser-extension 0.1.37 → 0.1.39
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/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/extension/background.js +687 -19
- package/dist/extension/content.js +267 -28
- package/dist/extension/tasks.js +74 -3
- package/dist/src/bridge.js +59 -6
- package/dist/src/carrier-attention-view.d.ts +13 -0
- package/dist/src/carrier-attention-view.js +54 -0
- package/dist/src/carrier-attention.d.ts +26 -0
- package/dist/src/carrier-attention.js +44 -0
- package/dist/src/carrier-continuation-control.d.ts +44 -0
- package/dist/src/carrier-continuation-control.js +114 -0
- package/dist/src/carrier-permission-attempt.d.ts +18 -0
- package/dist/src/carrier-permission-attempt.js +68 -0
- package/dist/src/carrier-permission-lifecycle.d.ts +34 -0
- package/dist/src/carrier-permission-lifecycle.js +73 -0
- package/dist/src/carrier-permission.d.ts +16 -0
- package/dist/src/carrier-permission.js +65 -0
- package/dist/src/chatgpt-runtime-adapter.d.ts +16 -0
- package/dist/src/chatgpt-runtime-adapter.js +173 -0
- package/dist/src/composer-submit.d.ts +12 -0
- package/dist/src/composer-submit.js +22 -0
- package/dist/src/pairing.js +13 -0
- package/extension/background.ts +622 -17
- package/extension/content.ts +40 -39
- package/extension/tasks.html +6 -0
- package/extension/tasks.ts +93 -5
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/proflow.module.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function createCarrierAttentionRegistry(idFactory = () => crypto.randomUUID()) {
|
|
2
|
+
const byTab = new Map();
|
|
3
|
+
return Object.freeze({
|
|
4
|
+
derive(input) {
|
|
5
|
+
const current = byTab.get(input.tabId);
|
|
6
|
+
if (current?.contentInstanceId === input.contentInstanceId &&
|
|
7
|
+
current.url === input.url &&
|
|
8
|
+
current.permissionFingerprint === input.permissionFingerprint) {
|
|
9
|
+
const refreshed = { ...current, ...input };
|
|
10
|
+
byTab.set(input.tabId, refreshed);
|
|
11
|
+
return refreshed;
|
|
12
|
+
}
|
|
13
|
+
const occurrenceRef = idFactory();
|
|
14
|
+
const attention = {
|
|
15
|
+
...input,
|
|
16
|
+
occurrenceRef,
|
|
17
|
+
attentionRef: `carrier-attention:${input.tabId}:${occurrenceRef}`,
|
|
18
|
+
};
|
|
19
|
+
byTab.set(input.tabId, attention);
|
|
20
|
+
return attention;
|
|
21
|
+
},
|
|
22
|
+
find(attentionRef) {
|
|
23
|
+
for (const attention of byTab.values())
|
|
24
|
+
if (attention.attentionRef === attentionRef)
|
|
25
|
+
return attention;
|
|
26
|
+
return null;
|
|
27
|
+
},
|
|
28
|
+
current(tabId) {
|
|
29
|
+
return byTab.get(tabId) ?? null;
|
|
30
|
+
},
|
|
31
|
+
values() {
|
|
32
|
+
return [...byTab.values()];
|
|
33
|
+
},
|
|
34
|
+
delete(attentionRef) {
|
|
35
|
+
for (const [tabId, attention] of byTab)
|
|
36
|
+
if (attention.attentionRef === attentionRef)
|
|
37
|
+
return byTab.delete(tabId);
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
removeTab(tabId) {
|
|
41
|
+
return byTab.delete(tabId);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CarrierContinuationDenial = {
|
|
2
|
+
attentionRef: string;
|
|
3
|
+
tabId: number;
|
|
4
|
+
taskId: string | null;
|
|
5
|
+
roleRef: string | null;
|
|
6
|
+
workerRef: string | null;
|
|
7
|
+
url: string;
|
|
8
|
+
contentInstanceId: string;
|
|
9
|
+
permissionFingerprint: string;
|
|
10
|
+
};
|
|
11
|
+
export type CarrierRecoveryObservation = {
|
|
12
|
+
tabId: number;
|
|
13
|
+
url: string;
|
|
14
|
+
contentInstanceId?: string;
|
|
15
|
+
pageState: "IDLE" | "BUSY" | "BLOCKED" | "UNKNOWN";
|
|
16
|
+
blockerFacts?: {
|
|
17
|
+
fingerprint: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type CarrierPermissionDenialContext = {
|
|
21
|
+
tabId: number;
|
|
22
|
+
contentInstanceId: string;
|
|
23
|
+
url: string;
|
|
24
|
+
permissionFingerprint: string;
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
roleRef: string | null;
|
|
27
|
+
workerRef: string | null;
|
|
28
|
+
};
|
|
29
|
+
export type CarrierDispatchDenialContext = {
|
|
30
|
+
taskId: string;
|
|
31
|
+
roleRef: string;
|
|
32
|
+
workerRef: string;
|
|
33
|
+
conversationLocator: string | null;
|
|
34
|
+
};
|
|
35
|
+
export declare function createCarrierContinuationControl(initial?: unknown): Readonly<{
|
|
36
|
+
beginDenied(denial: CarrierContinuationDenial): void;
|
|
37
|
+
cancelDenied(attentionRef: string): boolean;
|
|
38
|
+
hasMatchingPermissionDenial(context: CarrierPermissionDenialContext): boolean;
|
|
39
|
+
hasMatchingDispatchDenial(context: CarrierDispatchDenialContext): boolean;
|
|
40
|
+
consumeRecovery(previous: CarrierRecoveryObservation | undefined, current: CarrierRecoveryObservation): CarrierContinuationDenial | null;
|
|
41
|
+
suppressRecovery(previous: CarrierRecoveryObservation | undefined, current: CarrierRecoveryObservation): boolean;
|
|
42
|
+
snapshot(): CarrierContinuationDenial[];
|
|
43
|
+
load: (value: unknown) => boolean;
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function parseDenials(value) {
|
|
2
|
+
if (value === undefined)
|
|
3
|
+
return [];
|
|
4
|
+
if (!Array.isArray(value) || value.length > 128)
|
|
5
|
+
return null;
|
|
6
|
+
const parsed = [];
|
|
7
|
+
for (const item of value) {
|
|
8
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
9
|
+
return null;
|
|
10
|
+
const attentionRef = Reflect.get(item, "attentionRef");
|
|
11
|
+
const tabId = Reflect.get(item, "tabId");
|
|
12
|
+
const taskId = Reflect.get(item, "taskId");
|
|
13
|
+
const roleRef = Reflect.get(item, "roleRef");
|
|
14
|
+
const workerRef = Reflect.get(item, "workerRef");
|
|
15
|
+
const url = Reflect.get(item, "url");
|
|
16
|
+
const contentInstanceId = Reflect.get(item, "contentInstanceId");
|
|
17
|
+
const permissionFingerprint = Reflect.get(item, "permissionFingerprint");
|
|
18
|
+
if (typeof attentionRef !== "string" ||
|
|
19
|
+
!Number.isInteger(tabId) ||
|
|
20
|
+
(taskId !== null && typeof taskId !== "string") ||
|
|
21
|
+
(roleRef !== null && typeof roleRef !== "string") ||
|
|
22
|
+
(workerRef !== null && typeof workerRef !== "string") ||
|
|
23
|
+
typeof url !== "string" ||
|
|
24
|
+
typeof contentInstanceId !== "string" ||
|
|
25
|
+
typeof permissionFingerprint !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
parsed.push({
|
|
28
|
+
attentionRef,
|
|
29
|
+
tabId: tabId,
|
|
30
|
+
taskId,
|
|
31
|
+
roleRef,
|
|
32
|
+
workerRef,
|
|
33
|
+
url,
|
|
34
|
+
contentInstanceId,
|
|
35
|
+
permissionFingerprint,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
export function createCarrierContinuationControl(initial) {
|
|
41
|
+
const byTab = new Map();
|
|
42
|
+
const load = (value) => {
|
|
43
|
+
const denials = parseDenials(value);
|
|
44
|
+
byTab.clear();
|
|
45
|
+
if (!denials)
|
|
46
|
+
return false;
|
|
47
|
+
for (const denial of denials)
|
|
48
|
+
byTab.set(denial.tabId, denial);
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
if (initial !== undefined)
|
|
52
|
+
load(initial);
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
beginDenied(denial) {
|
|
55
|
+
byTab.set(denial.tabId, { ...denial });
|
|
56
|
+
},
|
|
57
|
+
cancelDenied(attentionRef) {
|
|
58
|
+
for (const [tabId, denial] of byTab)
|
|
59
|
+
if (denial.attentionRef === attentionRef)
|
|
60
|
+
return byTab.delete(tabId);
|
|
61
|
+
return false;
|
|
62
|
+
},
|
|
63
|
+
hasMatchingPermissionDenial(context) {
|
|
64
|
+
const denial = byTab.get(context.tabId);
|
|
65
|
+
return (denial?.contentInstanceId === context.contentInstanceId &&
|
|
66
|
+
denial.url === context.url &&
|
|
67
|
+
denial.permissionFingerprint === context.permissionFingerprint &&
|
|
68
|
+
denial.taskId === context.taskId &&
|
|
69
|
+
denial.roleRef === context.roleRef &&
|
|
70
|
+
denial.workerRef === context.workerRef);
|
|
71
|
+
},
|
|
72
|
+
hasMatchingDispatchDenial(context) {
|
|
73
|
+
if (context.conversationLocator === null)
|
|
74
|
+
return false;
|
|
75
|
+
for (const denial of byTab.values())
|
|
76
|
+
if (denial.taskId === context.taskId &&
|
|
77
|
+
denial.roleRef === context.roleRef &&
|
|
78
|
+
denial.workerRef === context.workerRef &&
|
|
79
|
+
denial.url === context.conversationLocator)
|
|
80
|
+
return true;
|
|
81
|
+
return false;
|
|
82
|
+
},
|
|
83
|
+
consumeRecovery(previous, current) {
|
|
84
|
+
const denial = byTab.get(current.tabId);
|
|
85
|
+
if (!denial)
|
|
86
|
+
return null;
|
|
87
|
+
if (denial.url !== current.url ||
|
|
88
|
+
denial.contentInstanceId !== current.contentInstanceId) {
|
|
89
|
+
byTab.delete(current.tabId);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (current.pageState !== "IDLE")
|
|
93
|
+
return null;
|
|
94
|
+
if (previous &&
|
|
95
|
+
(previous.tabId !== denial.tabId ||
|
|
96
|
+
previous.url !== denial.url ||
|
|
97
|
+
previous.contentInstanceId !== denial.contentInstanceId ||
|
|
98
|
+
previous.pageState !== "BLOCKED" ||
|
|
99
|
+
previous.blockerFacts?.fingerprint !== denial.permissionFingerprint)) {
|
|
100
|
+
byTab.delete(current.tabId);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
byTab.delete(current.tabId);
|
|
104
|
+
return { ...denial };
|
|
105
|
+
},
|
|
106
|
+
suppressRecovery(previous, current) {
|
|
107
|
+
return this.consumeRecovery(previous, current) !== null;
|
|
108
|
+
},
|
|
109
|
+
snapshot() {
|
|
110
|
+
return [...byTab.values()].map((denial) => ({ ...denial }));
|
|
111
|
+
},
|
|
112
|
+
load,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type CarrierPermissionAttemptSnapshot = ReadonlyArray<{
|
|
2
|
+
tabId: number;
|
|
3
|
+
key: string;
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* Tracks only automatic actions whose post-click reality is not yet known.
|
|
7
|
+
* A confirmed release removes the entry, so a later identical permission is a
|
|
8
|
+
* new occurrence; an MV3 service-worker restart reloads the bounded snapshot
|
|
9
|
+
* and therefore cannot blindly repeat an uncertain click.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createCarrierPermissionAttemptRegistry(initial?: unknown): Readonly<{
|
|
12
|
+
has(tabId: number, key: string): boolean;
|
|
13
|
+
begin(tabId: number, key: string): void;
|
|
14
|
+
release(tabId: number, key: string): boolean;
|
|
15
|
+
observe(tabId: number, currentKey: string | null): boolean;
|
|
16
|
+
load(value: unknown): boolean;
|
|
17
|
+
snapshot(): CarrierPermissionAttemptSnapshot;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function parsedAttempts(value) {
|
|
2
|
+
const result = new Map();
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return { attempts: result, valid: true };
|
|
5
|
+
if (!Array.isArray(value))
|
|
6
|
+
return { attempts: result, valid: false };
|
|
7
|
+
let valid = value.length <= 256;
|
|
8
|
+
for (const candidate of value.slice(0, 256)) {
|
|
9
|
+
if (typeof candidate !== "object" ||
|
|
10
|
+
candidate === null ||
|
|
11
|
+
Array.isArray(candidate)) {
|
|
12
|
+
valid = false;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const tabId = Reflect.get(candidate, "tabId");
|
|
16
|
+
const key = Reflect.get(candidate, "key");
|
|
17
|
+
if (!Number.isInteger(tabId) ||
|
|
18
|
+
tabId < 0 ||
|
|
19
|
+
typeof key !== "string" ||
|
|
20
|
+
key.length === 0 ||
|
|
21
|
+
key.length > 1_000) {
|
|
22
|
+
valid = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (result.has(tabId))
|
|
26
|
+
valid = false;
|
|
27
|
+
result.set(tabId, key);
|
|
28
|
+
}
|
|
29
|
+
return { attempts: result, valid };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Tracks only automatic actions whose post-click reality is not yet known.
|
|
33
|
+
* A confirmed release removes the entry, so a later identical permission is a
|
|
34
|
+
* new occurrence; an MV3 service-worker restart reloads the bounded snapshot
|
|
35
|
+
* and therefore cannot blindly repeat an uncertain click.
|
|
36
|
+
*/
|
|
37
|
+
export function createCarrierPermissionAttemptRegistry(initial) {
|
|
38
|
+
const attempts = parsedAttempts(initial).attempts;
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
has(tabId, key) {
|
|
41
|
+
return attempts.get(tabId) === key;
|
|
42
|
+
},
|
|
43
|
+
begin(tabId, key) {
|
|
44
|
+
attempts.set(tabId, key);
|
|
45
|
+
},
|
|
46
|
+
release(tabId, key) {
|
|
47
|
+
if (attempts.get(tabId) !== key)
|
|
48
|
+
return false;
|
|
49
|
+
return attempts.delete(tabId);
|
|
50
|
+
},
|
|
51
|
+
observe(tabId, currentKey) {
|
|
52
|
+
const attempted = attempts.get(tabId);
|
|
53
|
+
if (attempted === undefined || attempted === currentKey)
|
|
54
|
+
return false;
|
|
55
|
+
return attempts.delete(tabId);
|
|
56
|
+
},
|
|
57
|
+
load(value) {
|
|
58
|
+
const parsed = parsedAttempts(value);
|
|
59
|
+
attempts.clear();
|
|
60
|
+
for (const [tabId, key] of parsed.attempts)
|
|
61
|
+
attempts.set(tabId, key);
|
|
62
|
+
return parsed.valid;
|
|
63
|
+
},
|
|
64
|
+
snapshot() {
|
|
65
|
+
return [...attempts].map(([tabId, key]) => ({ tabId, key }));
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ActionPermissionFacts, PermissionSemanticAction } from "./carrier-permission.ts";
|
|
2
|
+
export type CarrierPermissionDecision = {
|
|
3
|
+
decision: "AUTO_ALLOW" | "DEFER" | "HUMAN_REQUIRED";
|
|
4
|
+
reason: string;
|
|
5
|
+
};
|
|
6
|
+
export type CarrierPermissionLifecycleResult = {
|
|
7
|
+
status: "RELEASED";
|
|
8
|
+
action: "allowAlways";
|
|
9
|
+
} | {
|
|
10
|
+
status: "HUMAN_REQUIRED";
|
|
11
|
+
reason: string;
|
|
12
|
+
} | {
|
|
13
|
+
status: "STALE";
|
|
14
|
+
};
|
|
15
|
+
export type CarrierPermissionLifecyclePort = {
|
|
16
|
+
classify(): Promise<CarrierPermissionDecision>;
|
|
17
|
+
revalidate(): boolean | Promise<boolean>;
|
|
18
|
+
waitBeforeReclassify?(): Promise<void>;
|
|
19
|
+
act(action: PermissionSemanticAction): Promise<void>;
|
|
20
|
+
released(): Promise<boolean>;
|
|
21
|
+
};
|
|
22
|
+
export declare function resolveRoutineCarrierPermission(input: {
|
|
23
|
+
facts: ActionPermissionFacts;
|
|
24
|
+
autoAlreadyAttempted: boolean;
|
|
25
|
+
maxClassifications?: number;
|
|
26
|
+
humanDenied?: () => boolean;
|
|
27
|
+
port: CarrierPermissionLifecyclePort;
|
|
28
|
+
}): Promise<CarrierPermissionLifecycleResult>;
|
|
29
|
+
export declare function resolveHumanCarrierPermission(input: {
|
|
30
|
+
action: "allowOnce" | "deny";
|
|
31
|
+
revalidate(): boolean | Promise<boolean>;
|
|
32
|
+
act(action: "allowOnce" | "deny"): Promise<void>;
|
|
33
|
+
released(): Promise<boolean>;
|
|
34
|
+
}): Promise<"RELEASED">;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export async function resolveRoutineCarrierPermission(input) {
|
|
2
|
+
const denied = () => input.humanDenied?.()
|
|
3
|
+
? { status: "HUMAN_REQUIRED", reason: "HUMAN_DENIED" }
|
|
4
|
+
: null;
|
|
5
|
+
const maxClassifications = Math.max(1, input.maxClassifications ?? 40);
|
|
6
|
+
let decision = null;
|
|
7
|
+
for (let attempt = 0; attempt < maxClassifications; attempt += 1) {
|
|
8
|
+
const beforeClassification = denied();
|
|
9
|
+
if (beforeClassification)
|
|
10
|
+
return beforeClassification;
|
|
11
|
+
try {
|
|
12
|
+
decision = await input.port.classify();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {
|
|
16
|
+
status: "HUMAN_REQUIRED",
|
|
17
|
+
reason: "PERMISSION_CLASSIFICATION_FAILED",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const afterClassification = denied();
|
|
21
|
+
if (afterClassification)
|
|
22
|
+
return afterClassification;
|
|
23
|
+
if (decision.decision !== "DEFER")
|
|
24
|
+
break;
|
|
25
|
+
if (!(await input.port.revalidate()))
|
|
26
|
+
return { status: "STALE" };
|
|
27
|
+
if (attempt === maxClassifications - 1)
|
|
28
|
+
return {
|
|
29
|
+
status: "HUMAN_REQUIRED",
|
|
30
|
+
reason: "PERMISSION_CONTEXT_DEFER_TIMEOUT",
|
|
31
|
+
};
|
|
32
|
+
await input.port.waitBeforeReclassify?.();
|
|
33
|
+
}
|
|
34
|
+
if (!decision)
|
|
35
|
+
return {
|
|
36
|
+
status: "HUMAN_REQUIRED",
|
|
37
|
+
reason: "PERMISSION_CLASSIFICATION_FAILED",
|
|
38
|
+
};
|
|
39
|
+
if (decision.decision !== "AUTO_ALLOW")
|
|
40
|
+
return { status: "HUMAN_REQUIRED", reason: decision.reason };
|
|
41
|
+
if (!input.facts.actions.includes("allowAlways"))
|
|
42
|
+
return {
|
|
43
|
+
status: "HUMAN_REQUIRED",
|
|
44
|
+
reason: "AUTO_ALLOW_ACTION_UNAVAILABLE",
|
|
45
|
+
};
|
|
46
|
+
if (input.autoAlreadyAttempted)
|
|
47
|
+
return {
|
|
48
|
+
status: "HUMAN_REQUIRED",
|
|
49
|
+
reason: "AUTO_ALLOW_REALITY_UNCONFIRMED",
|
|
50
|
+
};
|
|
51
|
+
if (!(await input.port.revalidate()))
|
|
52
|
+
return { status: "STALE" };
|
|
53
|
+
const beforeAction = denied();
|
|
54
|
+
if (beforeAction)
|
|
55
|
+
return beforeAction;
|
|
56
|
+
try {
|
|
57
|
+
await input.port.act("allowAlways");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { status: "HUMAN_REQUIRED", reason: "AUTO_ALLOW_FAILED" };
|
|
61
|
+
}
|
|
62
|
+
return (await input.port.released())
|
|
63
|
+
? { status: "RELEASED", action: "allowAlways" }
|
|
64
|
+
: { status: "HUMAN_REQUIRED", reason: "AUTO_ALLOW_REALITY_UNCONFIRMED" };
|
|
65
|
+
}
|
|
66
|
+
export async function resolveHumanCarrierPermission(input) {
|
|
67
|
+
if (!(await input.revalidate()))
|
|
68
|
+
throw new Error("STALE_PERMISSION");
|
|
69
|
+
await input.act(input.action);
|
|
70
|
+
if (!(await input.released()))
|
|
71
|
+
throw new Error("PERMISSION_ACTION_REALITY_UNCONFIRMED");
|
|
72
|
+
return "RELEASED";
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type PermissionSemanticAction = "allowAlways" | "allowOnce" | "deny";
|
|
2
|
+
export type ActionPermissionCandidate = {
|
|
3
|
+
text: string;
|
|
4
|
+
buttonLabels: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
export type ActionPermissionFacts = {
|
|
7
|
+
kind: "ACTION_PERMISSION";
|
|
8
|
+
targetHost: string | null;
|
|
9
|
+
operationId: string;
|
|
10
|
+
taskId: string | null;
|
|
11
|
+
actions: PermissionSemanticAction[];
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function permissionSemanticAction(label: string): PermissionSemanticAction | null;
|
|
15
|
+
export declare function detectActionPermission(candidates: readonly ActionPermissionCandidate[]): ActionPermissionFacts | null;
|
|
16
|
+
export declare function permissionActionAllowed(facts: ActionPermissionFacts, expectedFingerprint: string, action: string): action is PermissionSemanticAction;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const actionLabels = [
|
|
2
|
+
[/^(始终允许|always allow)$/i, "allowAlways"],
|
|
3
|
+
[/^(允许一次|allow once)$/i, "allowOnce"],
|
|
4
|
+
[/^(拒绝|deny)$/i, "deny"],
|
|
5
|
+
];
|
|
6
|
+
export function permissionSemanticAction(label) {
|
|
7
|
+
const normalized = label.trim();
|
|
8
|
+
for (const [pattern, action] of actionLabels)
|
|
9
|
+
if (pattern.test(normalized))
|
|
10
|
+
return action;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function normalizedText(value) {
|
|
14
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 4_096);
|
|
15
|
+
}
|
|
16
|
+
function hashFingerprint(value) {
|
|
17
|
+
let hash = 0x811c9dc5;
|
|
18
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
19
|
+
hash ^= value.charCodeAt(index);
|
|
20
|
+
hash = Math.imul(hash, 0x01000193);
|
|
21
|
+
}
|
|
22
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
23
|
+
}
|
|
24
|
+
function targetHost(text) {
|
|
25
|
+
const url = text.match(/https?:\/\/([a-z0-9.-]+)(?=[/:\s"'”]|$)/i)?.[1];
|
|
26
|
+
if (url)
|
|
27
|
+
return url.toLowerCase();
|
|
28
|
+
const hosts = text.match(/[a-z0-9][a-z0-9-]*(?:\.[a-z0-9-]+){2,}/gi) ?? [];
|
|
29
|
+
return (hosts.find((value) => value.includes("devtunnels.ms"))?.toLowerCase() ??
|
|
30
|
+
hosts[0]?.toLowerCase() ??
|
|
31
|
+
null);
|
|
32
|
+
}
|
|
33
|
+
function operationId(text) {
|
|
34
|
+
return (text.match(/(?:工具调用|tool call)\s*[::]\s*[^\s.]+(?:\.[^\s.]+)*\.([A-Za-z][A-Za-z0-9_]*)/i)?.[1] ?? null);
|
|
35
|
+
}
|
|
36
|
+
function taskId(text) {
|
|
37
|
+
return text.match(/\btask-[A-Za-z0-9-]+\b/)?.[0] ?? null;
|
|
38
|
+
}
|
|
39
|
+
export function detectActionPermission(candidates) {
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
const actions = candidate.buttonLabels
|
|
42
|
+
.map(permissionSemanticAction)
|
|
43
|
+
.filter((value) => value !== null);
|
|
44
|
+
if (!actions.includes("deny") ||
|
|
45
|
+
(!actions.includes("allowAlways") && !actions.includes("allowOnce")))
|
|
46
|
+
continue;
|
|
47
|
+
const text = normalizedText(candidate.text);
|
|
48
|
+
const operation = operationId(text);
|
|
49
|
+
if (!operation)
|
|
50
|
+
continue;
|
|
51
|
+
return {
|
|
52
|
+
kind: "ACTION_PERMISSION",
|
|
53
|
+
targetHost: targetHost(text),
|
|
54
|
+
operationId: operation,
|
|
55
|
+
taskId: taskId(text),
|
|
56
|
+
actions,
|
|
57
|
+
fingerprint: `permission:v1:${hashFingerprint(`${text}|${actions.join(",")}`)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
export function permissionActionAllowed(facts, expectedFingerprint, action) {
|
|
63
|
+
return (facts.fingerprint === expectedFingerprint &&
|
|
64
|
+
facts.actions.includes(action));
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ActionPermissionFacts, type PermissionSemanticAction } from "./carrier-permission.ts";
|
|
2
|
+
export type ChatGptPageReality = {
|
|
3
|
+
pageState: "IDLE" | "BUSY" | "BLOCKED" | "UNKNOWN";
|
|
4
|
+
activityKind: "GENERATING" | "ACTION_PERMISSION" | "ACTION_RUNNING" | "WAITING_HUMAN" | null;
|
|
5
|
+
blockerFacts?: ActionPermissionFacts;
|
|
6
|
+
};
|
|
7
|
+
export declare function classifyChatGptPageSignals(input: {
|
|
8
|
+
permission: ActionPermissionFacts | null;
|
|
9
|
+
hasDialog: boolean;
|
|
10
|
+
isGenerating: boolean;
|
|
11
|
+
hasComposer: boolean;
|
|
12
|
+
}): ChatGptPageReality;
|
|
13
|
+
export declare function observeChatGptPage(document: Document): ChatGptPageReality;
|
|
14
|
+
export declare function performChatGptPermissionAction(document: Document, expectedFingerprint: string, action: PermissionSemanticAction): ActionPermissionFacts;
|
|
15
|
+
export declare function submitChatGptComposer(document: Document, value: string): Promise<void>;
|
|
16
|
+
export declare function writeChatGptInput(document: Document, selector: string, value: string): void;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { detectActionPermission, permissionActionAllowed, permissionSemanticAction, } from "./carrier-permission.js";
|
|
2
|
+
import { submitControlledComposer } from "./composer-submit.js";
|
|
3
|
+
export function classifyChatGptPageSignals(input) {
|
|
4
|
+
if (input.permission)
|
|
5
|
+
return {
|
|
6
|
+
pageState: "BLOCKED",
|
|
7
|
+
activityKind: "ACTION_PERMISSION",
|
|
8
|
+
blockerFacts: input.permission,
|
|
9
|
+
};
|
|
10
|
+
if (input.hasDialog)
|
|
11
|
+
return { pageState: "BLOCKED", activityKind: "WAITING_HUMAN" };
|
|
12
|
+
if (input.isGenerating)
|
|
13
|
+
return { pageState: "BUSY", activityKind: "GENERATING" };
|
|
14
|
+
if (input.hasComposer)
|
|
15
|
+
return { pageState: "IDLE", activityKind: null };
|
|
16
|
+
return { pageState: "UNKNOWN", activityKind: null };
|
|
17
|
+
}
|
|
18
|
+
const composerSelector = '#prompt-textarea, textarea, [contenteditable="true"]';
|
|
19
|
+
const sendSelector = 'button[data-testid="send-button"], button[aria-label*="Send"], button[aria-label*="发送"]';
|
|
20
|
+
function buttonLabel(button) {
|
|
21
|
+
return (button.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
22
|
+
}
|
|
23
|
+
function actionPermissionDom(document) {
|
|
24
|
+
const view = document.defaultView;
|
|
25
|
+
if (!view)
|
|
26
|
+
return null;
|
|
27
|
+
const semanticButtons = [...document.querySelectorAll("button")].filter((button) => button instanceof view.HTMLButtonElement &&
|
|
28
|
+
permissionSemanticAction(buttonLabel(button)) !== null);
|
|
29
|
+
for (const seed of semanticButtons) {
|
|
30
|
+
let root = seed.parentElement;
|
|
31
|
+
for (let depth = 0; root && depth < 8; depth += 1, root = root.parentElement) {
|
|
32
|
+
const buttons = [...root.querySelectorAll("button")].filter((button) => button instanceof view.HTMLButtonElement);
|
|
33
|
+
const facts = detectActionPermission([
|
|
34
|
+
{
|
|
35
|
+
text: root.textContent ?? "",
|
|
36
|
+
buttonLabels: buttons.map(buttonLabel),
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
if (!facts)
|
|
40
|
+
continue;
|
|
41
|
+
const mapped = new Map();
|
|
42
|
+
for (const button of buttons) {
|
|
43
|
+
const action = permissionSemanticAction(buttonLabel(button));
|
|
44
|
+
if (action && !mapped.has(action))
|
|
45
|
+
mapped.set(action, button);
|
|
46
|
+
}
|
|
47
|
+
return { facts, buttons: mapped };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export function observeChatGptPage(document) {
|
|
53
|
+
const permission = actionPermissionDom(document);
|
|
54
|
+
return classifyChatGptPageSignals({
|
|
55
|
+
permission: permission?.facts ?? null,
|
|
56
|
+
hasDialog: document.querySelector('[role="dialog"]') !== null,
|
|
57
|
+
isGenerating: document.querySelector('[data-testid="stop-button"], button[aria-label*="Stop"], button[aria-label*="停止"]') !== null,
|
|
58
|
+
hasComposer: document.querySelector(composerSelector) !== null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function performChatGptPermissionAction(document, expectedFingerprint, action) {
|
|
62
|
+
const permission = actionPermissionDom(document);
|
|
63
|
+
if (!permission)
|
|
64
|
+
throw new Error("ACTION_PERMISSION_NOT_FOUND");
|
|
65
|
+
if (!permissionActionAllowed(permission.facts, expectedFingerprint, action))
|
|
66
|
+
throw new Error("STALE_PERMISSION");
|
|
67
|
+
const button = permission.buttons.get(action);
|
|
68
|
+
if (!button ||
|
|
69
|
+
button.disabled ||
|
|
70
|
+
button.getAttribute("aria-disabled") === "true")
|
|
71
|
+
throw new Error("PERMISSION_ACTION_NOT_READY");
|
|
72
|
+
button.click();
|
|
73
|
+
return permission.facts;
|
|
74
|
+
}
|
|
75
|
+
function composerElement(document) {
|
|
76
|
+
const view = document.defaultView;
|
|
77
|
+
const element = document.querySelector(composerSelector);
|
|
78
|
+
if (!view || !(element instanceof view.HTMLElement))
|
|
79
|
+
throw new Error("COMPOSER_NOT_FOUND");
|
|
80
|
+
return element;
|
|
81
|
+
}
|
|
82
|
+
function readElementValue(element) {
|
|
83
|
+
const view = element.ownerDocument.defaultView;
|
|
84
|
+
if (!view)
|
|
85
|
+
return "";
|
|
86
|
+
if (element instanceof view.HTMLTextAreaElement ||
|
|
87
|
+
element instanceof view.HTMLInputElement)
|
|
88
|
+
return element.value;
|
|
89
|
+
return element.textContent ?? "";
|
|
90
|
+
}
|
|
91
|
+
function nativeWrite(element, value) {
|
|
92
|
+
const view = element.ownerDocument.defaultView;
|
|
93
|
+
if (!view)
|
|
94
|
+
throw new Error("DOM_WINDOW_NOT_READY");
|
|
95
|
+
if (element instanceof view.HTMLTextAreaElement ||
|
|
96
|
+
element instanceof view.HTMLInputElement) {
|
|
97
|
+
const prototype = element instanceof view.HTMLTextAreaElement
|
|
98
|
+
? view.HTMLTextAreaElement.prototype
|
|
99
|
+
: view.HTMLInputElement.prototype;
|
|
100
|
+
const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
101
|
+
if (!setter)
|
|
102
|
+
throw new Error("COMPOSER_NATIVE_SETTER_MISSING");
|
|
103
|
+
setter.call(element, value);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
element.textContent = value;
|
|
107
|
+
}
|
|
108
|
+
function dispatchComposerInput(element, value) {
|
|
109
|
+
const view = element.ownerDocument.defaultView;
|
|
110
|
+
if (!view)
|
|
111
|
+
throw new Error("DOM_WINDOW_NOT_READY");
|
|
112
|
+
element.dispatchEvent(new view.InputEvent("input", {
|
|
113
|
+
bubbles: true,
|
|
114
|
+
inputType: "insertText",
|
|
115
|
+
data: value,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
function nextComposerFrame(document) {
|
|
119
|
+
const view = document.defaultView;
|
|
120
|
+
if (!view)
|
|
121
|
+
return Promise.reject(new Error("DOM_WINDOW_NOT_READY"));
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
let settled = false;
|
|
124
|
+
const finish = () => {
|
|
125
|
+
if (settled)
|
|
126
|
+
return;
|
|
127
|
+
settled = true;
|
|
128
|
+
resolve();
|
|
129
|
+
};
|
|
130
|
+
view.requestAnimationFrame(finish);
|
|
131
|
+
view.setTimeout(finish, 50);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function sendButton(document) {
|
|
135
|
+
const view = document.defaultView;
|
|
136
|
+
const element = document.querySelector(sendSelector);
|
|
137
|
+
return view && element instanceof view.HTMLButtonElement ? element : null;
|
|
138
|
+
}
|
|
139
|
+
export async function submitChatGptComposer(document, value) {
|
|
140
|
+
const composer = composerElement(document);
|
|
141
|
+
composer.focus();
|
|
142
|
+
await submitControlledComposer({
|
|
143
|
+
write: (next) => nativeWrite(composer, next),
|
|
144
|
+
dispatchInput: (next) => dispatchComposerInput(composer, next),
|
|
145
|
+
readValue: () => readElementValue(composer),
|
|
146
|
+
submitReady: () => {
|
|
147
|
+
const button = sendButton(document);
|
|
148
|
+
return Boolean(button &&
|
|
149
|
+
!button.disabled &&
|
|
150
|
+
button.getAttribute("aria-disabled") !== "true");
|
|
151
|
+
},
|
|
152
|
+
nextFrame: () => nextComposerFrame(document),
|
|
153
|
+
clickSubmit: () => {
|
|
154
|
+
const button = sendButton(document);
|
|
155
|
+
if (!button ||
|
|
156
|
+
button.disabled ||
|
|
157
|
+
button.getAttribute("aria-disabled") === "true")
|
|
158
|
+
throw new Error("COMPOSER_SUBMIT_NOT_READY");
|
|
159
|
+
button.click();
|
|
160
|
+
},
|
|
161
|
+
}, value);
|
|
162
|
+
}
|
|
163
|
+
export function writeChatGptInput(document, selector, value) {
|
|
164
|
+
if (!selector || selector.length > 512)
|
|
165
|
+
throw new Error("SELECTOR_INVALID");
|
|
166
|
+
const view = document.defaultView;
|
|
167
|
+
const element = document.querySelector(selector);
|
|
168
|
+
if (!view || !(element instanceof view.HTMLElement))
|
|
169
|
+
throw new Error("ELEMENT_NOT_FOUND");
|
|
170
|
+
element.focus();
|
|
171
|
+
nativeWrite(element, value);
|
|
172
|
+
dispatchComposerInput(element, value);
|
|
173
|
+
}
|