bunnyquery 1.9.0 → 1.9.4
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/bunnyquery.css +33 -0
- package/bunnyquery.js +353 -44
- package/dist/engine.cjs +127 -30
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +96 -3
- package/dist/engine.d.ts +96 -3
- package/dist/engine.mjs +127 -31
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/host.ts +29 -0
- package/src/engine/indexing_groups.ts +88 -12
- package/src/engine/prompts/chat_system_prompt.ts +20 -1
- package/src/engine/prompts/index.ts +1 -0
- package/src/engine/prompts/indexing_system_prompt.ts +13 -1
- package/src/engine/prompts/indexing_user_message.ts +29 -2
- package/src/engine/requests.ts +57 -6
- package/src/engine/session.ts +36 -3
- package/src/widget.css +33 -0
package/dist/engine.cjs
CHANGED
|
@@ -171,7 +171,22 @@ var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
171
171
|
// documents
|
|
172
172
|
"pdf",
|
|
173
173
|
"docx",
|
|
174
|
+
"docm",
|
|
174
175
|
"pptx",
|
|
176
|
+
"pptm",
|
|
177
|
+
"doc",
|
|
178
|
+
"ppt",
|
|
179
|
+
// Korean word processor (OLE/CFB and OOXML-style variants)
|
|
180
|
+
"hwp",
|
|
181
|
+
"hwpx",
|
|
182
|
+
// opendocument text/presentation (ods is a grid, listed above)
|
|
183
|
+
"odt",
|
|
184
|
+
"odp",
|
|
185
|
+
// other long-form documents
|
|
186
|
+
"epub",
|
|
187
|
+
"rtf",
|
|
188
|
+
"html",
|
|
189
|
+
"htm",
|
|
175
190
|
// plain text / data / markup
|
|
176
191
|
"txt",
|
|
177
192
|
"md",
|
|
@@ -284,11 +299,13 @@ function groupAttachmentFailures(attachments) {
|
|
|
284
299
|
// src/engine/prompts/chat_system_prompt.ts
|
|
285
300
|
function buildChatSystemPrompt(params) {
|
|
286
301
|
const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
|
|
302
|
+
const g = params.indexAccessGroup;
|
|
303
|
+
const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
|
|
287
304
|
let systemPrompt = `
|
|
288
305
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
289
306
|
Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
|
|
290
307
|
Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
|
|
291
|
-
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized"
|
|
308
|
+
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized", but THIS project indexes at access_group ${indexGroupLiteral}, so pass access_group ${indexGroupLiteral} EXPLICITLY on every index or tag query here - the auto-fill would search a group this project's data is not in and come back empty. Files uploaded before the project's setting changed may sit at another group, so when a scoped query comes back empty, retry it across the other groups (0, 1, "private") before concluding there is nothing, while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
|
|
292
309
|
Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
|
|
293
310
|
Embedded values: a search term is often stored inside a larger string. A merchant "BAKSA" appears as "DNH*BAKSA#4070277042", and a card as "5860****5173". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
|
|
294
311
|
File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
|
|
@@ -338,6 +355,7 @@ Project description: """${serviceDescription}"""`;
|
|
|
338
355
|
// src/engine/prompts/indexing_system_prompt.ts
|
|
339
356
|
function buildIndexingSystemPrompt(params) {
|
|
340
357
|
const { projectId, serviceName, serviceDescription } = params;
|
|
358
|
+
const accessGroup = params.accessGroup === "public" || params.accessGroup === "private" ? params.accessGroup : "authorized";
|
|
341
359
|
let systemPrompt = `You are a background indexing agent for project ${projectId}.
|
|
342
360
|
- Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
|
|
343
361
|
- Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
|
|
@@ -346,7 +364,8 @@ function buildIndexingSystemPrompt(params) {
|
|
|
346
364
|
- VISION: when the message (a readFileContent window, an embedded PDF page, or an inline attachment) includes IMAGES - scanned/rendered PDF pages, or photos embedded in a spreadsheet next to a row/block - LOOK at them and capture what they show as record data (the reading/values in a scanned table, the part/defect/condition visible in a photo). The image IS part of the data; correlate each photo with its labelled block ("PHOTO A3" markers tie a photo to that grid row).
|
|
347
365
|
- TRANSCRIBE, DO NOT DESCRIBE. When an image contains ANY text - a label, tag, stamp, form field, serial/part number, handwriting - your FIRST job is to read the characters out and store them VERBATIM, not to describe the scene. A record saying "a red inspection tag with handwritten markings" is worthless: it is unsearchable and every such photo produces the same sentence. Put the characters you can actually read into these EXACT fields, not variations of them: "printed_text" (the pre-printed wording), "handwritten_text" (what a person wrote by hand), and, when you can resolve one, "part_no", "tag_id" and "date". Same reason as the fixed table names: a field called photo_text in one pass and visible_text_notes in the next cannot be queried together. Read PARTIAL values rather than skipping: "500.7402.52__" beats nothing. Only when a character is genuinely unreadable, leave that field null or mark the unreadable span - do NOT invent it, and do NOT replace the whole transcription with a description of what the object looks like. A scene description is a nice extra AFTER the text, never instead of it.
|
|
348
366
|
- IMAGE FILES uploaded as the file itself: if ANY readable character appears ANYWHERE in the image (a label, a stamp, a sign in the background) it counts as an image WITH text - transcribe it per the rule above, and also capture the layout (what appears where) and every entity named. Only a truly text-free image gets description first: a one-line caption, then the objects present with their attributes (type, color, count, condition, position). Either way, save what you extract onto the file's "src::" record with updateRecords, TAG every entity and identifier visible, and INDEX the one number the image offers (a measured value, an amount, a count).
|
|
349
|
-
- Whatever the file type, this file's identity is "src::" + its storage path (the "storage path" metadata line) - never the inline content or a temporary URL. That record ALREADY EXISTS: the upload pipeline creates it in table "file_summaries" (access group "
|
|
367
|
+
- Whatever the file type, this file's identity is "src::" + its storage path (the "storage path" metadata line) - never the inline content or a temporary URL. That record ALREADY EXISTS: the upload pipeline creates it in table "file_summaries" (access group "${accessGroup}") before indexing starts, so posting it again is rejected as a duplicate unique_id. Reference it from every record you write, and add what you learn to it with updateRecords. If that update unexpectedly reports the record does not exist, post it yourself ONCE with that exact "src::" unique_id (table "file_summaries", access group "${accessGroup}") and carry on; this is the ONE exception to the do-NOT-post-the-file-record rules elsewhere in these instructions, because the source identity must never be dropped just because an update failed.
|
|
368
|
+
- ACCESS GROUP (hard rule): every record you write for this file - the file record, per-row records, chapters, summaries, intermediates - MUST be posted with access group "${accessGroup}". Pass it explicitly on every postRecords call; do not leave it out and do not vary it between passes of the same file. An access group is part of a record's table key, so records saved under a different group than the file are in a different table and will not come back with the rest of it: a "public" file whose rows were saved as "authorized" is one an anonymous visitor can see the name of and none of the contents of, and a re-index cannot find the strays to clean them up. The one exception is the EXTRACTED MEDIA records in "__MEDIA__", which the pipeline creates for you - leave their group alone and only enrich them.
|
|
350
369
|
- REACHABILITY (hard rule): every record you write while indexing this file MUST be reachable from the file's "src::<storage path>" record by following reference - either reference that record directly, or reference something that already reaches it. A record with no reference, or one pointing outside this file's chain, is an ORPHAN: deleting or re-indexing the file removes the reachable records and leaves the orphan behind forever, where it keeps turning up in later answers as stale data. If you create an intermediate record that OTHER records reference (a page record that rows hang off, a sheet or section record), set source.can_remove_referencing_records to true on it; the delete cascade passes a delete through a record only when that record carries the flag OR a unique_id starting "src::" (the file record cascades because its unique_id starts with "src::"; the intermediates you create carry no "src::" id, so they need the flag), and it cascades ONE LEVEL AT A TIME, so EVERY intermediate record in a chain needs its own marker - an unmarked link stops the cascade there and everything below it survives as orphans. When in doubt, reference the file record directly and keep the chain flat.
|
|
351
370
|
- TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a table named EXACTLY "spreadsheet_rows". Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, window by window, until it ends. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. The file-level "src::" record ALREADY EXISTS - the upload pipeline creates it before indexing starts - so do NOT create it. Link EVERY per-row record to it via reference (set each row record's reference to exactly "src::" + the storage path, with NO sheet/window/summary suffix added; the row records themselves do NOT carry a src:: unique_id). Enrich that same record with sheet name(s), column headers and total row count via updateRecords rather than posting another one. The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed. INDEX each row record on the row's most useful NUMERIC column (named by its header) so rows sort and range-query; when the row has no numeric column, index the grid row number instead. TAG each row record with the sheet name, the file name, and the row's categorical values (a status, a category, a type) - tags are how rows are filtered without scanning the table.
|
|
352
371
|
- ONE RECORD PER GRID ROW, ALWAYS. "Row" means the numbered row of the sheet (R37 is one record), never a visual block, item, section or left/right pair. Sheets that repeat the same columns side by side (an A/B block beside a C/D block, "paired" or "mirrored" layouts) still get ONE record per grid row, holding BOTH sides - suffix the keys to keep them apart (PART_NO_A / PART_NO_B). Collapsing a 16-row window into 2 or 3 "block" records is the single most damaging mistake here: it silently loses most of the cells and makes every later total wrong, because some windows were counted per row and others per block. If a window shows rows R37 to R52, you save records for R37..R52 and the count you report is the number of grid rows you actually wrote.
|
|
@@ -367,6 +386,10 @@ Project description: """${serviceDescription}"""`;
|
|
|
367
386
|
}
|
|
368
387
|
|
|
369
388
|
// src/engine/prompts/indexing_user_message.ts
|
|
389
|
+
function indexingAccessGroup(attachment) {
|
|
390
|
+
const g = attachment && attachment.accessGroup;
|
|
391
|
+
return g === "public" || g === "private" ? g : "authorized";
|
|
392
|
+
}
|
|
370
393
|
function buildIndexingUserMessage(attachment, options) {
|
|
371
394
|
const head = `A new file has just been uploaded. Index it now.
|
|
372
395
|
|
|
@@ -375,7 +398,11 @@ File metadata:
|
|
|
375
398
|
- storage path: ${attachment.storagePath}
|
|
376
399
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
377
400
|
` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
|
|
378
|
-
` : "")
|
|
401
|
+
` : "") + // Stated in the metadata block as well as the system prompt because this is
|
|
402
|
+
// the per-FILE value: one project can hold public and private files at once,
|
|
403
|
+
// and the system prompt is what is constant across the run.
|
|
404
|
+
`- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
405
|
+
`;
|
|
379
406
|
if (options?.inlineContent) {
|
|
380
407
|
return head + `
|
|
381
408
|
The file's content was parsed by the client and is provided inline below. Read it directly - do NOT fetch any URL for this file. Set every record's reference to exactly "src::" + the storage path above (not this content). That file record already exists, so enrich it with updateRecords rather than posting it.
|
|
@@ -423,7 +450,8 @@ function buildRenderMeta(attachment) {
|
|
|
423
450
|
- name: ${attachment.name}
|
|
424
451
|
- storage path: ${attachment.storagePath}
|
|
425
452
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
426
|
-
` : "")
|
|
453
|
+
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
454
|
+
`;
|
|
427
455
|
}
|
|
428
456
|
function buildRenderDatafy(placeholder) {
|
|
429
457
|
return `
|
|
@@ -466,7 +494,8 @@ File metadata:
|
|
|
466
494
|
- name: ${attachment.name}
|
|
467
495
|
- storage path: ${attachment.storagePath}
|
|
468
496
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
469
|
-
` : "") +
|
|
497
|
+
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
498
|
+
|
|
470
499
|
Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest row/window already saved). The reference ALONE is the whole query: it returns every record written from this file across ALL tables and ALL access groups, so do NOT add table_name or access_group to narrow it. The response is PAGED, so keep fetching pages until it reports there are no more, and take the furthest point from the WHOLE set, never from the first page. Then call readFileContent with the storage path above and a CURSOR that RESUMES just after that point - do NOT start at the beginning. The cursor is derivable from what you already saved:
|
|
471
500
|
- Spreadsheet: the cursor is "<sheetIndex>:<nextRow>" (0-based sheet index, 1-based row). If you saved up to row R of sheet S, use cursor="S:R+1".
|
|
472
501
|
- Text: the cursor is the character offset already read.
|
|
@@ -1527,6 +1556,11 @@ var MCP_NAME = "BunnyQuery";
|
|
|
1527
1556
|
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
|
|
1528
1557
|
var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
|
|
1529
1558
|
var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
1559
|
+
function mcpEndpointFor(anonymous, publicProjectId, service) {
|
|
1560
|
+
if (!anonymous) return { url: mcpUrl(), token: "$ACCESS_TOKEN" };
|
|
1561
|
+
const project = publicProjectId || service;
|
|
1562
|
+
return { url: String(mcpUrl()).replace(/\/+$/, "") + "/p/" + project };
|
|
1563
|
+
}
|
|
1530
1564
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
1531
1565
|
var VARIANT_IMAGE_DETAIL = "original";
|
|
1532
1566
|
var VARIANT_TEXT_VERBOSITY = "high";
|
|
@@ -1752,7 +1786,8 @@ async function callClaudeWithMcp({
|
|
|
1752
1786
|
}
|
|
1753
1787
|
});
|
|
1754
1788
|
}
|
|
1755
|
-
async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
|
|
1789
|
+
async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError, mcpScope) {
|
|
1790
|
+
const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
|
|
1756
1791
|
return callClaudeWithMcp({
|
|
1757
1792
|
prompt,
|
|
1758
1793
|
messages,
|
|
@@ -1766,11 +1801,14 @@ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1766
1801
|
fileUrls,
|
|
1767
1802
|
mcpServer: {
|
|
1768
1803
|
name: MCP_NAME,
|
|
1769
|
-
url:
|
|
1770
|
-
authorizationToken
|
|
1804
|
+
url: endpoint.url,
|
|
1805
|
+
// Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
|
|
1806
|
+
// guard below drops the key rather than sending an empty one.
|
|
1807
|
+
authorizationToken: endpoint.token
|
|
1771
1808
|
}});
|
|
1772
1809
|
}
|
|
1773
|
-
async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
|
|
1810
|
+
async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError, mcpScope) {
|
|
1811
|
+
const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
|
|
1774
1812
|
const resolvedModel = model || DEFAULT_OPENAI_MODEL;
|
|
1775
1813
|
const imageDetail = getOpenAIImageDetail(resolvedModel);
|
|
1776
1814
|
const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
|
|
@@ -1813,11 +1851,12 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1813
1851
|
{
|
|
1814
1852
|
type: "mcp",
|
|
1815
1853
|
server_label: MCP_NAME,
|
|
1816
|
-
server_url:
|
|
1854
|
+
server_url: endpoint.url,
|
|
1817
1855
|
require_approval: "never",
|
|
1818
|
-
headers:
|
|
1819
|
-
|
|
1820
|
-
|
|
1856
|
+
// No `headers` at all for an anonymous turn: `Bearer ` with an
|
|
1857
|
+
// empty token is a credential the MCP server rejects, and the
|
|
1858
|
+
// project-scoped endpoint needs none.
|
|
1859
|
+
...endpoint.token ? { headers: { Authorization: "Bearer " + endpoint.token } } : {}
|
|
1821
1860
|
},
|
|
1822
1861
|
...[
|
|
1823
1862
|
{
|
|
@@ -1928,7 +1967,10 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
1928
1967
|
// by the tools' schema pattern.
|
|
1929
1968
|
projectId: info.publicProjectId || service,
|
|
1930
1969
|
serviceName: info.serviceName,
|
|
1931
|
-
serviceDescription: info.serviceDescription
|
|
1970
|
+
serviceDescription: info.serviceDescription,
|
|
1971
|
+
// Per-FILE, not per-project: one project holds public and private files at
|
|
1972
|
+
// once, so this travels on the attachment rather than the identity.
|
|
1973
|
+
accessGroup: attachment.accessGroup
|
|
1932
1974
|
});
|
|
1933
1975
|
if (platform === "openai") {
|
|
1934
1976
|
const resolvedModel2 = info.model || DEFAULT_OPENAI_MODEL;
|
|
@@ -3510,10 +3552,23 @@ var ChatSession = class {
|
|
|
3510
3552
|
this._lidSeq += 1;
|
|
3511
3553
|
return "lid_" + this._lidSeq;
|
|
3512
3554
|
}
|
|
3555
|
+
/**
|
|
3556
|
+
* The key every per-chat cache hangs off: the restored message cache, the
|
|
3557
|
+
* hydrated-body memo, the live-index key and the per-file storage-path key.
|
|
3558
|
+
*
|
|
3559
|
+
* It carries the IDENTITY as well as the project and platform. A single
|
|
3560
|
+
* browser can hold more than one conversation on one project without a
|
|
3561
|
+
* reload — an anonymous visitor who signs in, or a dashboard user who logs
|
|
3562
|
+
* out and back in as someone else — and with an identity-free key the
|
|
3563
|
+
* previous conversation stayed in the cache and was re-rendered, and written
|
|
3564
|
+
* back, as the new one's. `userId` is the same value the request queue is
|
|
3565
|
+
* named after, so two identities that share a queue share a cache, which is
|
|
3566
|
+
* exactly right.
|
|
3567
|
+
*/
|
|
3513
3568
|
getHistoryCacheKey() {
|
|
3514
3569
|
var id = this.host.getIdentity();
|
|
3515
3570
|
if (!id.projectId || id.platform === "none") return "";
|
|
3516
|
-
return id.projectId + "#" + id.platform;
|
|
3571
|
+
return id.projectId + "#" + id.platform + "#" + (id.userId || "");
|
|
3517
3572
|
}
|
|
3518
3573
|
/** Re-apply memoized hydrated texts onto freshly-mapped messages. Both
|
|
3519
3574
|
* clients call this right after their mapper runs (loadHistory does it
|
|
@@ -3716,7 +3771,9 @@ var ChatSession = class {
|
|
|
3716
3771
|
if (projectId === void 0) projectId = id.projectId;
|
|
3717
3772
|
if (owner === void 0) owner = id.owner;
|
|
3718
3773
|
}
|
|
3719
|
-
|
|
3774
|
+
var liveId = this.host.getIdentity();
|
|
3775
|
+
var mcpScope = { anonymous: liveId.anonymous, publicProjectId: liveId.publicProjectId };
|
|
3776
|
+
return platform === "openai" ? callOpenAIWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls, void 0, void 0, mcpScope) : callClaudeWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls, void 0, void 0, mcpScope);
|
|
3720
3777
|
}
|
|
3721
3778
|
dispatchAgentRequest(params) {
|
|
3722
3779
|
var self = this;
|
|
@@ -6104,6 +6161,15 @@ var ChatSession = class {
|
|
|
6104
6161
|
})).catch(function() {
|
|
6105
6162
|
});
|
|
6106
6163
|
});
|
|
6164
|
+
var accessGroup;
|
|
6165
|
+
preIndex = preIndex.then(function() {
|
|
6166
|
+
if (alreadyIndexing) return;
|
|
6167
|
+
if (typeof self.host.uploadAccessGroup !== "function") return;
|
|
6168
|
+
return Promise.resolve(self.host.uploadAccessGroup(member.storagePath)).then(function(g) {
|
|
6169
|
+
accessGroup = g || void 0;
|
|
6170
|
+
}).catch(function() {
|
|
6171
|
+
});
|
|
6172
|
+
});
|
|
6107
6173
|
return preIndex.then(function() {
|
|
6108
6174
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
6109
6175
|
}).then(function(parsedContent) {
|
|
@@ -6122,7 +6188,8 @@ var ChatSession = class {
|
|
|
6122
6188
|
storagePath: member.storagePath,
|
|
6123
6189
|
mime: mime || void 0,
|
|
6124
6190
|
size: member.file.size,
|
|
6125
|
-
url
|
|
6191
|
+
url,
|
|
6192
|
+
accessGroup
|
|
6126
6193
|
},
|
|
6127
6194
|
parsedContent: parsedContent || void 0
|
|
6128
6195
|
}).then(function(ack) {
|
|
@@ -6249,6 +6316,9 @@ var ChatSession = class {
|
|
|
6249
6316
|
};
|
|
6250
6317
|
|
|
6251
6318
|
// src/engine/indexing_groups.ts
|
|
6319
|
+
function canonIndexKey(s) {
|
|
6320
|
+
return typeof s === "string" && s ? s.trim() : "";
|
|
6321
|
+
}
|
|
6252
6322
|
var RUN_RECORD_WORKING_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
6253
6323
|
var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
|
|
6254
6324
|
var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
|
|
@@ -6318,15 +6388,24 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6318
6388
|
var openRunOfKey = {};
|
|
6319
6389
|
var runsOfKey = {};
|
|
6320
6390
|
var keyOfRun = {};
|
|
6391
|
+
var newestTsOfRun = {};
|
|
6321
6392
|
var runSeq = 0;
|
|
6322
6393
|
for (var i = 0; i < list.length; i++) {
|
|
6323
6394
|
var msg = list[i];
|
|
6324
6395
|
if (!msg || !msg.isBackgroundTask) continue;
|
|
6325
6396
|
var runId;
|
|
6326
6397
|
var ref = msg.role === "user" ? readFileRef(msg) : null;
|
|
6327
|
-
if (
|
|
6398
|
+
if (msg._serverItemId && runByItemId[msg._serverItemId]) {
|
|
6399
|
+
runId = runByItemId[msg._serverItemId];
|
|
6400
|
+
} else if (ref) {
|
|
6328
6401
|
var key = ref.path || keyByName[ref.name] || ref.name;
|
|
6329
|
-
|
|
6402
|
+
var alreadySeen = !!(msg._serverItemId && runByItemId[msg._serverItemId]);
|
|
6403
|
+
var openId = openRunOfKey[key];
|
|
6404
|
+
var notLater = false;
|
|
6405
|
+
if (openId && typeof msg._ts === "number" && typeof newestTsOfRun[openId] === "number") {
|
|
6406
|
+
notLater = msg._ts <= newestTsOfRun[openId];
|
|
6407
|
+
}
|
|
6408
|
+
if (!ref.continued && !alreadySeen && !notLater && openRunOfKey[key]) delete openRunOfKey[key];
|
|
6330
6409
|
runId = openRunOfKey[key];
|
|
6331
6410
|
if (!runId) {
|
|
6332
6411
|
runId = "run" + runSeq++;
|
|
@@ -6334,12 +6413,14 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6334
6413
|
keyOfRun[runId] = key;
|
|
6335
6414
|
(runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
|
|
6336
6415
|
}
|
|
6337
|
-
} else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
|
|
6338
|
-
runId = runByItemId[msg._serverItemId];
|
|
6339
6416
|
} else if (msg.role !== "user") {
|
|
6340
6417
|
runId = runOfIndex[i - 1];
|
|
6341
6418
|
}
|
|
6342
6419
|
if (!runId) continue;
|
|
6420
|
+
if (typeof msg._ts === "number") {
|
|
6421
|
+
var prevTs = newestTsOfRun[runId];
|
|
6422
|
+
if (typeof prevTs !== "number" || msg._ts > prevTs) newestTsOfRun[runId] = msg._ts;
|
|
6423
|
+
}
|
|
6343
6424
|
var g = groups[runId];
|
|
6344
6425
|
if (!g) {
|
|
6345
6426
|
var fileKey = keyOfRun[runId];
|
|
@@ -6504,20 +6585,33 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6504
6585
|
for (var ci = 0; ci < order.length; ci++) {
|
|
6505
6586
|
var cg = groups[order[ci]];
|
|
6506
6587
|
if (cg.path) {
|
|
6507
|
-
coveredPaths[cg.path] = true;
|
|
6508
|
-
if (cg.key) coveredPaths[cg.key] = true;
|
|
6509
|
-
} else if (cg.name) coveredPathlessNames[cg.name] = true;
|
|
6510
|
-
else if (cg.key) coveredPaths[cg.key] = true;
|
|
6588
|
+
coveredPaths[canonIndexKey(cg.path)] = true;
|
|
6589
|
+
if (cg.key) coveredPaths[canonIndexKey(cg.key)] = true;
|
|
6590
|
+
} else if (cg.name) coveredPathlessNames[canonIndexKey(cg.name)] = true;
|
|
6591
|
+
else if (cg.key) coveredPaths[canonIndexKey(cg.key)] = true;
|
|
6511
6592
|
}
|
|
6512
6593
|
var now = opts && typeof opts.now === "number" ? opts.now : Date.now();
|
|
6513
6594
|
var stubClearedAt = opts && typeof opts.stubClearedAt === "number" && opts.stubClearedAt > 0 ? opts.stubClearedAt : 0;
|
|
6514
6595
|
for (var sp in runStubs) {
|
|
6515
6596
|
var rec = runStubs[sp];
|
|
6516
|
-
if (!sp || !rec || !rec.status || coveredPaths[sp]) continue;
|
|
6597
|
+
if (!sp || !rec || !rec.status || coveredPaths[canonIndexKey(sp)]) continue;
|
|
6517
6598
|
var fname = rec.filename || sp.split("/").pop() || sp;
|
|
6518
|
-
if (coveredPathlessNames[fname]) continue;
|
|
6599
|
+
if (coveredPathlessNames[canonIndexKey(fname)]) continue;
|
|
6519
6600
|
if (stubPlatform && rec.platform && rec.platform !== stubPlatform) continue;
|
|
6520
|
-
var live = !!liveIndexKeys[sp] || !!liveIndexKeys[
|
|
6601
|
+
var live = !!liveIndexKeys[sp] || !!liveIndexKeys[canonIndexKey(sp)];
|
|
6602
|
+
if (!live && (liveIndexKeys[fname] || liveIndexKeys[canonIndexKey(fname)])) {
|
|
6603
|
+
var claimedByOther = false;
|
|
6604
|
+
for (var lk in liveIndexKeys) {
|
|
6605
|
+
if (!liveIndexKeys[lk]) continue;
|
|
6606
|
+
var lkc = canonIndexKey(lk);
|
|
6607
|
+
if (lkc === canonIndexKey(sp)) continue;
|
|
6608
|
+
if (lkc.length > fname.length && lkc.slice(-(fname.length + 1)) === "/" + fname) {
|
|
6609
|
+
claimedByOther = true;
|
|
6610
|
+
break;
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
live = !claimedByOther;
|
|
6614
|
+
}
|
|
6521
6615
|
var recWhen = typeof rec.finished === "number" ? rec.finished : typeof rec.started === "number" ? rec.started : void 0;
|
|
6522
6616
|
if (stubClearedAt && !live && recWhen !== void 0 && recWhen <= stubClearedAt) continue;
|
|
6523
6617
|
var st = "active";
|
|
@@ -6525,7 +6619,7 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6525
6619
|
var res = false;
|
|
6526
6620
|
var reason;
|
|
6527
6621
|
if (!live) {
|
|
6528
|
-
if (rec.status === "done" || doneKeys[sp] || doneKeys[fname]) {
|
|
6622
|
+
if (rec.status === "done" || doneKeys[sp] || doneKeys[canonIndexKey(sp)] || doneKeys[fname] || doneKeys[canonIndexKey(fname)]) {
|
|
6529
6623
|
st = "done";
|
|
6530
6624
|
fin = true;
|
|
6531
6625
|
} else if (rec.status === "error") {
|
|
@@ -6579,11 +6673,13 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6579
6673
|
}
|
|
6580
6674
|
}
|
|
6581
6675
|
var suppressAnchor = {};
|
|
6676
|
+
var stubByCanon = {};
|
|
6677
|
+
if (runStubs) for (var ck in runStubs) stubByCanon[canonIndexKey(ck)] = runStubs[ck];
|
|
6582
6678
|
if (runStubs) {
|
|
6583
6679
|
for (var ti2 = 0; ti2 < order.length; ti2++) {
|
|
6584
6680
|
var tg = groups[order[ti2]];
|
|
6585
6681
|
if (!newestRunOfKey[order[ti2]]) continue;
|
|
6586
|
-
var trec = tg.path && runStubs[tg.path] || runStubs[tg.key];
|
|
6682
|
+
var trec = tg.path && (runStubs[tg.path] || stubByCanon[canonIndexKey(tg.path)]) || runStubs[tg.key] || stubByCanon[canonIndexKey(tg.key)];
|
|
6587
6683
|
if (!trec || typeof trec.started !== "number") continue;
|
|
6588
6684
|
if (stubPlatform && trec.platform && trec.platform !== stubPlatform) continue;
|
|
6589
6685
|
suppressAnchor[order[ti2]] = true;
|
|
@@ -6734,6 +6830,7 @@ exports.groupAttachmentFailures = groupAttachmentFailures;
|
|
|
6734
6830
|
exports.hasBom = hasBom;
|
|
6735
6831
|
exports.hydrateImagePreviews = hydrateImagePreviews;
|
|
6736
6832
|
exports.indexDoneUniqueId = indexDoneUniqueId;
|
|
6833
|
+
exports.indexingAccessGroup = indexingAccessGroup;
|
|
6737
6834
|
exports.isAuthExpiredError = isAuthExpiredError;
|
|
6738
6835
|
exports.isBgIndexingQueue = isBgIndexingQueue;
|
|
6739
6836
|
exports.isErrorResponseBody = isErrorResponseBody;
|