bunnyquery 1.15.0 → 1.15.1
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 +4 -2
- package/bunnyquery.js +135 -15
- package/dist/engine.cjs +131 -13
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +179 -27
- package/dist/engine.d.ts +179 -27
- package/dist/engine.mjs +122 -14
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/config.ts +76 -23
- package/src/engine/duplicates.ts +115 -0
- package/src/engine/host.ts +12 -0
- package/src/engine/index.ts +20 -0
- package/src/engine/project_settings.ts +47 -6
- package/src/engine/prompts/index.ts +1 -0
- package/src/engine/prompts/indexing_user_message.ts +23 -0
- package/src/engine/requests.ts +11 -8
- package/src/engine/session.ts +45 -3
package/README.md
CHANGED
|
@@ -140,7 +140,8 @@ Mounts the widget. Returns the `BunnyQuery` object.
|
|
|
140
140
|
| `attachmentParsers` | `array` | `null` | Client-side attachment parsers. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
141
141
|
| `windowedIndexing` | `boolean` | `true` | Server-driven windowed indexing for text and grid files (see [file types](#supported-file-types)). Pass `false` to fall back to agent-driven paging, which keeps the traversal inside the model's turn budget and the tab open. |
|
|
142
142
|
| `allowAnonymous` | `boolean` | `null` | Open the chat with no login for visitors without an account. `null` follows the project's own "Allow anonymous users" setting (`getConnectionInfo().conf.require_login`); `true`/`false` pins it. |
|
|
143
|
-
| `webAccess` | `boolean` | `null` | Whether the chat's answers may reach the open web (Claude `web_fetch`, OpenAI `web_search`).
|
|
143
|
+
| `webAccess` | `boolean` | `null` | Whether the chat's answers may reach the open web (Claude `web_fetch`, OpenAI `web_search`). The project's own **Web access for answers** switch on its Project Settings page (the `web_access` key of the project's `bq::settings` record, **off** unless the owner turned it on) is the **ceiling**, and this option may only restrict below it: `effective = projectSetting && webAccess !== false`. `null` or `true` follows the project; `false` turns the web off for this page whatever the project says; a value above the project setting has no effect. With the web off, answers come only from the project's files and database. Indexing is not affected. |
|
|
144
|
+
| `skipDuplicateFiles` | `boolean` | `null` | Whether a chat attachment the signed-in user **already uploaded** to this project (same stored file name, same size in bytes, same uploader, found from the file's `src::` record) is skipped: it is neither uploaded nor indexed again, the chip reads "already uploaded", and the message uses the stored copy's url. The project's **Skip duplicate files** setting (the `skip_duplicate_files` key of `bq::settings`, **on** unless the owner turned it off) is the **ceiling**, and this option may only restrict below it: `effective = projectSetting && skipDuplicateFiles !== false`. `null` or `true` follows the project; `false` lets this page re-upload duplicates; a value above the project setting has no effect. The check never blocks an upload: a failed lookup uploads as usual. |
|
|
144
145
|
| `liveStreaming` | `boolean` | `true` | Paint a chat answer into its bubble as it arrives, instead of at the end. On by default; pass `false` for buffered replies. Still a **request**, not a switch: the widget honours it only when your page's `skapi-js` actually carries skapi's half of the stream flag (it checks for `forwardRequestStream` and `forwardRequestFinalize`, or their older names `clientSecretRequestStream` and `clientSecretRequestFinalize` on an earlier `skapi-js`), and otherwise warns once and falls back to buffered replies. An older SDK silently drops the flag, which would leave the destination streaming SSE into a buffered row that reads back empty. It also needs a polling worker that relays the response bytes, which every deployed region now has; against a self-hosted worker that does not, pass `false`. |
|
|
145
146
|
| `liveStreamingRealtime` | `boolean` | `false` | Deliver streamed chunks over skapi's websocket instead of waiting for the next poll tick. Requires `liveStreaming`. Off unless you ask for it: skapi's `joinRealtime` **replaces** the connection's group, so for the length of a turn it takes the room out from under whatever else your app uses realtime for. Purely an accelerator; with it off the reply still streams, on the poll's cadence. |
|
|
146
147
|
|
|
@@ -492,7 +493,8 @@ helpers. See the `.d.ts` shipped with `bunnyquery/engine`.
|
|
|
492
493
|
| `attachmentParsers` | `array?` | Client-side attachment parsers, registered at configure time. More can be added later with `registerAttachmentParser()`. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
493
494
|
| `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default in the engine; the widget passes it as `true`. The deployed skapi workers support it; only leave it off against a self-hosted worker that does not yet strip the `_skapi_window` directive, where it would reach the provider as an unknown body field and fail the call terminally with no retry. |
|
|
494
495
|
| `liveStreaming` | `boolean?` | Opt in to **live streaming** of chat turns. Off by default in the engine (the widget passes it as `true`), and the backend ships first: a streamed row settles with a status and NO body (the answer was the stream), so against a worker that does not relay, the turn reads back empty. Pair it with `forwardRequestFinalize` and `forwardRequestStream`, and gate it on `skapiSupportsStreaming(skapi)`. |
|
|
495
|
-
| `webAccess` | `boolean?` | Whether a chat turn may reach the open web: Claude's `web_fetch` and OpenAI's `web_search` are attached to a chat request only when this resolves to `true`.
|
|
496
|
+
| `webAccess` | `boolean?` | Whether a chat turn may reach the open web: Claude's `web_fetch` and OpenAI's `web_search` are attached to a chat request only when this resolves to `true`. The project's own `web_access` setting (the `bq::settings` record, read through `configureProjectSettings`; **off** unless the project owner turned it on) is the **ceiling** and this option may only restrict below it: `effective = projectSetting && webAccess !== false`. Leave it out or pass `true` to follow the project; `false` turns the web off for your host; a value above the project setting has no effect. `webAccessEnabled(serviceId)` and `resolveWebAccess(serviceId, override?)` answer the resolved switch (sync from the cache, or awaiting the settings fetch; `override` is a per-call host value that restricts the same way). Indexing passes keep their tools regardless. Pass the same value to `buildChatSystemPrompt({ webAccess })` so the prompt matches the tool list. |
|
|
497
|
+
| `skipDuplicateFiles` | `boolean?` | Whether an attachment the signed-in user already uploaded to the project (same stored name, same size, same uploader) is skipped by `ChatSession`'s upload path: not uploaded, not indexed, the turn using the stored copy's url. The project's `skip_duplicate_files` setting (**on** unless the owner turned it off) is the **ceiling** and this option may only restrict: `effective = projectSetting && skipDuplicateFiles !== false`. The check runs only when the host implements the optional `findDuplicateUpload(file)` hook; `findDuplicateUpload(skapi, { name, size }, userId, { service? })` from the barrel is the ready-made implementation (an ends-with `getUniqueId` lookup narrowed to `src::<name>` and `src::.../<name>`, then the first record whose `data.size_bytes` and `user_id` match; it never throws, a failed lookup answers `null` and the file uploads as usual). `skipDuplicateFilesEnabled(serviceId)` and `resolveSkipDuplicateFiles(serviceId)` answer the resolved switch. |
|
|
496
498
|
| `forwardRequestFinalize` | `function?` | `skapi.forwardRequestFinalize`, bound to your Skapi instance. Stores the version of a streamed turn that history keeps (the engine sends the assembled provider body, so it reads back exactly like a buffered turn) and releases that request's chunks. Without it a streamed turn is never finalized and its row stays empty. |
|
|
497
499
|
| `forwardRequestStream` | `function?` | `skapi.forwardRequestStream`, bound to your Skapi instance. The **second half of the durability guarantee**: a row that settles while no poll is attached (closed tab, discarded background tab, slept device) is never finalized, so its answer stays in the chunk store and its history row is terminal and empty. Given the request id this drains that turn's chunks in one pass; the engine parses them exactly as it parses a live stream and finalizes what it read, so each row is recovered at most once. Without it the engine mints no recovery marker at all and behaves as it did before streaming. |
|
|
498
500
|
| `onLiveStreamUpdate` | `function?` | Observation hook for a streaming turn (`{ serverItemId, ownerKey, phase, text, thinkingText, toolNames, complete, errored }`). The engine already paints the answer text itself, so this is only for affordances it does not decide the presentation of. Never throw from it. |
|
package/bunnyquery.js
CHANGED
|
@@ -61,6 +61,13 @@
|
|
|
61
61
|
function webAccessFrom(data) {
|
|
62
62
|
return data ? normalizeWebAccess(data.web_access) : DEFAULT_WEB_ACCESS;
|
|
63
63
|
}
|
|
64
|
+
var DEFAULT_SKIP_DUPLICATE_FILES = true;
|
|
65
|
+
function normalizeSkipDuplicateFiles(value) {
|
|
66
|
+
return value !== false;
|
|
67
|
+
}
|
|
68
|
+
function skipDuplicateFilesFrom(data) {
|
|
69
|
+
return data ? normalizeSkipDuplicateFiles(data.skip_duplicate_files) : DEFAULT_SKIP_DUPLICATE_FILES;
|
|
70
|
+
}
|
|
64
71
|
var CHAT_GREETING_MAX_LENGTH = 400;
|
|
65
72
|
function normalizeChatGreeting(value) {
|
|
66
73
|
if (typeof value !== "string") return "";
|
|
@@ -203,16 +210,25 @@
|
|
|
203
210
|
function liveStreamingEnabled() {
|
|
204
211
|
return _config?.liveStreaming === true;
|
|
205
212
|
}
|
|
213
|
+
function underCeiling(projectSetting, option, override) {
|
|
214
|
+
if (!projectSetting) return false;
|
|
215
|
+
if (option === false) return false;
|
|
216
|
+
if (override === false) return false;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
206
219
|
function webAccessEnabled(service) {
|
|
207
220
|
const opt = _config ? _config.webAccess : void 0;
|
|
208
|
-
|
|
209
|
-
return webAccessFrom(service ? cachedProjectSettings(service) : null);
|
|
221
|
+
return underCeiling(webAccessFrom(service ? cachedProjectSettings(service) : null), opt);
|
|
210
222
|
}
|
|
211
|
-
function resolveWebAccess(service) {
|
|
223
|
+
function resolveWebAccess(service, override) {
|
|
212
224
|
const opt = _config ? _config.webAccess : void 0;
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
if (!service) return Promise.resolve(underCeiling(webAccessFrom(null), opt, override));
|
|
226
|
+
return loadProjectSettings(service).then(webAccessFrom, () => false).then((setting) => underCeiling(setting, opt, override));
|
|
227
|
+
}
|
|
228
|
+
function resolveSkipDuplicateFiles(service) {
|
|
229
|
+
const opt = _config ? _config.skipDuplicateFiles : void 0;
|
|
230
|
+
if (!service) return Promise.resolve(underCeiling(skipDuplicateFilesFrom(null), opt));
|
|
231
|
+
return loadProjectSettings(service).then(skipDuplicateFilesFrom, () => skipDuplicateFilesFrom(null)).then((setting) => underCeiling(setting, opt));
|
|
216
232
|
}
|
|
217
233
|
function streamRecoveryEnabled() {
|
|
218
234
|
if (!_config) return false;
|
|
@@ -228,6 +244,54 @@
|
|
|
228
244
|
return p === void 0 ? {} : { poll: p };
|
|
229
245
|
}
|
|
230
246
|
|
|
247
|
+
// src/engine/duplicates.ts
|
|
248
|
+
var DUPLICATE_LOOKUP_MAX_CANDIDATES = 25;
|
|
249
|
+
var SRC_PREFIX = "src::";
|
|
250
|
+
function isDuplicateCandidateId(uniqueId, name) {
|
|
251
|
+
if (typeof uniqueId !== "string" || !name) return false;
|
|
252
|
+
if (uniqueId.indexOf(SRC_PREFIX) !== 0) return false;
|
|
253
|
+
if (uniqueId === SRC_PREFIX + name) return true;
|
|
254
|
+
const tail = "/" + name;
|
|
255
|
+
return uniqueId.length > SRC_PREFIX.length + tail.length && uniqueId.slice(-tail.length) === tail;
|
|
256
|
+
}
|
|
257
|
+
function listOf(res) {
|
|
258
|
+
if (!res) return [];
|
|
259
|
+
if (Array.isArray(res)) return res;
|
|
260
|
+
return Array.isArray(res.list) ? res.list : [];
|
|
261
|
+
}
|
|
262
|
+
async function findDuplicateUpload(skapi, file, userId, opts) {
|
|
263
|
+
try {
|
|
264
|
+
if (!skapi || typeof skapi.getUniqueId !== "function" || typeof skapi.getRecords !== "function") return null;
|
|
265
|
+
const name = file && typeof file.name === "string" ? file.name : "";
|
|
266
|
+
const size = file && typeof file.size === "number" ? file.size : NaN;
|
|
267
|
+
if (!name || !(size >= 0) || !userId) return null;
|
|
268
|
+
const service = opts && opts.service ? { service: opts.service } : {};
|
|
269
|
+
const found = listOf(await skapi.getUniqueId(Object.assign({ unique_id: name, condition: "<=" }, service)));
|
|
270
|
+
const ids = [];
|
|
271
|
+
for (const item of found) {
|
|
272
|
+
const id = item && typeof item === "object" ? item.unique_id : item;
|
|
273
|
+
if (isDuplicateCandidateId(id, name) && ids.indexOf(id) === -1) ids.push(id);
|
|
274
|
+
if (ids.length >= DUPLICATE_LOOKUP_MAX_CANDIDATES) break;
|
|
275
|
+
}
|
|
276
|
+
for (const uniqueId of ids) {
|
|
277
|
+
let record = null;
|
|
278
|
+
try {
|
|
279
|
+
record = listOf(await skapi.getRecords(Object.assign({ unique_id: uniqueId }, service)))[0] || null;
|
|
280
|
+
} catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (!record || typeof record !== "object") continue;
|
|
284
|
+
const data = record.data && typeof record.data === "object" ? record.data : null;
|
|
285
|
+
if (!data || data.size_bytes !== size) continue;
|
|
286
|
+
if (record.user_id !== userId) continue;
|
|
287
|
+
return { storagePath: uniqueId.slice(SRC_PREFIX.length), uniqueId, record };
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
231
295
|
// src/engine/office.ts
|
|
232
296
|
var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
233
297
|
"doc",
|
|
@@ -637,6 +701,10 @@ Records for the earlier pages are ALREADY saved (they reference "${src}"). The N
|
|
|
637
701
|
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
638
702
|
`;
|
|
639
703
|
}
|
|
704
|
+
var PAGE_TEXT_TABLE = "page_texts";
|
|
705
|
+
var PAGE_TEXT_RULE = `PAGE TEXT for pages WITHOUT a text layer: for EVERY page whose label (its first label, when the page is split into strips) carries NO "Embedded text layer" block, ALSO post exactly ONE record in table "${PAGE_TEXT_TABLE}" with data {"page": <its document page number from the label, 1-based>, "page_text": "<the page's complete text exactly as printed, verbatim, in reading order, nothing summarised or left out>"}, reference "src::" + the storage path above, in addition to the records above. A page WITH an embedded text layer gets NO such record: its text is already stored.
|
|
706
|
+
|
|
707
|
+
`;
|
|
640
708
|
function buildRenderDatafy(placeholder) {
|
|
641
709
|
return `
|
|
642
710
|
${placeholder}
|
|
@@ -647,7 +715,7 @@ Each image is preceded by a label giving its DOCUMENT PAGE number. That label is
|
|
|
647
715
|
|
|
648
716
|
Transcribe COMPLETELY, not representatively. A table with twenty rows gets twenty records, not a sample of the first few - if a page has more rows than you can save comfortably, still save them all rather than summarising. Where a page carries an embedded text layer it is quoted above that page's image: it is the exact text and should be preferred over reading the pixels, with the image used for layout, tables, stamps and handwriting.
|
|
649
717
|
|
|
650
|
-
Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
|
|
718
|
+
` + PAGE_TEXT_RULE + `Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
|
|
651
719
|
}
|
|
652
720
|
function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
|
|
653
721
|
const src = `src::${attachment.storagePath}`;
|
|
@@ -1151,7 +1219,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1151
1219
|
var FILE_KEYS = ["path", "storage_path", "storagePath", "file", "file_path", "filePath"];
|
|
1152
1220
|
var TABLE_KEYS = ["table_name", "tableName"];
|
|
1153
1221
|
var GROUP_KEYS = ["access_group", "accessGroup"];
|
|
1154
|
-
var
|
|
1222
|
+
var SRC_PREFIX2 = "src::";
|
|
1155
1223
|
var RESULT_WALK_MAX_NODES = 400;
|
|
1156
1224
|
var RESULT_WALK_MAX_DEPTH = 5;
|
|
1157
1225
|
function isWebToolName(name) {
|
|
@@ -1180,8 +1248,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1180
1248
|
}
|
|
1181
1249
|
function pathFromSrcId(value) {
|
|
1182
1250
|
if (typeof value !== "string") return null;
|
|
1183
|
-
if (value.indexOf(
|
|
1184
|
-
var rest = value.slice(
|
|
1251
|
+
if (value.indexOf(SRC_PREFIX2) !== 0) return null;
|
|
1252
|
+
var rest = value.slice(SRC_PREFIX2.length).trim();
|
|
1185
1253
|
return rest ? rest : null;
|
|
1186
1254
|
}
|
|
1187
1255
|
function parseJsonLoose(value) {
|
|
@@ -2811,7 +2879,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2811
2879
|
fileUrls,
|
|
2812
2880
|
webAccess
|
|
2813
2881
|
}) {
|
|
2814
|
-
const web = typeof webAccess === "boolean" ? webAccess :
|
|
2882
|
+
const web = await resolveWebAccess(service, typeof webAccess === "boolean" ? webAccess : void 0);
|
|
2815
2883
|
const mcpServerDefinition = {
|
|
2816
2884
|
type: "url",
|
|
2817
2885
|
name: mcpServer.name,
|
|
@@ -8341,11 +8409,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8341
8409
|
if (!total) return Promise.reject(new Error("Empty attachment"));
|
|
8342
8410
|
var urls = [];
|
|
8343
8411
|
var anyIndexFailed = false;
|
|
8412
|
+
var reusedCount = 0;
|
|
8413
|
+
att.reused = false;
|
|
8344
8414
|
var chain = Promise.resolve();
|
|
8415
|
+
var findDuplicate = function(member) {
|
|
8416
|
+
if (typeof self.host.findDuplicateUpload !== "function") return Promise.resolve(null);
|
|
8417
|
+
var storedName = String(member.storagePath || "").split("/").filter(Boolean).pop() || member.file.name;
|
|
8418
|
+
return resolveSkipDuplicateFiles(id.projectId).then(function(on) {
|
|
8419
|
+
if (!on) return null;
|
|
8420
|
+
return Promise.resolve(self.host.findDuplicateUpload({ name: storedName, size: member.file.size })).then(function(hit) {
|
|
8421
|
+
return hit && typeof hit.storagePath === "string" && hit.storagePath ? hit.storagePath : null;
|
|
8422
|
+
});
|
|
8423
|
+
}).catch(function() {
|
|
8424
|
+
return null;
|
|
8425
|
+
});
|
|
8426
|
+
};
|
|
8345
8427
|
members.forEach(function(member, idx) {
|
|
8346
8428
|
chain = chain.then(function() {
|
|
8347
8429
|
var hadExists = false;
|
|
8348
8430
|
var skipped = false;
|
|
8431
|
+
var duplicateOf = null;
|
|
8349
8432
|
var existedBefore = false;
|
|
8350
8433
|
var onProg = function(p) {
|
|
8351
8434
|
if (p && p.total) {
|
|
@@ -8364,7 +8447,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8364
8447
|
}
|
|
8365
8448
|
});
|
|
8366
8449
|
};
|
|
8367
|
-
return
|
|
8450
|
+
return findDuplicate(member).then(function(dup) {
|
|
8451
|
+
if (dup) {
|
|
8452
|
+
duplicateOf = dup;
|
|
8453
|
+
console.log("[chat-engine] skipping an upload the user already has:", member.storagePath, "->", dup);
|
|
8454
|
+
return;
|
|
8455
|
+
}
|
|
8456
|
+
return doMemberUpload(true);
|
|
8457
|
+
}).catch(function(err) {
|
|
8368
8458
|
var code = err && (err.code || err.body && err.body.code);
|
|
8369
8459
|
var msg = err && (err.message || err.body && err.body.message || (typeof err === "string" ? err : ""));
|
|
8370
8460
|
var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
|
|
@@ -8384,9 +8474,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8384
8474
|
});
|
|
8385
8475
|
}).then(function() {
|
|
8386
8476
|
if (skipped) return;
|
|
8387
|
-
return self.host.getTemporaryUrl(member.storagePath);
|
|
8477
|
+
return self.host.getTemporaryUrl(duplicateOf || member.storagePath);
|
|
8388
8478
|
}).then(function(url) {
|
|
8389
8479
|
if (skipped) return;
|
|
8480
|
+
if (duplicateOf) {
|
|
8481
|
+
urls.push({ name: member.relPath, url, storagePath: duplicateOf });
|
|
8482
|
+
reusedCount += 1;
|
|
8483
|
+
if (att.kind !== "folder") {
|
|
8484
|
+
att.uploadedUrl = url;
|
|
8485
|
+
att.storagePath = duplicateOf;
|
|
8486
|
+
att.reused = true;
|
|
8487
|
+
} else att.reusedCount = reusedCount;
|
|
8488
|
+
self.host.renderAttachmentChips();
|
|
8489
|
+
return;
|
|
8490
|
+
}
|
|
8390
8491
|
urls.push({ name: member.relPath, url, storagePath: member.storagePath });
|
|
8391
8492
|
if (att.kind !== "folder") {
|
|
8392
8493
|
att.uploadedUrl = url;
|
|
@@ -9018,7 +9119,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9018
9119
|
(function() {
|
|
9019
9120
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
9020
9121
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
9021
|
-
var BQ_VERSION = "1.15.
|
|
9122
|
+
var BQ_VERSION = "1.15.1" ;
|
|
9022
9123
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
9023
9124
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
9024
9125
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -10858,6 +10959,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10858
10959
|
uploadAccessGroup: function(path) {
|
|
10859
10960
|
return resolveUploadAccessGroup(path);
|
|
10860
10961
|
},
|
|
10962
|
+
// "Already uploaded by this user?" from the src:: file records. The engine
|
|
10963
|
+
// asks only while the skip-duplicates switch is on; null means upload.
|
|
10964
|
+
findDuplicateUpload: function(file) {
|
|
10965
|
+
var uid = S.user && S.user.user_id;
|
|
10966
|
+
if (!uid) return Promise.resolve(null);
|
|
10967
|
+
return findDuplicateUpload(S.skapi, file, uid, { service: S.projectId });
|
|
10968
|
+
},
|
|
10861
10969
|
storagePathFor: function(relPath) {
|
|
10862
10970
|
return attachmentStoragePath(relPath);
|
|
10863
10971
|
},
|
|
@@ -11839,7 +11947,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11839
11947
|
});
|
|
11840
11948
|
chip.appendChild(h("span", { class: "bq-attachment-icon", html: isFolder ? FOLDER_ICON_SVG : FILE_ICON_SVG }));
|
|
11841
11949
|
chip.appendChild(h("span", { class: "bq-attachment-name", text: att.name, title: att.name }));
|
|
11842
|
-
var meta = att.status === "error" ? "(Failed)" : att.status === "indexError" ? "(Error)" : preparing ? "Preparing" : finalizing ? "Finalizing" : att.status === "uploading" ? att.progress + "%" : isFolder ? "(" + (att.files ? att.files.length : 0) + ")" : formatBytes(att.file ? att.file.size : att.size);
|
|
11950
|
+
var meta = att.status === "error" ? "(Failed)" : att.status === "indexError" ? "(Error)" : preparing ? "Preparing" : finalizing ? "Finalizing" : att.status === "uploading" ? att.progress + "%" : isFolder ? "(" + (att.files ? att.files.length : 0) + ")" : att.reused ? "already uploaded" : formatBytes(att.file ? att.file.size : att.size);
|
|
11843
11951
|
chip.appendChild(h("span", { class: "bq-attachment-meta", text: meta }));
|
|
11844
11952
|
if (clickable) chip.appendChild(h("span", { class: "bq-attachment-arrow", text: "\u2197" }));
|
|
11845
11953
|
if (att.status !== "uploading" && att.status !== "done") {
|
|
@@ -13663,6 +13771,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13663
13771
|
// true/false pins it for this page, both ways. Read at
|
|
13664
13772
|
// configureChatEngine time and when each chat request is built.
|
|
13665
13773
|
webAccess: null,
|
|
13774
|
+
// Whether a chat attachment the signed-in user ALREADY uploaded to this
|
|
13775
|
+
// project (same file name, same size, same uploader) is skipped: not
|
|
13776
|
+
// uploaded again, not indexed again, the message using the stored
|
|
13777
|
+
// copy. null = follow the project's own "Skip duplicate files" setting
|
|
13778
|
+
// (`skip_duplicate_files` in the bq::settings record), which is ON
|
|
13779
|
+
// unless the owner turned it off. The project setting is the ceiling:
|
|
13780
|
+
// false here restricts below it for this page, true has no effect on
|
|
13781
|
+
// a project that turned it off.
|
|
13782
|
+
skipDuplicateFiles: null,
|
|
13666
13783
|
// Server-driven windowed indexing; read at configureChatEngine time.
|
|
13667
13784
|
// Listed here so the defaults object is the full opt surface.
|
|
13668
13785
|
windowedIndexing: true,
|
|
@@ -13744,6 +13861,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13744
13861
|
// chat request (off unless the owner turned it on). Passing the null
|
|
13745
13862
|
// default through would read as a decision.
|
|
13746
13863
|
webAccess: typeof S.opts.webAccess === "boolean" ? S.opts.webAccess : void 0,
|
|
13864
|
+
// Skip duplicate uploads. Same hand-over rule: only a pinned boolean is
|
|
13865
|
+
// passed, and the project's setting is the ceiling the engine applies.
|
|
13866
|
+
skipDuplicateFiles: typeof S.opts.skipDuplicateFiles === "boolean" ? S.opts.skipDuplicateFiles : void 0,
|
|
13747
13867
|
// ---- live streaming (mirrored in agent.vue's ai_agent.ts) --------
|
|
13748
13868
|
// On by default since every region's polling worker relays the
|
|
13749
13869
|
// response bytes. It used to be off for the shipping-order reason
|
package/dist/engine.cjs
CHANGED
|
@@ -73,6 +73,13 @@ function normalizeWebAccess(value) {
|
|
|
73
73
|
function webAccessFrom(data) {
|
|
74
74
|
return data ? normalizeWebAccess(data.web_access) : DEFAULT_WEB_ACCESS;
|
|
75
75
|
}
|
|
76
|
+
var DEFAULT_SKIP_DUPLICATE_FILES = true;
|
|
77
|
+
function normalizeSkipDuplicateFiles(value) {
|
|
78
|
+
return value !== false;
|
|
79
|
+
}
|
|
80
|
+
function skipDuplicateFilesFrom(data) {
|
|
81
|
+
return data ? normalizeSkipDuplicateFiles(data.skip_duplicate_files) : DEFAULT_SKIP_DUPLICATE_FILES;
|
|
82
|
+
}
|
|
76
83
|
var CHAT_GREETING_MAX_LENGTH = 400;
|
|
77
84
|
function normalizeChatGreeting(value) {
|
|
78
85
|
if (typeof value !== "string") return "";
|
|
@@ -159,6 +166,9 @@ function projectChatGreeting(service) {
|
|
|
159
166
|
function projectWebAccess(service) {
|
|
160
167
|
return webAccessFrom(cachedProjectSettings(service));
|
|
161
168
|
}
|
|
169
|
+
function projectSkipDuplicateFiles(service) {
|
|
170
|
+
return skipDuplicateFilesFrom(cachedProjectSettings(service));
|
|
171
|
+
}
|
|
162
172
|
function setProjectSettings(service, data) {
|
|
163
173
|
if (!service) return;
|
|
164
174
|
cache.set(service, { data: data || null, settled: true, inflight: null });
|
|
@@ -238,16 +248,29 @@ function liveStreamingRealtimeEnabled() {
|
|
|
238
248
|
function liveStreamingEnabled() {
|
|
239
249
|
return _config?.liveStreaming === true;
|
|
240
250
|
}
|
|
251
|
+
function underCeiling(projectSetting, option, override) {
|
|
252
|
+
if (!projectSetting) return false;
|
|
253
|
+
if (option === false) return false;
|
|
254
|
+
if (override === false) return false;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
241
257
|
function webAccessEnabled(service) {
|
|
242
258
|
const opt = _config ? _config.webAccess : void 0;
|
|
243
|
-
|
|
244
|
-
return webAccessFrom(service ? cachedProjectSettings(service) : null);
|
|
259
|
+
return underCeiling(webAccessFrom(service ? cachedProjectSettings(service) : null), opt);
|
|
245
260
|
}
|
|
246
|
-
function resolveWebAccess(service) {
|
|
261
|
+
function resolveWebAccess(service, override) {
|
|
247
262
|
const opt = _config ? _config.webAccess : void 0;
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
if (!service) return Promise.resolve(underCeiling(webAccessFrom(null), opt, override));
|
|
264
|
+
return loadProjectSettings(service).then(webAccessFrom, () => false).then((setting) => underCeiling(setting, opt, override));
|
|
265
|
+
}
|
|
266
|
+
function skipDuplicateFilesEnabled(service) {
|
|
267
|
+
const opt = _config ? _config.skipDuplicateFiles : void 0;
|
|
268
|
+
return underCeiling(skipDuplicateFilesFrom(service ? cachedProjectSettings(service) : null), opt);
|
|
269
|
+
}
|
|
270
|
+
function resolveSkipDuplicateFiles(service) {
|
|
271
|
+
const opt = _config ? _config.skipDuplicateFiles : void 0;
|
|
272
|
+
if (!service) return Promise.resolve(underCeiling(skipDuplicateFilesFrom(null), opt));
|
|
273
|
+
return loadProjectSettings(service).then(skipDuplicateFilesFrom, () => skipDuplicateFilesFrom(null)).then((setting) => underCeiling(setting, opt));
|
|
251
274
|
}
|
|
252
275
|
function streamRecoveryEnabled() {
|
|
253
276
|
if (!_config) return false;
|
|
@@ -263,6 +286,54 @@ function pollOpt() {
|
|
|
263
286
|
return p === void 0 ? {} : { poll: p };
|
|
264
287
|
}
|
|
265
288
|
|
|
289
|
+
// src/engine/duplicates.ts
|
|
290
|
+
var DUPLICATE_LOOKUP_MAX_CANDIDATES = 25;
|
|
291
|
+
var SRC_PREFIX = "src::";
|
|
292
|
+
function isDuplicateCandidateId(uniqueId, name) {
|
|
293
|
+
if (typeof uniqueId !== "string" || !name) return false;
|
|
294
|
+
if (uniqueId.indexOf(SRC_PREFIX) !== 0) return false;
|
|
295
|
+
if (uniqueId === SRC_PREFIX + name) return true;
|
|
296
|
+
const tail = "/" + name;
|
|
297
|
+
return uniqueId.length > SRC_PREFIX.length + tail.length && uniqueId.slice(-tail.length) === tail;
|
|
298
|
+
}
|
|
299
|
+
function listOf(res) {
|
|
300
|
+
if (!res) return [];
|
|
301
|
+
if (Array.isArray(res)) return res;
|
|
302
|
+
return Array.isArray(res.list) ? res.list : [];
|
|
303
|
+
}
|
|
304
|
+
async function findDuplicateUpload(skapi, file, userId, opts) {
|
|
305
|
+
try {
|
|
306
|
+
if (!skapi || typeof skapi.getUniqueId !== "function" || typeof skapi.getRecords !== "function") return null;
|
|
307
|
+
const name = file && typeof file.name === "string" ? file.name : "";
|
|
308
|
+
const size = file && typeof file.size === "number" ? file.size : NaN;
|
|
309
|
+
if (!name || !(size >= 0) || !userId) return null;
|
|
310
|
+
const service = opts && opts.service ? { service: opts.service } : {};
|
|
311
|
+
const found = listOf(await skapi.getUniqueId(Object.assign({ unique_id: name, condition: "<=" }, service)));
|
|
312
|
+
const ids = [];
|
|
313
|
+
for (const item of found) {
|
|
314
|
+
const id = item && typeof item === "object" ? item.unique_id : item;
|
|
315
|
+
if (isDuplicateCandidateId(id, name) && ids.indexOf(id) === -1) ids.push(id);
|
|
316
|
+
if (ids.length >= DUPLICATE_LOOKUP_MAX_CANDIDATES) break;
|
|
317
|
+
}
|
|
318
|
+
for (const uniqueId of ids) {
|
|
319
|
+
let record = null;
|
|
320
|
+
try {
|
|
321
|
+
record = listOf(await skapi.getRecords(Object.assign({ unique_id: uniqueId }, service)))[0] || null;
|
|
322
|
+
} catch {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (!record || typeof record !== "object") continue;
|
|
326
|
+
const data = record.data && typeof record.data === "object" ? record.data : null;
|
|
327
|
+
if (!data || data.size_bytes !== size) continue;
|
|
328
|
+
if (record.user_id !== userId) continue;
|
|
329
|
+
return { storagePath: uniqueId.slice(SRC_PREFIX.length), uniqueId, record };
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
} catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
266
337
|
// src/engine/office.ts
|
|
267
338
|
var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
268
339
|
"doc",
|
|
@@ -675,6 +746,10 @@ function buildRenderMeta(attachment) {
|
|
|
675
746
|
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
676
747
|
`;
|
|
677
748
|
}
|
|
749
|
+
var PAGE_TEXT_TABLE = "page_texts";
|
|
750
|
+
var PAGE_TEXT_RULE = `PAGE TEXT for pages WITHOUT a text layer: for EVERY page whose label (its first label, when the page is split into strips) carries NO "Embedded text layer" block, ALSO post exactly ONE record in table "${PAGE_TEXT_TABLE}" with data {"page": <its document page number from the label, 1-based>, "page_text": "<the page's complete text exactly as printed, verbatim, in reading order, nothing summarised or left out>"}, reference "src::" + the storage path above, in addition to the records above. A page WITH an embedded text layer gets NO such record: its text is already stored.
|
|
751
|
+
|
|
752
|
+
`;
|
|
678
753
|
function buildRenderDatafy(placeholder) {
|
|
679
754
|
return `
|
|
680
755
|
${placeholder}
|
|
@@ -685,7 +760,7 @@ Each image is preceded by a label giving its DOCUMENT PAGE number. That label is
|
|
|
685
760
|
|
|
686
761
|
Transcribe COMPLETELY, not representatively. A table with twenty rows gets twenty records, not a sample of the first few - if a page has more rows than you can save comfortably, still save them all rather than summarising. Where a page carries an embedded text layer it is quoted above that page's image: it is the exact text and should be preferred over reading the pixels, with the image used for layout, tables, stamps and handwriting.
|
|
687
762
|
|
|
688
|
-
Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
|
|
763
|
+
` + PAGE_TEXT_RULE + `Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
|
|
689
764
|
}
|
|
690
765
|
function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
|
|
691
766
|
const src = `src::${attachment.storagePath}`;
|
|
@@ -1227,7 +1302,7 @@ var RETRIEVAL_NOTE_PREFIX = "[Retrieval note, not shown to the user";
|
|
|
1227
1302
|
var FILE_KEYS = ["path", "storage_path", "storagePath", "file", "file_path", "filePath"];
|
|
1228
1303
|
var TABLE_KEYS = ["table_name", "tableName"];
|
|
1229
1304
|
var GROUP_KEYS = ["access_group", "accessGroup"];
|
|
1230
|
-
var
|
|
1305
|
+
var SRC_PREFIX2 = "src::";
|
|
1231
1306
|
var RESULT_WALK_MAX_NODES = 400;
|
|
1232
1307
|
var RESULT_WALK_MAX_DEPTH = 5;
|
|
1233
1308
|
function isWebToolName(name) {
|
|
@@ -1256,8 +1331,8 @@ function pushHit(list, path, page) {
|
|
|
1256
1331
|
}
|
|
1257
1332
|
function pathFromSrcId(value) {
|
|
1258
1333
|
if (typeof value !== "string") return null;
|
|
1259
|
-
if (value.indexOf(
|
|
1260
|
-
var rest = value.slice(
|
|
1334
|
+
if (value.indexOf(SRC_PREFIX2) !== 0) return null;
|
|
1335
|
+
var rest = value.slice(SRC_PREFIX2.length).trim();
|
|
1261
1336
|
return rest ? rest : null;
|
|
1262
1337
|
}
|
|
1263
1338
|
function parseJsonLoose(value) {
|
|
@@ -2926,7 +3001,7 @@ async function callClaudeWithMcp({
|
|
|
2926
3001
|
fileUrls,
|
|
2927
3002
|
webAccess
|
|
2928
3003
|
}) {
|
|
2929
|
-
const web = typeof webAccess === "boolean" ? webAccess :
|
|
3004
|
+
const web = await resolveWebAccess(service, typeof webAccess === "boolean" ? webAccess : void 0);
|
|
2930
3005
|
const mcpServerDefinition = {
|
|
2931
3006
|
type: "url",
|
|
2932
3007
|
name: mcpServer.name,
|
|
@@ -8515,11 +8590,26 @@ var ChatSession = class {
|
|
|
8515
8590
|
if (!total) return Promise.reject(new Error("Empty attachment"));
|
|
8516
8591
|
var urls = [];
|
|
8517
8592
|
var anyIndexFailed = false;
|
|
8593
|
+
var reusedCount = 0;
|
|
8594
|
+
att.reused = false;
|
|
8518
8595
|
var chain = Promise.resolve();
|
|
8596
|
+
var findDuplicate = function(member) {
|
|
8597
|
+
if (typeof self.host.findDuplicateUpload !== "function") return Promise.resolve(null);
|
|
8598
|
+
var storedName = String(member.storagePath || "").split("/").filter(Boolean).pop() || member.file.name;
|
|
8599
|
+
return resolveSkipDuplicateFiles(id.projectId).then(function(on) {
|
|
8600
|
+
if (!on) return null;
|
|
8601
|
+
return Promise.resolve(self.host.findDuplicateUpload({ name: storedName, size: member.file.size })).then(function(hit) {
|
|
8602
|
+
return hit && typeof hit.storagePath === "string" && hit.storagePath ? hit.storagePath : null;
|
|
8603
|
+
});
|
|
8604
|
+
}).catch(function() {
|
|
8605
|
+
return null;
|
|
8606
|
+
});
|
|
8607
|
+
};
|
|
8519
8608
|
members.forEach(function(member, idx) {
|
|
8520
8609
|
chain = chain.then(function() {
|
|
8521
8610
|
var hadExists = false;
|
|
8522
8611
|
var skipped = false;
|
|
8612
|
+
var duplicateOf = null;
|
|
8523
8613
|
var existedBefore = false;
|
|
8524
8614
|
var onProg = function(p) {
|
|
8525
8615
|
if (p && p.total) {
|
|
@@ -8538,7 +8628,14 @@ var ChatSession = class {
|
|
|
8538
8628
|
}
|
|
8539
8629
|
});
|
|
8540
8630
|
};
|
|
8541
|
-
return
|
|
8631
|
+
return findDuplicate(member).then(function(dup) {
|
|
8632
|
+
if (dup) {
|
|
8633
|
+
duplicateOf = dup;
|
|
8634
|
+
console.log("[chat-engine] skipping an upload the user already has:", member.storagePath, "->", dup);
|
|
8635
|
+
return;
|
|
8636
|
+
}
|
|
8637
|
+
return doMemberUpload(true);
|
|
8638
|
+
}).catch(function(err) {
|
|
8542
8639
|
var code = err && (err.code || err.body && err.body.code);
|
|
8543
8640
|
var msg = err && (err.message || err.body && err.body.message || (typeof err === "string" ? err : ""));
|
|
8544
8641
|
var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
|
|
@@ -8558,9 +8655,20 @@ var ChatSession = class {
|
|
|
8558
8655
|
});
|
|
8559
8656
|
}).then(function() {
|
|
8560
8657
|
if (skipped) return;
|
|
8561
|
-
return self.host.getTemporaryUrl(member.storagePath);
|
|
8658
|
+
return self.host.getTemporaryUrl(duplicateOf || member.storagePath);
|
|
8562
8659
|
}).then(function(url) {
|
|
8563
8660
|
if (skipped) return;
|
|
8661
|
+
if (duplicateOf) {
|
|
8662
|
+
urls.push({ name: member.relPath, url, storagePath: duplicateOf });
|
|
8663
|
+
reusedCount += 1;
|
|
8664
|
+
if (att.kind !== "folder") {
|
|
8665
|
+
att.uploadedUrl = url;
|
|
8666
|
+
att.storagePath = duplicateOf;
|
|
8667
|
+
att.reused = true;
|
|
8668
|
+
} else att.reusedCount = reusedCount;
|
|
8669
|
+
self.host.renderAttachmentChips();
|
|
8670
|
+
return;
|
|
8671
|
+
}
|
|
8564
8672
|
urls.push({ name: member.relPath, url, storagePath: member.storagePath });
|
|
8565
8673
|
if (att.kind !== "folder") {
|
|
8566
8674
|
att.uploadedUrl = url;
|
|
@@ -9201,8 +9309,10 @@ exports.DATA_TOOL_NAMES = DATA_TOOL_NAMES;
|
|
|
9201
9309
|
exports.DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_MODEL;
|
|
9202
9310
|
exports.DEFAULT_CONTEXT_WINDOW = DEFAULT_CONTEXT_WINDOW;
|
|
9203
9311
|
exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
|
|
9312
|
+
exports.DEFAULT_SKIP_DUPLICATE_FILES = DEFAULT_SKIP_DUPLICATE_FILES;
|
|
9204
9313
|
exports.DEFAULT_UPLOAD_ACCESS_GROUP = DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
9205
9314
|
exports.DEFAULT_WEB_ACCESS = DEFAULT_WEB_ACCESS;
|
|
9315
|
+
exports.DUPLICATE_LOOKUP_MAX_CANDIDATES = DUPLICATE_LOOKUP_MAX_CANDIDATES;
|
|
9206
9316
|
exports.EMPTY_INDEXING_REPLY = EMPTY_INDEXING_REPLY;
|
|
9207
9317
|
exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
|
|
9208
9318
|
exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
|
|
@@ -9234,6 +9344,7 @@ exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
|
|
|
9234
9344
|
exports.MIN_PER_REQUEST_INPUT_CAP = MIN_PER_REQUEST_INPUT_CAP;
|
|
9235
9345
|
exports.NO_DATA_TOOL_MARK = NO_DATA_TOOL_MARK;
|
|
9236
9346
|
exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
|
|
9347
|
+
exports.PAGE_TEXT_TABLE = PAGE_TEXT_TABLE;
|
|
9237
9348
|
exports.POLL_INTERVAL = POLL_INTERVAL;
|
|
9238
9349
|
exports.PRESIGN_SAFETY_MARGIN_MS = PRESIGN_SAFETY_MARGIN_MS;
|
|
9239
9350
|
exports.PREVIEWABLE_IMAGE_CONTENT_TYPES = PREVIEWABLE_IMAGE_CONTENT_TYPES;
|
|
@@ -9322,6 +9433,7 @@ exports.fetchLiveIndexingKeys = fetchLiveIndexingKeys;
|
|
|
9322
9433
|
exports.fillHistoryViewport = fillHistoryViewport;
|
|
9323
9434
|
exports.filterListByClearHorizon = filterListByClearHorizon;
|
|
9324
9435
|
exports.findAttachmentParser = findAttachmentParser;
|
|
9436
|
+
exports.findDuplicateUpload = findDuplicateUpload;
|
|
9325
9437
|
exports.formatChatTimestamp = formatChatTimestamp;
|
|
9326
9438
|
exports.formatDuration = formatDuration;
|
|
9327
9439
|
exports.formatRetrievalNote = formatRetrievalNote;
|
|
@@ -9346,6 +9458,7 @@ exports.isAuthExpiredError = isAuthExpiredError;
|
|
|
9346
9458
|
exports.isBgIndexingQueue = isBgIndexingQueue;
|
|
9347
9459
|
exports.isCsrStatusEnvelope = isCsrStatusEnvelope;
|
|
9348
9460
|
exports.isDataToolName = isDataToolName;
|
|
9461
|
+
exports.isDuplicateCandidateId = isDuplicateCandidateId;
|
|
9349
9462
|
exports.isErrorResponseBody = isErrorResponseBody;
|
|
9350
9463
|
exports.isHttpUrlLike = isHttpUrlLike;
|
|
9351
9464
|
exports.isImageVisionFile = isImageVisionFile;
|
|
@@ -9379,6 +9492,7 @@ exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
|
|
|
9379
9492
|
exports.normalizeChatGreeting = normalizeChatGreeting;
|
|
9380
9493
|
exports.normalizeExt = normalizeExt;
|
|
9381
9494
|
exports.normalizeProjectAccessSetting = normalizeProjectAccessSetting;
|
|
9495
|
+
exports.normalizeSkipDuplicateFiles = normalizeSkipDuplicateFiles;
|
|
9382
9496
|
exports.normalizeTextContent = normalizeTextContent;
|
|
9383
9497
|
exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
|
|
9384
9498
|
exports.normalizeUploadAccessGroup = normalizeUploadAccessGroup;
|
|
@@ -9401,6 +9515,7 @@ exports.projectAccessSetting = projectAccessSetting;
|
|
|
9401
9515
|
exports.projectAsksUploadAccess = projectAsksUploadAccess;
|
|
9402
9516
|
exports.projectChatGreeting = projectChatGreeting;
|
|
9403
9517
|
exports.projectSettingsSettled = projectSettingsSettled;
|
|
9518
|
+
exports.projectSkipDuplicateFiles = projectSkipDuplicateFiles;
|
|
9404
9519
|
exports.projectUploadAccessGroup = projectUploadAccessGroup;
|
|
9405
9520
|
exports.projectWebAccess = projectWebAccess;
|
|
9406
9521
|
exports.providerBodyWasRead = providerBodyWasRead;
|
|
@@ -9412,6 +9527,7 @@ exports.renderInlineLinkHtml = renderInlineLinkHtml;
|
|
|
9412
9527
|
exports.repairUrlEntities = repairUrlEntities;
|
|
9413
9528
|
exports.repairUrlWhitespace = repairUrlWhitespace;
|
|
9414
9529
|
exports.resolveImagePreviewUrl = resolveImagePreviewUrl;
|
|
9530
|
+
exports.resolveSkipDuplicateFiles = resolveSkipDuplicateFiles;
|
|
9415
9531
|
exports.resolveWebAccess = resolveWebAccess;
|
|
9416
9532
|
exports.retrievalNoteFromText = retrievalNoteFromText;
|
|
9417
9533
|
exports.retrievalNoteOfAnswer = retrievalNoteOfAnswer;
|
|
@@ -9422,6 +9538,8 @@ exports.setProjectContextWindow = setProjectContextWindow;
|
|
|
9422
9538
|
exports.setProjectSettings = setProjectSettings;
|
|
9423
9539
|
exports.shouldRescueInFlightMessage = shouldRescueInFlightMessage;
|
|
9424
9540
|
exports.skapiSupportsStreaming = skapiSupportsStreaming;
|
|
9541
|
+
exports.skipDuplicateFilesEnabled = skipDuplicateFilesEnabled;
|
|
9542
|
+
exports.skipDuplicateFilesFrom = skipDuplicateFilesFrom;
|
|
9425
9543
|
exports.streamRecoveryEnabled = streamRecoveryEnabled;
|
|
9426
9544
|
exports.streamRecoveryLabels = streamRecoveryLabels;
|
|
9427
9545
|
exports.streamRecoveryPhase = streamRecoveryPhase;
|