@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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export function parseBrowserExtensionSetupArgs(args, cwd) {
|
|
3
|
+
if (args[0] !== "setup")
|
|
4
|
+
throw new Error("SETUP_COMMAND_REQUIRED");
|
|
5
|
+
let workspaceRoot = resolve(cwd);
|
|
6
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
7
|
+
const value = args[index];
|
|
8
|
+
if (value === "--workspace") {
|
|
9
|
+
const workspace = args[index + 1];
|
|
10
|
+
if (!workspace || workspace.startsWith("--")) {
|
|
11
|
+
throw new Error("MISSING_WORKSPACE_VALUE");
|
|
12
|
+
}
|
|
13
|
+
workspaceRoot = resolve(workspace);
|
|
14
|
+
index += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (value?.startsWith("--")) {
|
|
18
|
+
throw new Error(`UNSUPPORTED_SETUP_OPTION:${value}`);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`UNSUPPORTED_SETUP_STEP:${value}`);
|
|
21
|
+
}
|
|
22
|
+
return { workspaceRoot };
|
|
23
|
+
}
|
package/dist/src/configure.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
3
2
|
import { readFile } from "node:fs/promises";
|
|
4
3
|
import { resolve } from "node:path";
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
4
|
import { behaviorAdapter, materializeProductionConfig, } from "../deployment/adapter.js";
|
|
5
|
+
import { parseBrowserExtensionSetupArgs } from "./configure-args.js";
|
|
6
|
+
import { browserExtensionSetupFailureMessage, browserExtensionSetupSuccessMessage, runInteractiveBrowserExtensionSetup, } from "./install-workflow.js";
|
|
7
7
|
function reportFatal(error) {
|
|
8
|
-
process.stderr.write(
|
|
8
|
+
process.stderr.write(browserExtensionSetupFailureMessage(error));
|
|
9
9
|
process.exitCode = 1;
|
|
10
10
|
}
|
|
11
11
|
process.on("uncaughtException", reportFatal);
|
|
@@ -41,21 +41,6 @@ export async function materializeBrowserExtensionConfig(workspaceRoot) {
|
|
|
41
41
|
workspaceRoot,
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
function openChromeExtensions() {
|
|
45
|
-
const url = "chrome://extensions";
|
|
46
|
-
const command = process.platform === "darwin"
|
|
47
|
-
? "open"
|
|
48
|
-
: process.platform === "win32"
|
|
49
|
-
? "cmd"
|
|
50
|
-
: "xdg-open";
|
|
51
|
-
const parameters = process.platform === "darwin"
|
|
52
|
-
? ["-a", "Google Chrome", url]
|
|
53
|
-
: process.platform === "win32"
|
|
54
|
-
? ["/c", "start", "", url]
|
|
55
|
-
: [url];
|
|
56
|
-
const child = spawn(command, parameters, { detached: true, stdio: "ignore" });
|
|
57
|
-
child.unref();
|
|
58
|
-
}
|
|
59
44
|
async function main() {
|
|
60
45
|
const args = process.argv.slice(2);
|
|
61
46
|
if (args.includes("--json"))
|
|
@@ -68,60 +53,12 @@ async function main() {
|
|
|
68
53
|
? resolve(option("--workspace"))
|
|
69
54
|
: process.cwd();
|
|
70
55
|
if (args[0] === "setup") {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const command = process.platform === "darwin"
|
|
76
|
-
? "pbcopy"
|
|
77
|
-
: process.platform === "win32"
|
|
78
|
-
? "clip"
|
|
79
|
-
: "xclip";
|
|
80
|
-
const parameters = process.platform === "linux" ? ["-selection", "clipboard"] : [];
|
|
81
|
-
spawnSync(command, parameters, { input: loadDir, encoding: "utf8" });
|
|
82
|
-
openChromeExtensions();
|
|
83
|
-
process.stdout.write(`\n✓ 扩展目录已准备并复制到剪贴板\n ${loadDir}\n 启用开发者模式并加载该目录,然后运行 setup 02。\n`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (step === "03") {
|
|
87
|
-
const result = await behaviorAdapter.status({ workspaceRoot });
|
|
88
|
-
process.stdout.write(result.result.data.setupStatus === "READY"
|
|
89
|
-
? "✓ Service Worker 与 Bridge 验证通过\n"
|
|
90
|
-
: "✕ 扩展尚未就绪,请检查 Reload 和后台错误\n");
|
|
91
|
-
if (result.result.data.setupStatus !== "READY")
|
|
92
|
-
process.exitCode = 1;
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (step !== undefined && step !== "02")
|
|
96
|
-
throw new Error(`UNSUPPORTED_SETUP_STEP:${step}`);
|
|
97
|
-
let extensionId = option("--extension-id");
|
|
98
|
-
if (!extensionId && !process.stdin.isTTY)
|
|
99
|
-
throw new Error("非交互环境必须提供 --extension-id");
|
|
100
|
-
if (!extensionId) {
|
|
101
|
-
const prepared = await behaviorAdapter.install({ workspaceRoot });
|
|
102
|
-
const loadDir = String(prepared.result.data.loadDir);
|
|
103
|
-
const prompt = createInterface({
|
|
104
|
-
input: process.stdin,
|
|
105
|
-
output: process.stdout,
|
|
106
|
-
});
|
|
107
|
-
try {
|
|
108
|
-
process.stdout.write(`\nChrome 扩展配置\n\n 1. 已准备扩展目录:${loadDir}\n 2. 在 Chrome 打开“扩展程序”,启用“开发者模式”。\n 3. 点击“加载已解压的扩展程序”,选择上面的目录。\n 4. 确认 Service Worker(后台服务)显示正常,再复制扩展 ID。\n\n`);
|
|
109
|
-
openChromeExtensions();
|
|
110
|
-
extensionId = await prompt.question("◆ Chrome Extension ID(32 位小写字母)\n> ");
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
prompt.close();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const result = await behaviorAdapter.setup({
|
|
117
|
-
workspaceRoot,
|
|
118
|
-
input: { extensionId, serviceWorker: "RUNNING" },
|
|
56
|
+
const setup = parseBrowserExtensionSetupArgs(args, process.cwd());
|
|
57
|
+
await runInteractiveBrowserExtensionSetup({
|
|
58
|
+
workspaceRoot: setup.workspaceRoot,
|
|
59
|
+
timeoutMs: 120_000,
|
|
119
60
|
});
|
|
120
|
-
process.stdout.write(
|
|
121
|
-
? "\n✓ Extension ID 已保存\n✓ Service Worker 运行证据已记录\n"
|
|
122
|
-
: "\n✕ 浏览器扩展尚未就绪,请检查扩展错误后重试。\n");
|
|
123
|
-
if (result.result.status === "FAILED")
|
|
124
|
-
process.exitCode = 1;
|
|
61
|
+
process.stdout.write(browserExtensionSetupSuccessMessage());
|
|
125
62
|
return;
|
|
126
63
|
}
|
|
127
64
|
if (args[0] === "verify") {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type CustomGptCapability = "webSearch" | "imageGeneration" | "codeInterpreter";
|
|
2
|
+
export type CustomGptKnowledgeFile = {
|
|
3
|
+
name: string;
|
|
4
|
+
mime: string;
|
|
5
|
+
sizeBytes: number;
|
|
6
|
+
sha256: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
export type CustomGptProvisioningRequest = {
|
|
10
|
+
packageName: string;
|
|
11
|
+
version: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
description: string;
|
|
14
|
+
instructions: string;
|
|
15
|
+
conversationStarters: string[];
|
|
16
|
+
recommendedModel: string;
|
|
17
|
+
capabilities: Record<CustomGptCapability, boolean>;
|
|
18
|
+
knowledgeBundle: string;
|
|
19
|
+
actionSchema: string;
|
|
20
|
+
knowledgeFiles: CustomGptKnowledgeFile[];
|
|
21
|
+
bearerCredential?: string;
|
|
22
|
+
};
|
|
23
|
+
export interface CustomGptEditorPort {
|
|
24
|
+
setTextField(field: "displayName" | "description" | "instructions", value: string): Promise<void>;
|
|
25
|
+
replaceConversationStarters(values: readonly string[]): Promise<void>;
|
|
26
|
+
selectRecommendedModel(value: string): Promise<void>;
|
|
27
|
+
setCapability(capability: CustomGptCapability, enabled: boolean): Promise<void>;
|
|
28
|
+
installActionSchema(value: string): Promise<void>;
|
|
29
|
+
configureBearerAuth(value: string): Promise<void>;
|
|
30
|
+
uploadKnowledge(files: readonly CustomGptKnowledgeFile[]): Promise<void>;
|
|
31
|
+
verifyReady(material: CustomGptProvisioningRequest): Promise<void>;
|
|
32
|
+
createPrivate(): Promise<{
|
|
33
|
+
gptId: string;
|
|
34
|
+
carrierUrl: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export declare function parseCustomGptProvisioningRequest(input: unknown): CustomGptProvisioningRequest;
|
|
38
|
+
export declare function createCustomGptEditorDriver(port: CustomGptEditorPort): Readonly<{
|
|
39
|
+
configureDraft: (material: CustomGptProvisioningRequest) => Promise<{
|
|
40
|
+
status: "DRAFT_CONFIGURED";
|
|
41
|
+
packageName: string;
|
|
42
|
+
version: string;
|
|
43
|
+
}>;
|
|
44
|
+
provision(material: CustomGptProvisioningRequest): Promise<{
|
|
45
|
+
gptId: string;
|
|
46
|
+
carrierUrl: string;
|
|
47
|
+
status: "LIVE_CREATED";
|
|
48
|
+
packageName: string;
|
|
49
|
+
version: string;
|
|
50
|
+
}>;
|
|
51
|
+
}>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
3
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
function requiredString(value) {
|
|
7
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
8
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function stringArray(value) {
|
|
12
|
+
if (!Array.isArray(value) ||
|
|
13
|
+
value.length === 0 ||
|
|
14
|
+
!value.every((item) => typeof item === "string" && item.trim().length > 0))
|
|
15
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
16
|
+
return [...value];
|
|
17
|
+
}
|
|
18
|
+
function knowledgeFiles(value) {
|
|
19
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64)
|
|
20
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
21
|
+
return value.map((candidate) => {
|
|
22
|
+
const file = record(candidate);
|
|
23
|
+
const name = requiredString(file.name);
|
|
24
|
+
const mime = requiredString(file.mime);
|
|
25
|
+
const sha256 = requiredString(file.sha256);
|
|
26
|
+
const url = requiredString(file.url);
|
|
27
|
+
const sizeBytes = file.sizeBytes;
|
|
28
|
+
if (!Number.isInteger(sizeBytes) ||
|
|
29
|
+
Number(sizeBytes) <= 0 ||
|
|
30
|
+
!/^sha256:[0-9a-f]{64}$/.test(sha256))
|
|
31
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = new URL(url);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
38
|
+
}
|
|
39
|
+
if (parsed.protocol !== "http:" ||
|
|
40
|
+
parsed.hostname !== "127.0.0.1" ||
|
|
41
|
+
!parsed.pathname.startsWith("/v1/provisioning/files/") ||
|
|
42
|
+
parsed.username !== "" ||
|
|
43
|
+
parsed.password !== "")
|
|
44
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
45
|
+
return { name, mime, sizeBytes: Number(sizeBytes), sha256, url };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function parseCustomGptProvisioningRequest(input) {
|
|
49
|
+
const source = record(input);
|
|
50
|
+
const capabilities = record(source.capabilities);
|
|
51
|
+
for (const name of [
|
|
52
|
+
"webSearch",
|
|
53
|
+
"imageGeneration",
|
|
54
|
+
"codeInterpreter",
|
|
55
|
+
])
|
|
56
|
+
if (typeof capabilities[name] !== "boolean")
|
|
57
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
58
|
+
return {
|
|
59
|
+
packageName: requiredString(source.packageName),
|
|
60
|
+
version: requiredString(source.version),
|
|
61
|
+
displayName: requiredString(source.displayName),
|
|
62
|
+
description: requiredString(source.description),
|
|
63
|
+
instructions: requiredString(source.instructions),
|
|
64
|
+
conversationStarters: stringArray(source.conversationStarters),
|
|
65
|
+
recommendedModel: requiredString(source.recommendedModel),
|
|
66
|
+
capabilities: {
|
|
67
|
+
webSearch: capabilities.webSearch,
|
|
68
|
+
imageGeneration: capabilities.imageGeneration,
|
|
69
|
+
codeInterpreter: capabilities.codeInterpreter,
|
|
70
|
+
},
|
|
71
|
+
knowledgeBundle: requiredString(source.knowledgeBundle),
|
|
72
|
+
actionSchema: requiredString(source.actionSchema),
|
|
73
|
+
knowledgeFiles: knowledgeFiles(source.knowledgeFiles),
|
|
74
|
+
...(source.bearerCredential === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { bearerCredential: requiredString(source.bearerCredential) }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function createCustomGptEditorDriver(port) {
|
|
80
|
+
const configureDraft = async (material) => {
|
|
81
|
+
await port.setTextField("displayName", material.displayName);
|
|
82
|
+
await port.setTextField("description", material.description);
|
|
83
|
+
await port.setTextField("instructions", material.instructions);
|
|
84
|
+
await port.replaceConversationStarters(material.conversationStarters);
|
|
85
|
+
await port.selectRecommendedModel(material.recommendedModel);
|
|
86
|
+
for (const capability of [
|
|
87
|
+
"webSearch",
|
|
88
|
+
"imageGeneration",
|
|
89
|
+
"codeInterpreter",
|
|
90
|
+
])
|
|
91
|
+
await port.setCapability(capability, material.capabilities[capability]);
|
|
92
|
+
await port.installActionSchema(material.actionSchema);
|
|
93
|
+
if (material.bearerCredential)
|
|
94
|
+
await port.configureBearerAuth(material.bearerCredential);
|
|
95
|
+
return {
|
|
96
|
+
status: "DRAFT_CONFIGURED",
|
|
97
|
+
packageName: material.packageName,
|
|
98
|
+
version: material.version,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
configureDraft,
|
|
103
|
+
async provision(material) {
|
|
104
|
+
await configureDraft(material);
|
|
105
|
+
await port.uploadKnowledge(material.knowledgeFiles);
|
|
106
|
+
await port.verifyReady(material);
|
|
107
|
+
const live = await port.createPrivate();
|
|
108
|
+
return {
|
|
109
|
+
status: "LIVE_CREATED",
|
|
110
|
+
packageName: material.packageName,
|
|
111
|
+
version: material.version,
|
|
112
|
+
...live,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MaterializedKnowledgeFile = {
|
|
2
|
+
name: string;
|
|
3
|
+
relativePath: string;
|
|
4
|
+
path: string;
|
|
5
|
+
mime: string;
|
|
6
|
+
sizeBytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
};
|
|
9
|
+
export type MaterializedKnowledgeBundle = {
|
|
10
|
+
bundlePath: string;
|
|
11
|
+
bundleSha256: string;
|
|
12
|
+
stagingDirectory: string;
|
|
13
|
+
files: MaterializedKnowledgeFile[];
|
|
14
|
+
};
|
|
15
|
+
export declare function materializeCustomGptKnowledgeBundle(input: {
|
|
16
|
+
bundlePath: string;
|
|
17
|
+
stagingRoot: string;
|
|
18
|
+
maxEntries?: number;
|
|
19
|
+
maxEntryBytes?: number;
|
|
20
|
+
maxTotalBytes?: number;
|
|
21
|
+
}): Promise<MaterializedKnowledgeBundle>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
4
|
+
import { inflateRawSync } from "node:zlib";
|
|
5
|
+
const MIME_BY_EXTENSION = {
|
|
6
|
+
".md": "text/markdown",
|
|
7
|
+
".txt": "text/plain",
|
|
8
|
+
".pdf": "application/pdf",
|
|
9
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10
|
+
".json": "application/json",
|
|
11
|
+
".csv": "text/csv",
|
|
12
|
+
".yaml": "text/yaml",
|
|
13
|
+
".yml": "text/yaml",
|
|
14
|
+
".js": "text/javascript",
|
|
15
|
+
".ts": "text/plain",
|
|
16
|
+
".py": "text/plain",
|
|
17
|
+
};
|
|
18
|
+
function sha256(bytes) {
|
|
19
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
function unsafeEntry(name) {
|
|
22
|
+
if (name.length === 0 ||
|
|
23
|
+
name.includes("\\") ||
|
|
24
|
+
name.includes("\0") ||
|
|
25
|
+
name.startsWith("/") ||
|
|
26
|
+
/^[a-zA-Z]:/.test(name))
|
|
27
|
+
return true;
|
|
28
|
+
const parts = name.split("/");
|
|
29
|
+
return parts.some((part) => part === "" || part === "." || part === "..");
|
|
30
|
+
}
|
|
31
|
+
function findEndOfCentralDirectory(buffer) {
|
|
32
|
+
const minimum = Math.max(0, buffer.length - 65_557);
|
|
33
|
+
for (let offset = buffer.length - 22; offset >= minimum; offset -= 1)
|
|
34
|
+
if (buffer.readUInt32LE(offset) === 0x06054b50)
|
|
35
|
+
return offset;
|
|
36
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
37
|
+
}
|
|
38
|
+
function parseEntries(buffer, maxEntries) {
|
|
39
|
+
const eocd = findEndOfCentralDirectory(buffer);
|
|
40
|
+
const entries = buffer.readUInt16LE(eocd + 10);
|
|
41
|
+
const centralSize = buffer.readUInt32LE(eocd + 12);
|
|
42
|
+
const centralOffset = buffer.readUInt32LE(eocd + 16);
|
|
43
|
+
if (entries === 0 ||
|
|
44
|
+
entries > maxEntries ||
|
|
45
|
+
centralOffset + centralSize > buffer.length)
|
|
46
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
47
|
+
const result = [];
|
|
48
|
+
let offset = centralOffset;
|
|
49
|
+
for (let index = 0; index < entries; index += 1) {
|
|
50
|
+
if (offset + 46 > buffer.length ||
|
|
51
|
+
buffer.readUInt32LE(offset) !== 0x02014b50)
|
|
52
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
53
|
+
const flags = buffer.readUInt16LE(offset + 8);
|
|
54
|
+
const method = buffer.readUInt16LE(offset + 10);
|
|
55
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
56
|
+
const uncompressedSize = buffer.readUInt32LE(offset + 24);
|
|
57
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
58
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
59
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
60
|
+
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
61
|
+
const end = offset + 46 + nameLength + extraLength + commentLength;
|
|
62
|
+
if (end > buffer.length || (flags & 0x1) !== 0)
|
|
63
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
64
|
+
const name = buffer
|
|
65
|
+
.subarray(offset + 46, offset + 46 + nameLength)
|
|
66
|
+
.toString("utf8");
|
|
67
|
+
result.push({
|
|
68
|
+
name,
|
|
69
|
+
method,
|
|
70
|
+
compressedSize,
|
|
71
|
+
uncompressedSize,
|
|
72
|
+
localHeaderOffset,
|
|
73
|
+
});
|
|
74
|
+
offset = end;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function extractEntry(buffer, entry) {
|
|
79
|
+
const offset = entry.localHeaderOffset;
|
|
80
|
+
if (offset + 30 > buffer.length || buffer.readUInt32LE(offset) !== 0x04034b50)
|
|
81
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
82
|
+
const nameLength = buffer.readUInt16LE(offset + 26);
|
|
83
|
+
const extraLength = buffer.readUInt16LE(offset + 28);
|
|
84
|
+
const dataOffset = offset + 30 + nameLength + extraLength;
|
|
85
|
+
const dataEnd = dataOffset + entry.compressedSize;
|
|
86
|
+
if (dataEnd > buffer.length)
|
|
87
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
88
|
+
const compressed = buffer.subarray(dataOffset, dataEnd);
|
|
89
|
+
const output = entry.method === 0
|
|
90
|
+
? Buffer.from(compressed)
|
|
91
|
+
: entry.method === 8
|
|
92
|
+
? inflateRawSync(compressed)
|
|
93
|
+
: (() => {
|
|
94
|
+
throw new Error("KNOWLEDGE_ZIP_COMPRESSION_UNSUPPORTED");
|
|
95
|
+
})();
|
|
96
|
+
if (output.length !== entry.uncompressedSize)
|
|
97
|
+
throw new Error("KNOWLEDGE_ZIP_SIZE_MISMATCH");
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
export async function materializeCustomGptKnowledgeBundle(input) {
|
|
101
|
+
const maxEntries = input.maxEntries ?? 64;
|
|
102
|
+
const maxEntryBytes = input.maxEntryBytes ?? 32 * 1024 * 1024;
|
|
103
|
+
const maxTotalBytes = input.maxTotalBytes ?? 128 * 1024 * 1024;
|
|
104
|
+
const bundlePath = resolve(input.bundlePath);
|
|
105
|
+
const archive = await readFile(bundlePath);
|
|
106
|
+
const bundleSha256 = sha256(archive);
|
|
107
|
+
const entries = parseEntries(archive, maxEntries);
|
|
108
|
+
let totalBytes = 0;
|
|
109
|
+
let knowledgeEntryCount = 0;
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (entry.name.endsWith("/"))
|
|
112
|
+
continue;
|
|
113
|
+
if (unsafeEntry(entry.name))
|
|
114
|
+
throw new Error("KNOWLEDGE_ZIP_ENTRY_UNSAFE");
|
|
115
|
+
if (entry.uncompressedSize > maxEntryBytes)
|
|
116
|
+
throw new Error("KNOWLEDGE_ZIP_ENTRY_TOO_LARGE");
|
|
117
|
+
totalBytes += entry.uncompressedSize;
|
|
118
|
+
if (totalBytes > maxTotalBytes)
|
|
119
|
+
throw new Error("KNOWLEDGE_ZIP_TOTAL_TOO_LARGE");
|
|
120
|
+
if (!MIME_BY_EXTENSION[extname(entry.name).toLowerCase()])
|
|
121
|
+
throw new Error("KNOWLEDGE_FILE_TYPE_UNSUPPORTED");
|
|
122
|
+
extractEntry(archive, entry);
|
|
123
|
+
knowledgeEntryCount += 1;
|
|
124
|
+
}
|
|
125
|
+
if (knowledgeEntryCount === 0)
|
|
126
|
+
throw new Error("KNOWLEDGE_ZIP_EMPTY");
|
|
127
|
+
const stagingDirectory = join(resolve(input.stagingRoot), bundleSha256.slice("sha256:".length, "sha256:".length + 24));
|
|
128
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
129
|
+
await mkdir(stagingDirectory, { recursive: true, mode: 0o700 });
|
|
130
|
+
const fileName = basename(bundlePath);
|
|
131
|
+
const stagedBundlePath = join(stagingDirectory, fileName);
|
|
132
|
+
await writeFile(stagedBundlePath, archive, { mode: 0o600 });
|
|
133
|
+
return {
|
|
134
|
+
bundlePath,
|
|
135
|
+
bundleSha256,
|
|
136
|
+
stagingDirectory,
|
|
137
|
+
files: [
|
|
138
|
+
{
|
|
139
|
+
name: fileName,
|
|
140
|
+
relativePath: fileName,
|
|
141
|
+
path: stagedBundlePath,
|
|
142
|
+
mime: "application/zip",
|
|
143
|
+
sizeBytes: archive.length,
|
|
144
|
+
sha256: bundleSha256,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CustomGptProvisioningRequest } from "./custom-gpt-editor-driver.ts";
|
|
2
|
+
import { type CustomGptProvisioningBridgeOptions } from "./provisioning-bridge.ts";
|
|
3
|
+
export type CustomGptPackageProvisioningMaterial = Omit<CustomGptProvisioningRequest, "actionSchema" | "knowledgeFiles" | "bearerCredential"> & {
|
|
4
|
+
actionSchema: string;
|
|
5
|
+
};
|
|
6
|
+
export type CustomGptProvisioningHostOptions = CustomGptProvisioningBridgeOptions & {
|
|
7
|
+
onlineTimeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type CustomGptProvisioningResult = {
|
|
10
|
+
status: "LIVE_CREATED";
|
|
11
|
+
packageName: string;
|
|
12
|
+
version: string;
|
|
13
|
+
gptId: string;
|
|
14
|
+
carrierUrl: string;
|
|
15
|
+
knowledgeBundleSha256: string;
|
|
16
|
+
knowledgeFiles: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
mime: string;
|
|
19
|
+
sizeBytes: number;
|
|
20
|
+
sha256: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export declare function createCustomGptProvisioningHost(options: CustomGptProvisioningHostOptions): Promise<Readonly<{
|
|
24
|
+
endpoint: string;
|
|
25
|
+
status: () => {
|
|
26
|
+
online: boolean;
|
|
27
|
+
extensionInstanceId: string | null;
|
|
28
|
+
queuedCommands: number;
|
|
29
|
+
pendingCommands: number;
|
|
30
|
+
relayFiles: number;
|
|
31
|
+
};
|
|
32
|
+
provisionPackage: (input: {
|
|
33
|
+
packageRoot: string;
|
|
34
|
+
stagingRoot: string;
|
|
35
|
+
gatewayUrl: string;
|
|
36
|
+
material: CustomGptPackageProvisioningMaterial;
|
|
37
|
+
credential?: string;
|
|
38
|
+
}) => Promise<CustomGptProvisioningResult>;
|
|
39
|
+
finalizeRoleAuth: (input: {
|
|
40
|
+
carrierUrl: string;
|
|
41
|
+
credential: string;
|
|
42
|
+
}) => Promise<{
|
|
43
|
+
status: "AUTH_UPDATED";
|
|
44
|
+
gptId: string;
|
|
45
|
+
carrierUrl: string;
|
|
46
|
+
}>;
|
|
47
|
+
close: () => Promise<void>;
|
|
48
|
+
}>>;
|
|
49
|
+
export declare function createWorkspaceCustomGptProvisioningHost(input: {
|
|
50
|
+
workspaceRoot: string;
|
|
51
|
+
commandTimeoutMs?: number;
|
|
52
|
+
onlineTimeoutMs?: number;
|
|
53
|
+
}): Promise<Readonly<{
|
|
54
|
+
endpoint: string;
|
|
55
|
+
status: () => {
|
|
56
|
+
online: boolean;
|
|
57
|
+
extensionInstanceId: string | null;
|
|
58
|
+
queuedCommands: number;
|
|
59
|
+
pendingCommands: number;
|
|
60
|
+
relayFiles: number;
|
|
61
|
+
};
|
|
62
|
+
provisionPackage: (input: {
|
|
63
|
+
packageRoot: string;
|
|
64
|
+
stagingRoot: string;
|
|
65
|
+
gatewayUrl: string;
|
|
66
|
+
material: CustomGptPackageProvisioningMaterial;
|
|
67
|
+
credential?: string;
|
|
68
|
+
}) => Promise<CustomGptProvisioningResult>;
|
|
69
|
+
finalizeRoleAuth: (input: {
|
|
70
|
+
carrierUrl: string;
|
|
71
|
+
credential: string;
|
|
72
|
+
}) => Promise<{
|
|
73
|
+
status: "AUTH_UPDATED";
|
|
74
|
+
gptId: string;
|
|
75
|
+
carrierUrl: string;
|
|
76
|
+
}>;
|
|
77
|
+
close: () => Promise<void>;
|
|
78
|
+
}>>;
|