mirror of
https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension.git
synced 2026-08-10 21:50:29 -06:00
101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
// Pure URL construction. No browser APIs, so node:test imports this directly.
|
|
//
|
|
// Every ChatGPT parameter below is undocumented by OpenAI and has broken before.
|
|
// This module's contract is only that it emits a well-formed URL; whether
|
|
// ChatGPT honours model / temporary-chat / hints is out of our control, which is
|
|
// why each is independently omittable and every label is hint-strength.
|
|
//
|
|
// The root path is mandatory: chatgpt.com/search began returning 404 in
|
|
// mid-2025, and the root form is what OpenAI's own extension uses.
|
|
|
|
const BASE = "https://chatgpt.com/";
|
|
|
|
export const MAX_URL_CHARS = 8000;
|
|
|
|
// Fraction of the kept text within which a whitespace cut is preferred.
|
|
const WHITESPACE_WINDOW = 0.9;
|
|
|
|
function applyTemplate(template, query) {
|
|
if (query === "") {
|
|
return "";
|
|
}
|
|
const tpl = typeof template === "string" ? template : "";
|
|
if (tpl.includes("{query}")) {
|
|
return tpl.replaceAll("{query}", query);
|
|
}
|
|
const trimmed = tpl.trim();
|
|
return trimmed === "" ? query : `${trimmed} ${query}`;
|
|
}
|
|
|
|
function addOptionalParams(url, settings) {
|
|
const model = typeof settings.model === "string" ? settings.model.trim() : "";
|
|
if (model !== "") {
|
|
url.searchParams.set("model", model);
|
|
}
|
|
if (settings.temporaryChat) {
|
|
url.searchParams.set("temporary-chat", "true");
|
|
}
|
|
if (settings.webSearch) {
|
|
url.searchParams.set("hints", "search");
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function lastWhitespaceIndex(text) {
|
|
for (let i = text.length - 1; i >= 0; i -= 1) {
|
|
if (/\s/.test(text[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Returns the longest prefix of `prompt` whose finished URL fits the budget.
|
|
// Slicing happens by CODE POINT on the decoded string, so a surrogate pair can
|
|
// never be split and a percent-escape can never be severed (encoding happens
|
|
// afterwards, via URLSearchParams).
|
|
function fitToBudget(prompt, settings) {
|
|
const codePoints = Array.from(prompt);
|
|
|
|
const urlLengthFor = (count) => {
|
|
const url = new URL(BASE);
|
|
url.searchParams.set("q", codePoints.slice(0, count).join(""));
|
|
addOptionalParams(url, settings);
|
|
return url.href.length;
|
|
};
|
|
|
|
if (urlLengthFor(codePoints.length) <= MAX_URL_CHARS) {
|
|
return prompt;
|
|
}
|
|
|
|
let low = 0;
|
|
let high = codePoints.length;
|
|
while (low < high) {
|
|
const mid = Math.floor((low + high + 1) / 2);
|
|
if (urlLengthFor(mid) <= MAX_URL_CHARS) {
|
|
low = mid;
|
|
} else {
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
|
|
const kept = codePoints.slice(0, low).join("");
|
|
const boundary = Math.floor(kept.length * WHITESPACE_WINDOW);
|
|
const cut = lastWhitespaceIndex(kept);
|
|
return cut >= boundary && cut > 0 ? kept.slice(0, cut) : kept;
|
|
}
|
|
|
|
export function buildChatGptUrl(settings, rawQuery) {
|
|
const query = String(rawQuery ?? "").trim();
|
|
const prompt = fitToBudget(applyTemplate(settings.promptTemplate, query), settings);
|
|
|
|
// q first, matching the most-cited working examples. Total serialized length
|
|
// is order-independent, so measuring without q above is sound.
|
|
const url = new URL(BASE);
|
|
if (prompt !== "") {
|
|
url.searchParams.set("q", prompt);
|
|
}
|
|
addOptionalParams(url, settings);
|
|
return url.href;
|
|
}
|