bunnyquery 1.14.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 +143 -17
- package/dist/engine.cjs +139 -15
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +181 -29
- package/dist/engine.d.ts +181 -29
- package/dist/engine.mjs +130 -16
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +6 -2
- 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 +16 -10
- 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) {
|
|
@@ -1380,6 +1448,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1380
1448
|
"claude-haiku-4-5": 2e5,
|
|
1381
1449
|
"claude-3-5-sonnet": 2e5,
|
|
1382
1450
|
// openai, exact ids
|
|
1451
|
+
// gpt-6.1: OpenAI's listing carries no window; taken as the gpt-6 family's until a
|
|
1452
|
+
// published figure says otherwise (2026-10-12, the default model).
|
|
1453
|
+
"gpt-6.1-sol": 105e4,
|
|
1383
1454
|
"gpt-6-astra": 105e4,
|
|
1384
1455
|
"gpt-6-sol": 105e4,
|
|
1385
1456
|
"gpt-6-luna": 105e4,
|
|
@@ -1399,6 +1470,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1399
1470
|
"claude-opus": 1e6,
|
|
1400
1471
|
"claude-sonnet": 1e6,
|
|
1401
1472
|
"claude-haiku": 2e5,
|
|
1473
|
+
"gpt-6.1": 105e4,
|
|
1402
1474
|
"gpt-6": 105e4,
|
|
1403
1475
|
"gpt-5.6": 105e4,
|
|
1404
1476
|
"gpt-5": 128e3
|
|
@@ -1416,6 +1488,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1416
1488
|
"claude-haiku-4-5": 64e3,
|
|
1417
1489
|
"claude-3-5-sonnet": 8e3,
|
|
1418
1490
|
// openai
|
|
1491
|
+
"gpt-6.1-sol": 128e3,
|
|
1419
1492
|
"gpt-6-astra": 128e3,
|
|
1420
1493
|
"gpt-6-sol": 128e3,
|
|
1421
1494
|
"gpt-6-luna": 128e3,
|
|
@@ -1435,6 +1508,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1435
1508
|
"claude-opus": 128e3,
|
|
1436
1509
|
"claude-sonnet": 64e3,
|
|
1437
1510
|
"claude-haiku": 64e3,
|
|
1511
|
+
"gpt-6.1": 128e3,
|
|
1438
1512
|
"gpt-6": 128e3,
|
|
1439
1513
|
"gpt-5.6": 128e3,
|
|
1440
1514
|
"gpt-5": 128e3
|
|
@@ -2602,8 +2676,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2602
2676
|
return [openaiWebSearchTool()] ;
|
|
2603
2677
|
};
|
|
2604
2678
|
var MCP_NAME = "BunnyQuery";
|
|
2605
|
-
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
|
|
2606
|
-
var DEFAULT_OPENAI_MODEL = "gpt-
|
|
2679
|
+
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5-5";
|
|
2680
|
+
var DEFAULT_OPENAI_MODEL = "gpt-6.1-sol";
|
|
2607
2681
|
var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
2608
2682
|
function withMcpParams(base, params) {
|
|
2609
2683
|
if (!base) return base;
|
|
@@ -2805,7 +2879,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2805
2879
|
fileUrls,
|
|
2806
2880
|
webAccess
|
|
2807
2881
|
}) {
|
|
2808
|
-
const web = typeof webAccess === "boolean" ? webAccess :
|
|
2882
|
+
const web = await resolveWebAccess(service, typeof webAccess === "boolean" ? webAccess : void 0);
|
|
2809
2883
|
const mcpServerDefinition = {
|
|
2810
2884
|
type: "url",
|
|
2811
2885
|
name: mcpServer.name,
|
|
@@ -8335,11 +8409,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8335
8409
|
if (!total) return Promise.reject(new Error("Empty attachment"));
|
|
8336
8410
|
var urls = [];
|
|
8337
8411
|
var anyIndexFailed = false;
|
|
8412
|
+
var reusedCount = 0;
|
|
8413
|
+
att.reused = false;
|
|
8338
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
|
+
};
|
|
8339
8427
|
members.forEach(function(member, idx) {
|
|
8340
8428
|
chain = chain.then(function() {
|
|
8341
8429
|
var hadExists = false;
|
|
8342
8430
|
var skipped = false;
|
|
8431
|
+
var duplicateOf = null;
|
|
8343
8432
|
var existedBefore = false;
|
|
8344
8433
|
var onProg = function(p) {
|
|
8345
8434
|
if (p && p.total) {
|
|
@@ -8358,7 +8447,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8358
8447
|
}
|
|
8359
8448
|
});
|
|
8360
8449
|
};
|
|
8361
|
-
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) {
|
|
8362
8458
|
var code = err && (err.code || err.body && err.body.code);
|
|
8363
8459
|
var msg = err && (err.message || err.body && err.body.message || (typeof err === "string" ? err : ""));
|
|
8364
8460
|
var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
|
|
@@ -8378,9 +8474,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8378
8474
|
});
|
|
8379
8475
|
}).then(function() {
|
|
8380
8476
|
if (skipped) return;
|
|
8381
|
-
return self.host.getTemporaryUrl(member.storagePath);
|
|
8477
|
+
return self.host.getTemporaryUrl(duplicateOf || member.storagePath);
|
|
8382
8478
|
}).then(function(url) {
|
|
8383
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
|
+
}
|
|
8384
8491
|
urls.push({ name: member.relPath, url, storagePath: member.storagePath });
|
|
8385
8492
|
if (att.kind !== "folder") {
|
|
8386
8493
|
att.uploadedUrl = url;
|
|
@@ -9012,7 +9119,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9012
9119
|
(function() {
|
|
9013
9120
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
9014
9121
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
9015
|
-
var BQ_VERSION = "1.
|
|
9122
|
+
var BQ_VERSION = "1.15.1" ;
|
|
9016
9123
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
9017
9124
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
9018
9125
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -10852,6 +10959,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10852
10959
|
uploadAccessGroup: function(path) {
|
|
10853
10960
|
return resolveUploadAccessGroup(path);
|
|
10854
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
|
+
},
|
|
10855
10969
|
storagePathFor: function(relPath) {
|
|
10856
10970
|
return attachmentStoragePath(relPath);
|
|
10857
10971
|
},
|
|
@@ -11833,7 +11947,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11833
11947
|
});
|
|
11834
11948
|
chip.appendChild(h("span", { class: "bq-attachment-icon", html: isFolder ? FOLDER_ICON_SVG : FILE_ICON_SVG }));
|
|
11835
11949
|
chip.appendChild(h("span", { class: "bq-attachment-name", text: att.name, title: att.name }));
|
|
11836
|
-
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);
|
|
11837
11951
|
chip.appendChild(h("span", { class: "bq-attachment-meta", text: meta }));
|
|
11838
11952
|
if (clickable) chip.appendChild(h("span", { class: "bq-attachment-arrow", text: "\u2197" }));
|
|
11839
11953
|
if (att.status !== "uploading" && att.status !== "done") {
|
|
@@ -13657,6 +13771,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13657
13771
|
// true/false pins it for this page, both ways. Read at
|
|
13658
13772
|
// configureChatEngine time and when each chat request is built.
|
|
13659
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,
|
|
13660
13783
|
// Server-driven windowed indexing; read at configureChatEngine time.
|
|
13661
13784
|
// Listed here so the defaults object is the full opt surface.
|
|
13662
13785
|
windowedIndexing: true,
|
|
@@ -13738,6 +13861,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13738
13861
|
// chat request (off unless the owner turned it on). Passing the null
|
|
13739
13862
|
// default through would read as a decision.
|
|
13740
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,
|
|
13741
13867
|
// ---- live streaming (mirrored in agent.vue's ai_agent.ts) --------
|
|
13742
13868
|
// On by default since every region's polling worker relays the
|
|
13743
13869
|
// response bytes. It used to be off for the shipping-order reason
|