vanspod 0.0.0-stage → 0.1.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 +74 -2
- package/bin/register.cjs +11 -0
- package/bin/vanspod.cjs +2 -0
- package/dist/adapterSettings.js +37 -0
- package/dist/adapters/chat-output.js +69 -0
- package/dist/adapters/chat-stream.js +44 -0
- package/dist/adapters/chat.js +63 -0
- package/dist/adapters/claude-reasoning.js +118 -0
- package/dist/adapters/claude-request.js +190 -0
- package/dist/adapters/claude-response.js +79 -0
- package/dist/adapters/index.js +19 -0
- package/dist/adapters/messages-stream.js +126 -0
- package/dist/adapters/messages.js +21 -0
- package/dist/adapters/responses-stream.js +70 -0
- package/dist/adapters/responses.js +28 -0
- package/dist/adapters/stream.js +99 -0
- package/dist/adapters/transport.js +26 -0
- package/dist/adapters/types.js +35 -0
- package/dist/appearance.js +10 -0
- package/dist/classroomApp.js +1814 -0
- package/dist/classroomUpdate.js +155 -0
- package/dist/claudeWebSearch.js +248 -0
- package/dist/clientDetection.js +35 -0
- package/dist/clientIcons.js +33 -0
- package/dist/clientRouteRuntime.js +149 -0
- package/dist/clientRoutes.js +118 -0
- package/dist/clientSettingsBackup.js +110 -0
- package/dist/clientTraffic.js +26 -0
- package/dist/clipboard.js +32 -0
- package/dist/codexWebSearch.js +80 -0
- package/dist/codexWire/apply-patch-envelope.js +139 -0
- package/dist/codexWire/chat-options.js +51 -0
- package/dist/codexWire/chat-tools.js +80 -0
- package/dist/codexWire/freeform-wrapper-scan.js +326 -0
- package/dist/codexWire/progressive-freeform-input.js +133 -0
- package/dist/codexWire/request.js +73 -0
- package/dist/codexWire/responses-tools.js +88 -0
- package/dist/codexWire/tool-arguments.js +39 -0
- package/dist/codexWire/tool-guidance.js +77 -0
- package/dist/commandRunner.js +100 -0
- package/dist/connectionMascot.js +235 -0
- package/dist/courseCatalog.js +131 -0
- package/dist/desktop.js +25 -0
- package/dist/entry.js +17 -0
- package/dist/environmentIcons.js +11 -0
- package/dist/exitGuard.js +17 -0
- package/dist/folderPicker.js +35 -0
- package/dist/installProgress.js +171 -0
- package/dist/installations.js +72 -0
- package/dist/instance.js +160 -0
- package/dist/launcher.js +155 -0
- package/dist/legacyMigration.js +87 -0
- package/dist/lifecyclePage.js +41 -0
- package/dist/modelProxy.js +105 -0
- package/dist/modelStatus.js +42 -0
- package/dist/page.js +1016 -0
- package/dist/pagePrototype.js +528 -0
- package/dist/platformStorage.js +91 -0
- package/dist/removal.js +41 -0
- package/dist/responsesChat.js +835 -0
- package/dist/routeFiles.js +646 -0
- package/dist/routerSessionModels.js +32 -0
- package/dist/runtimeOptions.js +14 -0
- package/dist/secureStorage.js +63 -0
- package/dist/settingsActions.js +100 -0
- package/dist/start.js +822 -0
- package/dist/storageErrors.js +17 -0
- package/dist/storageLocation.js +58 -0
- package/dist/version.js +4 -0
- package/dist/vsCodeRouteSettings.js +208 -0
- package/dist/webSearch.js +56 -0
- package/dist/windowsSecureStorage.js +163 -0
- package/media/brand-icon.ico +0 -0
- package/media/brand-logo.png +0 -0
- package/media/fadou-superhero-blink.png +0 -0
- package/media/fadou-superhero.png +0 -0
- package/package.json +27 -4
- package/scripts/tray.ps1 +40 -0
- package/scripts/uninstall.ps1 +133 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CLIENT_ROUTE_IDS = void 0;
|
|
7
|
+
exports.supportedClientRouteAdapters = supportedClientRouteAdapters;
|
|
8
|
+
exports.isClientRouteAdapter = isClientRouteAdapter;
|
|
9
|
+
exports.defaultClientRoutes = defaultClientRoutes;
|
|
10
|
+
exports.defaultClientRouteSettings = defaultClientRouteSettings;
|
|
11
|
+
exports.createClientRouteSettingsStorage = createClientRouteSettingsStorage;
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
exports.CLIENT_ROUTE_IDS = ["codex", "claudeCode", "vsCodeChat"];
|
|
16
|
+
const SUPPORTED_ADAPTERS = {
|
|
17
|
+
codex: ["responses", "chat"],
|
|
18
|
+
claudeCode: ["responses", "chat", "messages"],
|
|
19
|
+
vsCodeChat: ["responses", "chat"],
|
|
20
|
+
};
|
|
21
|
+
function supportedClientRouteAdapters(route) {
|
|
22
|
+
return [...SUPPORTED_ADAPTERS[route]];
|
|
23
|
+
}
|
|
24
|
+
function isClientRouteAdapter(route, adapter) {
|
|
25
|
+
return SUPPORTED_ADAPTERS[route].some(candidate => candidate === adapter);
|
|
26
|
+
}
|
|
27
|
+
function defaultClientRoutes() {
|
|
28
|
+
return {
|
|
29
|
+
codex: { id: "codex", mode: "native", adapter: "responses", health: "gray", restart: "ready" },
|
|
30
|
+
claudeCode: { id: "claudeCode", mode: "native", adapter: "responses", health: "gray", restart: "ready" },
|
|
31
|
+
vsCodeChat: { id: "vsCodeChat", mode: "native", adapter: "responses", health: "gray", restart: "ready" },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function defaultClientRouteSettings() {
|
|
35
|
+
const routes = defaultClientRoutes();
|
|
36
|
+
return {
|
|
37
|
+
codex: { mode: routes.codex.mode, adapter: routes.codex.adapter },
|
|
38
|
+
claudeCode: { mode: routes.claudeCode.mode, adapter: routes.claudeCode.adapter },
|
|
39
|
+
vsCodeChat: { mode: routes.vsCodeChat.mode, adapter: routes.vsCodeChat.adapter },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isMode(value) {
|
|
43
|
+
return value === "native" || value === "classroom";
|
|
44
|
+
}
|
|
45
|
+
function routePreference(route, value) {
|
|
46
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
47
|
+
throw new Error("Client Route 設定格式無效。");
|
|
48
|
+
const record = value;
|
|
49
|
+
if (!isMode(record.mode) || typeof record.adapter !== "string" || !isClientRouteAdapter(route, record.adapter)) {
|
|
50
|
+
throw new Error("Client Route 設定格式無效。");
|
|
51
|
+
}
|
|
52
|
+
return { mode: record.mode, adapter: record.adapter };
|
|
53
|
+
}
|
|
54
|
+
function parseClientRouteSettings(value) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
throw new Error("Client Route 設定格式無效。");
|
|
57
|
+
const document = value;
|
|
58
|
+
if (document.version === 1) {
|
|
59
|
+
const adapters = document.adapters;
|
|
60
|
+
if (!adapters || typeof adapters.codex !== "string" || !isClientRouteAdapter("codex", adapters.codex) ||
|
|
61
|
+
typeof adapters.claude !== "string" || !isClientRouteAdapter("claudeCode", adapters.claude)) {
|
|
62
|
+
throw new Error("Client Route 設定格式無效。");
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
codex: { mode: "native", adapter: adapters.codex },
|
|
66
|
+
claudeCode: { mode: "native", adapter: adapters.claude },
|
|
67
|
+
vsCodeChat: { mode: "native", adapter: "responses" },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (document.version !== 2 || !document.routes || typeof document.routes !== "object" || Array.isArray(document.routes)) {
|
|
71
|
+
throw new Error("Client Route 設定版本無效。");
|
|
72
|
+
}
|
|
73
|
+
const routes = document.routes;
|
|
74
|
+
return {
|
|
75
|
+
codex: routePreference("codex", routes.codex),
|
|
76
|
+
claudeCode: routePreference("claudeCode", routes.claudeCode),
|
|
77
|
+
vsCodeChat: routePreference("vsCodeChat", routes.vsCodeChat),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function createClientRouteSettingsStorage(filename) {
|
|
81
|
+
return {
|
|
82
|
+
async load() {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await promises_1.default.readFile(filename, "utf8");
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === "ENOENT")
|
|
89
|
+
return defaultClientRouteSettings();
|
|
90
|
+
throw new Error("無法讀取 Client Route 設定。");
|
|
91
|
+
}
|
|
92
|
+
let document;
|
|
93
|
+
try {
|
|
94
|
+
document = JSON.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error("無法讀取 Client Route 設定。");
|
|
98
|
+
}
|
|
99
|
+
return parseClientRouteSettings(document);
|
|
100
|
+
},
|
|
101
|
+
async save(settings) {
|
|
102
|
+
const value = {
|
|
103
|
+
codex: routePreference("codex", settings.codex),
|
|
104
|
+
claudeCode: routePreference("claudeCode", settings.claudeCode),
|
|
105
|
+
vsCodeChat: routePreference("vsCodeChat", settings.vsCodeChat),
|
|
106
|
+
};
|
|
107
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filename), { recursive: true });
|
|
108
|
+
const temporary = `${filename}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
109
|
+
try {
|
|
110
|
+
await promises_1.default.writeFile(temporary, JSON.stringify({ version: 2, routes: value }) + "\n", "utf8");
|
|
111
|
+
await promises_1.default.rename(temporary, filename);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await promises_1.default.rm(temporary, { force: true });
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.clientSettingsBackup = clientSettingsBackup;
|
|
7
|
+
exports.optionalText = optionalText;
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
/** One durable journal per client, shared by route, model and cache writes. */
|
|
11
|
+
function clientSettingsBackup(file, resources) {
|
|
12
|
+
async function load() {
|
|
13
|
+
const raw = await optionalText(file);
|
|
14
|
+
if (raw === null)
|
|
15
|
+
return {};
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
18
|
+
throw new Error("Invalid client settings backup");
|
|
19
|
+
for (const [id, value] of Object.entries(parsed)) {
|
|
20
|
+
const entry = value;
|
|
21
|
+
if (!resources[id] || !entry || (entry.original !== null && typeof entry.original !== "string") || typeof entry.written !== "string" || (entry.previousWritten !== undefined && typeof entry.previousWritten !== "string") || !Array.isArray(entry.keys) || entry.keys.some(key => typeof key !== "string"))
|
|
22
|
+
throw new Error("Invalid client settings backup");
|
|
23
|
+
}
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
async function save(journal) {
|
|
27
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(file), { recursive: true });
|
|
28
|
+
const temporary = `${file}.tmp`;
|
|
29
|
+
try {
|
|
30
|
+
await promises_1.default.writeFile(temporary, JSON.stringify(journal) + "\n", "utf8");
|
|
31
|
+
await promises_1.default.rename(temporary, file);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await promises_1.default.rm(temporary, { force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
async seed(id, original, keys) {
|
|
39
|
+
const journal = await load();
|
|
40
|
+
if (journal[id])
|
|
41
|
+
return;
|
|
42
|
+
const current = await optionalText(resources[id].file);
|
|
43
|
+
if (current === null)
|
|
44
|
+
return;
|
|
45
|
+
journal[id] = { original, written: current, keys };
|
|
46
|
+
await save(journal);
|
|
47
|
+
},
|
|
48
|
+
async write(id, next, keys, original) {
|
|
49
|
+
const resource = resources[id];
|
|
50
|
+
const journal = await load();
|
|
51
|
+
const current = await optionalText(resource.file);
|
|
52
|
+
const entry = journal[id] ?? { original: original === undefined ? current : original, written: current ?? "", keys: [] };
|
|
53
|
+
if (journal[id] && current !== entry.written && current !== entry.previousWritten) {
|
|
54
|
+
// Preserve intervening edits before a classroom replay takes ownership.
|
|
55
|
+
if (current === null)
|
|
56
|
+
entry.original = null;
|
|
57
|
+
else {
|
|
58
|
+
let baseline = resource.restore(current, entry.original ?? "", entry.written, entry.keys);
|
|
59
|
+
if (entry.previousWritten !== undefined)
|
|
60
|
+
baseline = resource.restore(baseline, entry.original ?? "", entry.previousWritten, entry.keys);
|
|
61
|
+
entry.original = baseline;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
journal[id] = { ...entry, previousWritten: entry.written, written: next, keys: [...new Set([...entry.keys, ...keys])] };
|
|
65
|
+
// Persist both the baseline and planned write before touching the client.
|
|
66
|
+
await save(journal);
|
|
67
|
+
if (next !== current) {
|
|
68
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(resource.file), { recursive: true });
|
|
69
|
+
await promises_1.default.writeFile(resource.file, next, "utf8");
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async restore(ids) {
|
|
73
|
+
const journal = await load();
|
|
74
|
+
const selected = ids ? new Set(ids) : undefined;
|
|
75
|
+
for (const [id, entry] of Object.entries(journal)) {
|
|
76
|
+
if (selected && !selected.has(id))
|
|
77
|
+
continue;
|
|
78
|
+
const resource = resources[id];
|
|
79
|
+
const current = await optionalText(resource.file);
|
|
80
|
+
if (current === null)
|
|
81
|
+
continue; // An external deletion must stay deleted.
|
|
82
|
+
const exact = current === entry.written || current === entry.previousWritten;
|
|
83
|
+
let next = exact ? entry.original : resource.restore(current, entry.original ?? "", entry.written, entry.keys);
|
|
84
|
+
if (!exact && next !== null && entry.previousWritten !== undefined)
|
|
85
|
+
next = resource.restore(next, entry.original ?? "", entry.previousWritten, entry.keys);
|
|
86
|
+
if (next === null)
|
|
87
|
+
await promises_1.default.rm(resource.file, { force: true });
|
|
88
|
+
else if (next !== current)
|
|
89
|
+
await promises_1.default.writeFile(resource.file, next, "utf8");
|
|
90
|
+
}
|
|
91
|
+
// Keep every unselected resource and the whole journal on any failed
|
|
92
|
+
// restoration, so independently detected clients can retry safely.
|
|
93
|
+
const remaining = Object.fromEntries(Object.entries(journal).filter(([id]) => selected && !selected.has(id)));
|
|
94
|
+
if (Object.keys(remaining).length)
|
|
95
|
+
await save(remaining);
|
|
96
|
+
else
|
|
97
|
+
await promises_1.default.rm(file, { force: true });
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function optionalText(file) {
|
|
102
|
+
try {
|
|
103
|
+
return await promises_1.default.readFile(file, "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error.code === "ENOENT")
|
|
107
|
+
return null;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createClientTraffic = createClientTraffic;
|
|
4
|
+
// The page asks once a second; a request shorter than that must still be seen.
|
|
5
|
+
const LINGER_MS = 1500;
|
|
6
|
+
function createClientTraffic(now = Date.now) {
|
|
7
|
+
const inFlight = { codex: 0, claude: 0, vscode: 0 };
|
|
8
|
+
const lastEndedAt = { codex: -Infinity, claude: -Infinity, vscode: -Infinity };
|
|
9
|
+
const isFlowing = (client) => inFlight[client] > 0 || now() - lastEndedAt[client] < LINGER_MS;
|
|
10
|
+
return {
|
|
11
|
+
begin(client) {
|
|
12
|
+
inFlight[client] += 1;
|
|
13
|
+
let done = false;
|
|
14
|
+
return () => {
|
|
15
|
+
if (done)
|
|
16
|
+
return;
|
|
17
|
+
done = true;
|
|
18
|
+
inFlight[client] -= 1;
|
|
19
|
+
lastEndedAt[client] = now();
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
flowing() {
|
|
23
|
+
return { codex: isFlowing("codex"), claude: isFlowing("claude"), vscode: isFlowing("vscode") };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeClipboard = writeClipboard;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
// clip.exe starts in ~25ms versus ~700ms for PowerShell, and reads UTF-16LE input without a BOM
|
|
10
|
+
// as Unicode. It normalizes LF to CRLF, which Windows editors and terminals expect anyway.
|
|
11
|
+
const CLIP = node_path_1.default.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "clip.exe");
|
|
12
|
+
function writeClipboard(text) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const child = (0, node_child_process_1.spawn)(CLIP, [], { stdio: ["pipe", "ignore", "pipe"], windowsHide: true });
|
|
15
|
+
let failed = false;
|
|
16
|
+
child.on("error", () => {
|
|
17
|
+
failed = true;
|
|
18
|
+
reject(new Error("clipboard failed"));
|
|
19
|
+
});
|
|
20
|
+
child.stderr.on("data", () => {
|
|
21
|
+
failed = true;
|
|
22
|
+
});
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
if (!failed && code === 0) {
|
|
25
|
+
resolve();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
reject(new Error("clipboard failed"));
|
|
29
|
+
});
|
|
30
|
+
child.stdin.end(Buffer.from(text, "utf16le"));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.codexWebSearch = codexWebSearch;
|
|
4
|
+
const types_1 = require("./adapters/types");
|
|
5
|
+
const webSearch_1 = require("./webSearch");
|
|
6
|
+
// Codex's web__run tool posts every command to /alpha/search and reads the reply as
|
|
7
|
+
// codex-api's SearchResponse { encrypted_output, output, results }: `output` is the
|
|
8
|
+
// text handed to the model, `results` only feeds the client's own display.
|
|
9
|
+
// Router's POST /v1/search knows none of this (ADR-0007); the translation lives here.
|
|
10
|
+
// A failure is still a 200: Codex treats a non-2xx reply as fatal to the whole turn
|
|
11
|
+
// (and retries 5xx first), so the model would never get to tell the student why.
|
|
12
|
+
const MAX_QUERIES = 3;
|
|
13
|
+
// These commands have no stand-in at all, so the ban here is wider than NO_SHELL's: one
|
|
14
|
+
// curl to a weather or quote site is not a search, yet it is exactly what must not happen.
|
|
15
|
+
function unsupported(commands) {
|
|
16
|
+
return `課堂模式沒有這些網路指令。${commands.length ? `These web commands are not supported in this classroom session and were not run: ${commands.join(", ")}.` : "This request named no web command that this classroom session runs."} Searching is done with search_query (searching by query text); its results already include a text snippet of each page. Do not retry the unsupported commands. Do not use shell commands (curl, wget, scripts) to get that information some other way. If that is not enough, tell the user this classroom session cannot do it.`;
|
|
17
|
+
}
|
|
18
|
+
// open, click and find read a page Codex already knows the address of. That is not Web
|
|
19
|
+
// Search (CONTEXT.md): Router is not called, so whether the Class Session allows Web
|
|
20
|
+
// Search is never asked, and the Coding Agent fetches the page itself.
|
|
21
|
+
const READ_COMMANDS = ["open", "click", "find"];
|
|
22
|
+
// What is written back to the model; a longer ref_id is still answered, just not quoted.
|
|
23
|
+
const MAX_URL_LENGTH = 2000;
|
|
24
|
+
/** `afterRefusal`: the reply already carries a refusal this must not be read as part of. */
|
|
25
|
+
function readYourself(commands, names, afterRefusal) {
|
|
26
|
+
const refs = names.flatMap(name => Array.isArray(commands[name]) ? commands[name] : [])
|
|
27
|
+
.map(item => item?.ref_id)
|
|
28
|
+
.filter((ref) => typeof ref === "string");
|
|
29
|
+
const urls = [...new Set(refs.filter(ref => ref.length <= MAX_URL_LENGTH && /^https?:\/\/\S+$/i.test(ref)))];
|
|
30
|
+
// The model reads only `output`, where resultsText numbers the results from 1 and
|
|
31
|
+
// never prints the ref_id (turn0search0 is result [1]) that envelope gives the client.
|
|
32
|
+
const referenceId = refs.find(ref => /^\w{1,64}$/.test(ref));
|
|
33
|
+
const resultNumber = /^turn\d+search(\d+)$/.exec(referenceId ?? "");
|
|
34
|
+
return [`課堂模式沒有開網頁的指令,要讀的網頁請自行讀取。The ${names.join(", ")} command${names.length > 1 ? "s are" : " is"} not run for you in this classroom session.`,
|
|
35
|
+
...(afterRefusal ? ["This is separate from the above: reading a page is not a web search, and asking to read a page is not retrying it."] : []),
|
|
36
|
+
"To read a page, fetch it yourself.",
|
|
37
|
+
...(urls.length ? [`Requested: ${urls.join(" ")}`] : []),
|
|
38
|
+
"Fetch only the page you asked for, once, with a single shell command; keep the readable text rather than the full HTML, and truncate long output.",
|
|
39
|
+
...(resultNumber ? [`Reference ids are not kept in this classroom session: ${referenceId} is result [${Number(resultNumber[1]) + 1}] of your latest web search results, so use the URL listed there.`]
|
|
40
|
+
: referenceId ? [`A reference id such as ${referenceId} is not kept in this classroom session: use the URL of the page itself.`] : []),
|
|
41
|
+
...(names.includes("click") ? ["Link numbers are not kept either: for a numbered link, fetch the page, find the link you meant, and then fetch the URL it points to."] : []),
|
|
42
|
+
...(names.includes("find") ? ["To find text in a page, fetch the page and search its text for the pattern yourself."] : []),
|
|
43
|
+
"Do not follow links from a page on your own. To read another page, call open or click again and the same applies to that page."].join(" ");
|
|
44
|
+
}
|
|
45
|
+
function envelope(output, results = []) {
|
|
46
|
+
return Response.json({ encrypted_output: null, output,
|
|
47
|
+
results: results.map((result, index) => ({ type: "text_result", ref_id: `turn0search${index}`, url: result.url, title: result.title, snippet: result.text })) });
|
|
48
|
+
}
|
|
49
|
+
function resultsText(queries, results) {
|
|
50
|
+
if (results.length === 0)
|
|
51
|
+
return `No web search results were found for: ${queries.join(" | ")}`;
|
|
52
|
+
return [`Web search results for: ${queries.join(" | ")}`,
|
|
53
|
+
...results.map((result, index) => `[${index + 1}] ${result.title}\nURL: ${result.url}\n${result.text}`)].join("\n\n");
|
|
54
|
+
}
|
|
55
|
+
async function codexWebSearch(upstream, baseUrl) {
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = JSON.parse(upstream.body ?? "");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new types_1.AdapterError("請求正文必須是有效 JSON。");
|
|
62
|
+
}
|
|
63
|
+
const commands = (0, types_1.object)((0, types_1.object)(body, "request").commands ?? {}, "commands");
|
|
64
|
+
const queries = (Array.isArray(commands.search_query) ? commands.search_query : [])
|
|
65
|
+
.map(item => { const q = item?.q; return typeof q === "string" ? q.trim().slice(0, webSearch_1.MAX_QUERY_LENGTH) : ""; })
|
|
66
|
+
.filter(Boolean).slice(0, MAX_QUERIES);
|
|
67
|
+
// response_length only shapes a search; every other key is a command Router does not run.
|
|
68
|
+
const otherCommands = Object.keys(commands).filter(name => name !== "search_query" && name !== "response_length"
|
|
69
|
+
&& commands[name] != null && !(Array.isArray(commands[name]) && commands[name].length === 0));
|
|
70
|
+
const readCommands = otherCommands.filter(name => READ_COMMANDS.includes(name));
|
|
71
|
+
const unsupportedCommands = otherCommands.filter(name => !READ_COMMANDS.includes(name));
|
|
72
|
+
const nothingToRun = queries.length === 0 && readCommands.length === 0;
|
|
73
|
+
const reply = queries.length ? await (0, webSearch_1.webSearch)(queries, baseUrl, upstream.apiKey ?? "", upstream.signal) : undefined;
|
|
74
|
+
const failure = reply && "errorCode" in reply ? (0, webSearch_1.explainSearchFailure)(reply.errorCode) : undefined;
|
|
75
|
+
const refusals = [...(failure ? [failure] : []), ...(unsupportedCommands.length || nothingToRun ? [unsupported(unsupportedCommands)] : [])];
|
|
76
|
+
// The permission to read a page comes last, after every refusal it is an exception to.
|
|
77
|
+
const output = [...(reply && "results" in reply ? [resultsText(queries, reply.results)] : []), ...refusals,
|
|
78
|
+
...(readCommands.length ? [readYourself(commands, readCommands, refusals.length > 0)] : [])].join("\n\n");
|
|
79
|
+
return envelope(output, reply && "results" in reply ? reply.results : []);
|
|
80
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Representation repair for top-level Codex apply_patch custom-tool payloads.
|
|
3
|
+
//
|
|
4
|
+
// Some routed models decorate the first and last lines as
|
|
5
|
+
// `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
|
|
6
|
+
// valid custom-tool payloads. Repair of executable bodies is deliberately limited to
|
|
7
|
+
// unambiguous wrapper mistakes: one recognized alternate field or one complete outer
|
|
8
|
+
// Markdown fence. Ordinary `exec` JavaScript remains byte-identical.
|
|
9
|
+
//
|
|
10
|
+
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
|
|
11
|
+
// repair the one faithful reading, leave genuine patch content alone.
|
|
12
|
+
//
|
|
13
|
+
// One accepted exception, added 2026-09-05: a body that is ITSELF one complete
|
|
14
|
+
// operation-bearing envelope, submitted as the `exec` body. That shape is never valid
|
|
15
|
+
// JavaScript — `*** Begin Patch` fails to parse at the leading `**` — so it has zero
|
|
16
|
+
// executable readings and exactly one faithful one. `isCompletePatchEnvelope` recognizes
|
|
17
|
+
// it and the CALLERS retarget it to the apply_patch helper; the repair functions here are
|
|
18
|
+
// unchanged and still return every exec body byte-identical. JavaScript that merely
|
|
19
|
+
// mentions an envelope keeps a real executable reading and is left alone.
|
|
20
|
+
// See devlog/_plan/260905_apply_patch_envelope_gap.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.freeformFallbackKeys = freeformFallbackKeys;
|
|
23
|
+
exports.unwrapFreeformToolInput = unwrapFreeformToolInput;
|
|
24
|
+
exports.normalizeApplyPatchDelimiters = normalizeApplyPatchDelimiters;
|
|
25
|
+
exports.isCompletePatchEnvelope = isCompletePatchEnvelope;
|
|
26
|
+
exports.mayBecomePatchEnvelope = mayBecomePatchEnvelope;
|
|
27
|
+
exports.repairFreeformToolInput = repairFreeformToolInput;
|
|
28
|
+
const PATCH_BEGIN = "*** Begin Patch";
|
|
29
|
+
const PATCH_END = "*** End Patch";
|
|
30
|
+
const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
|
|
31
|
+
const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
|
|
32
|
+
const OUTER_MARKDOWN_CODE_FENCE = /^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```$/;
|
|
33
|
+
const FREEFORM_FALLBACK_KEYS = {
|
|
34
|
+
exec: ["code", "script", "js", "javascript", "command", "cmd", "content"],
|
|
35
|
+
apply_patch: ["patch", "content"],
|
|
36
|
+
};
|
|
37
|
+
/** The field names a tool's freeform wrapper may unwrap through, besides canonical `input`. */
|
|
38
|
+
function freeformFallbackKeys(toolName) {
|
|
39
|
+
return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
|
|
40
|
+
}
|
|
41
|
+
function stripMarkdownCodeFence(text, toolName) {
|
|
42
|
+
if (toolName !== "exec" && toolName !== "apply_patch")
|
|
43
|
+
return text;
|
|
44
|
+
const match = OUTER_MARKDOWN_CODE_FENCE.exec(text.trim());
|
|
45
|
+
return match ? match[1] : text;
|
|
46
|
+
}
|
|
47
|
+
/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
|
|
48
|
+
function unwrapFreeformToolInput(argumentsText, toolName = "") {
|
|
49
|
+
if (typeof argumentsText !== "string")
|
|
50
|
+
return "";
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(argumentsText);
|
|
53
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
54
|
+
const record = parsed;
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(record, "input")) {
|
|
56
|
+
return typeof record.input === "string"
|
|
57
|
+
? stripMarkdownCodeFence(record.input, toolName)
|
|
58
|
+
: argumentsText;
|
|
59
|
+
}
|
|
60
|
+
const fallbackKeys = freeformFallbackKeys(toolName);
|
|
61
|
+
const candidates = fallbackKeys.filter(key => typeof record[key] === "string");
|
|
62
|
+
if (candidates.length === 1) {
|
|
63
|
+
return stripMarkdownCodeFence(record[candidates[0]], toolName);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// The string is the freeform body, not nested JSON.
|
|
69
|
+
}
|
|
70
|
+
return stripMarkdownCodeFence(argumentsText, toolName);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Strip trailing `***` only from the outer lines of one complete patch.
|
|
74
|
+
* Internal patch content, incomplete envelopes, and non-patch text are exact
|
|
75
|
+
* pass-throughs.
|
|
76
|
+
*/
|
|
77
|
+
function normalizeApplyPatchDelimiters(text) {
|
|
78
|
+
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
|
|
79
|
+
if (!match)
|
|
80
|
+
return text;
|
|
81
|
+
const [, begin, beginBreak, body, endBreak, end, trailingBreak = ""] = match;
|
|
82
|
+
if (!PATCH_OPERATION_LINE.test(body))
|
|
83
|
+
return text;
|
|
84
|
+
if (begin === PATCH_BEGIN && end === PATCH_END)
|
|
85
|
+
return text;
|
|
86
|
+
return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* True when a body is one COMPLETE top-level patch envelope carrying a real file
|
|
90
|
+
* operation.
|
|
91
|
+
*
|
|
92
|
+
* A code-mode `exec` body is JavaScript, and this shape is never valid JavaScript: a
|
|
93
|
+
* leading `*** Begin Patch` fails to parse at the `**`. So a body satisfying this
|
|
94
|
+
* predicate cannot be a program the caller meant to run, which is what makes reading it
|
|
95
|
+
* as an apply_patch call the one faithful reading rather than a guess.
|
|
96
|
+
*
|
|
97
|
+
* Deliberately strict, and reusing the same two checks `normalizeApplyPatchDelimiters`
|
|
98
|
+
* uses: a second, looser notion of "looks like a patch" is how a repair boundary drifts.
|
|
99
|
+
* JavaScript that merely CONTAINS an envelope in a string, template, or comment fails the
|
|
100
|
+
* anchored match and is left alone.
|
|
101
|
+
*/
|
|
102
|
+
function isCompletePatchEnvelope(text) {
|
|
103
|
+
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
|
|
104
|
+
if (!match)
|
|
105
|
+
return false;
|
|
106
|
+
return PATCH_OPERATION_LINE.test(match[3] ?? "");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* True when a PARTIAL buffer could still grow into a complete patch envelope.
|
|
110
|
+
*
|
|
111
|
+
* Streaming decides per delta, before the body is complete, while
|
|
112
|
+
* `isCompletePatchEnvelope` can only answer once it is. Without this the bridge would
|
|
113
|
+
* stream raw envelope bytes and then replace them with compiled helper JavaScript at
|
|
114
|
+
* completion — the rewind that path explicitly refuses to perform.
|
|
115
|
+
*
|
|
116
|
+
* Deliberately one-sided: it holds a buffer that MIGHT become an envelope and never
|
|
117
|
+
* claims one will. A held buffer that turns out to be something else still reaches the
|
|
118
|
+
* client in the authoritative completed item; only the live preview is skipped.
|
|
119
|
+
*/
|
|
120
|
+
function mayBecomePatchEnvelope(text) {
|
|
121
|
+
if (text === "")
|
|
122
|
+
return false;
|
|
123
|
+
return text.startsWith(PATCH_BEGIN) || PATCH_BEGIN.startsWith(text);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Repair freeform input before Codex sees it.
|
|
127
|
+
*
|
|
128
|
+
* Only a bare or reserved-`functions` tool may receive fallback-field or outer-fence
|
|
129
|
+
* repair, and only `apply_patch` may receive delimiter repair. Remote namespaces own
|
|
130
|
+
* their grammar; those bodies and every other freeform input are unwrapped and left
|
|
131
|
+
* byte-exact.
|
|
132
|
+
*/
|
|
133
|
+
function repairFreeformToolInput(argumentsText, toolName = "", namespace) {
|
|
134
|
+
const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
|
|
135
|
+
const unwrapped = unwrapFreeformToolInput(argumentsText, ownsApplyPatchGrammar ? toolName : "");
|
|
136
|
+
return ownsApplyPatchGrammar && toolName === "apply_patch"
|
|
137
|
+
? normalizeApplyPatchDelimiters(unwrapped)
|
|
138
|
+
: unwrapped;
|
|
139
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.codexChatOptions = codexChatOptions;
|
|
4
|
+
const types_1 = require("../adapters/types");
|
|
5
|
+
// Build Chat options by projection, as opposed to validating the whole client
|
|
6
|
+
// envelope. Codex may add private transport fields (such as access_programs);
|
|
7
|
+
// those are not model options and must neither reject the turn nor reach Chat.
|
|
8
|
+
const DIRECT_FIELDS = ["temperature", "top_p", "parallel_tool_calls", "store", "metadata", "user", "stream_options", "prompt_cache_key", "prompt_cache_retention", "safety_identifier", "service_tier", "stop", "presence_penalty", "frequency_penalty"];
|
|
9
|
+
function codexChatOptions(body, responseFormat) {
|
|
10
|
+
// Explicitly reject known semantic features that this adapter cannot retain.
|
|
11
|
+
// Content, tool declarations and reasoning have their own strict validation.
|
|
12
|
+
for (const key of ["prompt", "context_management", "audio", "modalities", "prediction", "max_tool_calls", "logprobs", "top_logprobs"])
|
|
13
|
+
if (body[key] != null)
|
|
14
|
+
throw new types_1.AdapterError(`Chat 無法保留 ${key} 的請求語意。`);
|
|
15
|
+
for (const key of ["previous_response_id", "conversation"])
|
|
16
|
+
if (body[key] != null)
|
|
17
|
+
throw new types_1.AdapterError(`Chat 無法恢復 ${key};請傳送完整 input 歷史。`);
|
|
18
|
+
if (body.background != null && body.background !== false)
|
|
19
|
+
throw new types_1.AdapterError("Chat 無法轉換背景執行請求。");
|
|
20
|
+
if (body.truncation != null && body.truncation !== "disabled")
|
|
21
|
+
throw new types_1.AdapterError("Chat 無法保留自動截斷 input 的設定。");
|
|
22
|
+
if (body.include !== undefined)
|
|
23
|
+
for (const include of (0, types_1.list)(body.include, "include")) {
|
|
24
|
+
// Chat has no encrypted reasoning to include; this only requests optional
|
|
25
|
+
// output. Replayed encrypted_content in input is skipped as opaque.
|
|
26
|
+
if (include !== "reasoning.encrypted_content")
|
|
27
|
+
throw new types_1.AdapterError(`Chat 無法轉換 include 選項 ${String(include)}。`);
|
|
28
|
+
}
|
|
29
|
+
const fields = Object.fromEntries(DIRECT_FIELDS.filter(key => body[key] !== undefined).map(key => [key, body[key]]));
|
|
30
|
+
if (body.max_output_tokens !== undefined)
|
|
31
|
+
fields.max_completion_tokens = body.max_output_tokens;
|
|
32
|
+
if (body.reasoning !== undefined) {
|
|
33
|
+
const reasoning = (0, types_1.object)(body.reasoning, "reasoning");
|
|
34
|
+
// Match opencodex's reasoning-config projection: context and future
|
|
35
|
+
// config extensions are not Chat options. Input reasoning items are
|
|
36
|
+
// handled separately when the input history is converted.
|
|
37
|
+
if (reasoning.summary != null && !["auto", "concise", "detailed", "none"].includes(String(reasoning.summary)))
|
|
38
|
+
throw new types_1.AdapterError("Chat 無法轉換此 reasoning summary 設定。");
|
|
39
|
+
if (reasoning.effort !== undefined)
|
|
40
|
+
fields.reasoning_effort = reasoning.effort === "ultra" ? "max" : reasoning.effort;
|
|
41
|
+
}
|
|
42
|
+
if (body.text !== undefined) {
|
|
43
|
+
const text = (0, types_1.object)(body.text, "text");
|
|
44
|
+
(0, types_1.allowedFields)(text, ["format", "verbosity"], "Chat text");
|
|
45
|
+
if (text.format !== undefined)
|
|
46
|
+
fields.response_format = responseFormat(text.format);
|
|
47
|
+
if (text.verbosity !== undefined)
|
|
48
|
+
fields.verbosity = text.verbosity;
|
|
49
|
+
}
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.codexChatTools = codexChatTools;
|
|
4
|
+
exports.restoreChatToolEvent = restoreChatToolEvent;
|
|
5
|
+
exports.restoreChatToolResponse = restoreChatToolResponse;
|
|
6
|
+
exports.restoreChatToolStream = restoreChatToolStream;
|
|
7
|
+
const types_1 = require("../adapters/types");
|
|
8
|
+
const request_1 = require("./request");
|
|
9
|
+
/** Chat has flat tool names; keep an explicit mapping back to Codex namespaces. */
|
|
10
|
+
function codexChatTools(raw) {
|
|
11
|
+
const body = (0, request_1.normalizeCodexRequest)(raw);
|
|
12
|
+
const identities = new Map();
|
|
13
|
+
const aliases = new Map();
|
|
14
|
+
const tools = body.tools === undefined ? [] : (0, types_1.list)(body.tools, "tools");
|
|
15
|
+
const occupied = new Set(tools.map(value => (0, types_1.object)(value, "tool").name));
|
|
16
|
+
const flattened = tools.flatMap(value => {
|
|
17
|
+
const tool = (0, types_1.object)(value, "tool");
|
|
18
|
+
if (tool.type !== "namespace")
|
|
19
|
+
return [tool];
|
|
20
|
+
(0, types_1.allowedFields)(tool, ["type", "name", "description", "tools"], "Chat namespace");
|
|
21
|
+
const namespace = (0, types_1.string)(tool.name, "namespace");
|
|
22
|
+
return (0, types_1.list)(tool.tools, "namespace tools").map(value => {
|
|
23
|
+
const child = (0, types_1.object)(value, "namespace tool");
|
|
24
|
+
if (child.type !== "function" && child.type !== "custom")
|
|
25
|
+
throw new types_1.AdapterError("Chat 無法轉換此 namespace 工具。");
|
|
26
|
+
const name = (0, types_1.string)(child.name, "tool name"), key = JSON.stringify([namespace, name]);
|
|
27
|
+
if (aliases.has(key))
|
|
28
|
+
throw new types_1.AdapterError("Chat namespace 工具名稱重複。");
|
|
29
|
+
let alias = `${namespace}__${name}`;
|
|
30
|
+
if (alias.length > 64 || !/^[a-zA-Z0-9_-]+$/.test(alias) || occupied.has(alias)) {
|
|
31
|
+
let index = identities.size;
|
|
32
|
+
do {
|
|
33
|
+
alias = `vanspod_tool_${index++}`;
|
|
34
|
+
} while (occupied.has(alias));
|
|
35
|
+
}
|
|
36
|
+
occupied.add(alias);
|
|
37
|
+
aliases.set(key, alias);
|
|
38
|
+
identities.set(alias, { name, namespace });
|
|
39
|
+
return { ...child, name: alias, description: `${namespace}.${name}${tool.description ? ` (${tool.description})` : ""}${child.description ? `: ${child.description}` : ""}` };
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
const translate = (value) => {
|
|
43
|
+
if (value.namespace === undefined)
|
|
44
|
+
return value;
|
|
45
|
+
const alias = aliases.get(JSON.stringify([value.namespace, value.name]));
|
|
46
|
+
if (!alias)
|
|
47
|
+
throw new types_1.AdapterError("Chat 無法找到此 namespace 工具的宣告。");
|
|
48
|
+
const { namespace: _namespace, ...rest } = value;
|
|
49
|
+
return { ...rest, name: alias };
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
body: { ...body, ...(body.tools !== undefined ? { tools: flattened } : {}),
|
|
53
|
+
...(Array.isArray(body.input) ? { input: body.input.map(value => {
|
|
54
|
+
const item = (0, types_1.object)(value, "input item");
|
|
55
|
+
return item.type === "function_call" || item.type === "custom_tool_call" ? translate(item) : item;
|
|
56
|
+
}) } : {}),
|
|
57
|
+
...(body.tool_choice && typeof body.tool_choice === "object" ? { tool_choice: translate((0, types_1.object)(body.tool_choice, "tool_choice")) } : {}),
|
|
58
|
+
},
|
|
59
|
+
restore(item) {
|
|
60
|
+
if (item.type !== "function_call" && item.type !== "custom_tool_call")
|
|
61
|
+
return item;
|
|
62
|
+
const identity = typeof item.name === "string" ? identities.get(item.name) : undefined;
|
|
63
|
+
return identity ? { ...item, ...identity } : item;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function restoreChatToolEvent(data, restore) {
|
|
68
|
+
if (data.item)
|
|
69
|
+
return { ...data, item: restore((0, types_1.object)(data.item, "output item")) };
|
|
70
|
+
if (data.response)
|
|
71
|
+
return { ...data, response: restoreChatToolResponse((0, types_1.object)(data.response, "response"), restore) };
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
function restoreChatToolResponse(body, restore) {
|
|
75
|
+
return Array.isArray(body.output) ? { ...body, output: body.output.map(value => restore((0, types_1.object)(value, "output item"))) } : body;
|
|
76
|
+
}
|
|
77
|
+
async function* restoreChatToolStream(source, restore) {
|
|
78
|
+
for await (const chunk of source)
|
|
79
|
+
yield chunk.replace(/^data: (.+)$/gm, (line, raw) => raw === "[DONE]" ? line : `data: ${JSON.stringify(restoreChatToolEvent((0, types_1.object)(JSON.parse(raw), "event"), restore))}`);
|
|
80
|
+
}
|