bunnyquery 1.3.4 → 1.4.0
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 +76 -7
- package/bunnyquery.js +177 -44
- package/dist/engine.cjs +175 -40
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +72 -1
- package/dist/engine.d.ts +72 -1
- package/dist/engine.mjs +170 -41
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/attachment_parsers.ts +112 -0
- package/src/engine/config.ts +11 -0
- package/src/engine/index.ts +12 -0
- package/src/engine/office.ts +26 -4
- package/src/engine/prompts/indexing_user_message.ts +20 -0
- package/src/engine/requests.ts +17 -5
- package/src/engine/session.ts +63 -0
package/dist/engine.cjs
CHANGED
|
@@ -1,9 +1,57 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/engine/attachment_parsers.ts
|
|
4
|
+
var MAX_PARSED_CONTENT_CHARS = 2e5;
|
|
5
|
+
var _parsers = [];
|
|
6
|
+
function registerAttachmentParser(parser) {
|
|
7
|
+
if (parser && typeof parser.match === "function" && typeof parser.parse === "function" && _parsers.indexOf(parser) === -1) {
|
|
8
|
+
_parsers.push(parser);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function clearAttachmentParsers() {
|
|
12
|
+
_parsers.length = 0;
|
|
13
|
+
}
|
|
14
|
+
function getAttachmentParsers() {
|
|
15
|
+
return _parsers.slice();
|
|
16
|
+
}
|
|
17
|
+
function findAttachmentParser(name, mime) {
|
|
18
|
+
for (let i = 0; i < _parsers.length; i++) {
|
|
19
|
+
try {
|
|
20
|
+
if (_parsers[i].match({ name, mime })) return _parsers[i];
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
async function parseAttachmentContent(file, name, mime) {
|
|
27
|
+
const parser = findAttachmentParser(name, mime);
|
|
28
|
+
if (!parser) return null;
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = await parser.parse(file);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(
|
|
34
|
+
`[chat-engine] attachment parser ${parser.name || "(unnamed)"} failed for ${name}:`,
|
|
35
|
+
err
|
|
36
|
+
);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
let text = (raw == null ? "" : String(raw)).trim();
|
|
40
|
+
if (!text) return null;
|
|
41
|
+
if (text.length > MAX_PARSED_CONTENT_CHARS) {
|
|
42
|
+
text = text.slice(0, MAX_PARSED_CONTENT_CHARS) + `
|
|
43
|
+
...[truncated for length; original ${text.length} characters]`;
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
3
48
|
// src/engine/config.ts
|
|
4
49
|
var _config = null;
|
|
5
50
|
function configureChatEngine(config) {
|
|
6
51
|
_config = config;
|
|
52
|
+
if (config.attachmentParsers) {
|
|
53
|
+
for (const parser of config.attachmentParsers) registerAttachmentParser(parser);
|
|
54
|
+
}
|
|
7
55
|
}
|
|
8
56
|
function chatEngineConfig() {
|
|
9
57
|
if (!_config) {
|
|
@@ -30,13 +78,41 @@ var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
30
78
|
"pptx",
|
|
31
79
|
"pptm",
|
|
32
80
|
"hwp",
|
|
33
|
-
"hwpx"
|
|
81
|
+
"hwpx",
|
|
82
|
+
"ods",
|
|
83
|
+
"odt",
|
|
84
|
+
"odp",
|
|
85
|
+
"epub"
|
|
86
|
+
]);
|
|
87
|
+
var WEB_FETCHABLE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
88
|
+
"csv",
|
|
89
|
+
"tsv",
|
|
90
|
+
"tab",
|
|
91
|
+
"txt",
|
|
92
|
+
"text",
|
|
93
|
+
"md",
|
|
94
|
+
"markdown",
|
|
95
|
+
"json",
|
|
96
|
+
"ndjson",
|
|
97
|
+
"jsonl",
|
|
98
|
+
"xml",
|
|
99
|
+
"yaml",
|
|
100
|
+
"yml",
|
|
101
|
+
"log",
|
|
102
|
+
// RTF is a TEXT format (not a binary zip), so web_fetch can read it and the
|
|
103
|
+
// model decodes its control words. Pin it here so a `.rtf` reported as
|
|
104
|
+
// `application/msword` isn't misrouted to server-side extraction (which has no
|
|
105
|
+
// .rtf extractor → "unsupported format" note).
|
|
106
|
+
"rtf",
|
|
107
|
+
"htm",
|
|
108
|
+
"html"
|
|
34
109
|
]);
|
|
35
110
|
function isOfficeFile(name, mime) {
|
|
36
111
|
const ext = (name || "").split(".").pop()?.toLowerCase() || "";
|
|
37
112
|
if (OFFICE_FILE_EXTENSIONS.has(ext)) return true;
|
|
113
|
+
if (WEB_FETCHABLE_TEXT_EXTENSIONS.has(ext)) return false;
|
|
38
114
|
const m = (mime || "").toLowerCase();
|
|
39
|
-
return m.includes("officedocument") || m.includes("hwp") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
|
|
115
|
+
return m.includes("officedocument") || m.includes("opendocument") || m.includes("hwp") || m.includes("epub") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
|
|
40
116
|
}
|
|
41
117
|
var _extractPlaceholderSeq = 0;
|
|
42
118
|
function makeExtractPlaceholder(seed) {
|
|
@@ -157,6 +233,14 @@ File metadata:
|
|
|
157
233
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
158
234
|
` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
|
|
159
235
|
` : "");
|
|
236
|
+
if (options?.inlineContent) {
|
|
237
|
+
return head + `
|
|
238
|
+
The file's content was parsed by the client and is provided inline below. Read it directly \u2014 do NOT fetch any URL for this file. Use the storage path above (not this content) for the "src::" unique_id.
|
|
239
|
+
|
|
240
|
+
----- BEGIN FILE CONTENT -----
|
|
241
|
+
${options.inlineContent}
|
|
242
|
+
----- END FILE CONTENT -----`;
|
|
243
|
+
}
|
|
160
244
|
if (options?.inlineContentPlaceholder) {
|
|
161
245
|
return head + `
|
|
162
246
|
The file's text content was extracted on the server and is provided inline below. Read it directly \u2014 do NOT fetch any URL for this file. Use the storage path above (not this content) for the "src::" unique_id.
|
|
@@ -655,14 +739,14 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
655
739
|
});
|
|
656
740
|
}
|
|
657
741
|
async function notifyAgentSaveAttachment(info) {
|
|
658
|
-
const { platform, service, owner, attachment } = info;
|
|
659
|
-
const office = isOfficeFile(attachment.name, attachment.mime);
|
|
742
|
+
const { platform, service, owner, attachment, parsedContent } = info;
|
|
743
|
+
const office = !parsedContent && isOfficeFile(attachment.name, attachment.mime);
|
|
660
744
|
const placeholder = office ? makeExtractPlaceholder(attachment.storagePath) : void 0;
|
|
661
745
|
const extractContent = office && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
|
|
662
746
|
const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
|
|
663
747
|
const userMessage = buildIndexingUserMessage(
|
|
664
748
|
attachment,
|
|
665
|
-
placeholder ? { inlineContentPlaceholder: placeholder } : void 0
|
|
749
|
+
parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : void 0
|
|
666
750
|
);
|
|
667
751
|
const systemPrompt = buildIndexingSystemPrompt({
|
|
668
752
|
service,
|
|
@@ -1248,6 +1332,7 @@ var ChatSession = class {
|
|
|
1248
1332
|
this.enqueueTypewrite(aiIdx, answer, lid);
|
|
1249
1333
|
}
|
|
1250
1334
|
}
|
|
1335
|
+
this._removeStrayPendingAssistants();
|
|
1251
1336
|
this.promoteNextQueuedToRunning();
|
|
1252
1337
|
this.updateHistoryCache();
|
|
1253
1338
|
this.host.notify();
|
|
@@ -1290,6 +1375,7 @@ var ChatSession = class {
|
|
|
1290
1375
|
if (thPos2 !== -1) this.state.messages.splice(thPos2, 1);
|
|
1291
1376
|
}
|
|
1292
1377
|
if (serverId) this.cancelledServerIds.delete(serverId);
|
|
1378
|
+
this._removeStrayPendingAssistants();
|
|
1293
1379
|
this.promoteNextQueuedToRunning();
|
|
1294
1380
|
this.updateHistoryCache();
|
|
1295
1381
|
this.host.notify();
|
|
@@ -1303,6 +1389,7 @@ var ChatSession = class {
|
|
|
1303
1389
|
return;
|
|
1304
1390
|
}
|
|
1305
1391
|
this.insertAtTarget({ role: "assistant", content: getErrorMessage(err), isError: true }, targetIdx);
|
|
1392
|
+
this._removeStrayPendingAssistants();
|
|
1306
1393
|
this.promoteNextQueuedToRunning();
|
|
1307
1394
|
this.updateHistoryCache();
|
|
1308
1395
|
this.host.notify();
|
|
@@ -1464,16 +1551,48 @@ var ChatSession = class {
|
|
|
1464
1551
|
if (pendingIdx === -1) return Promise.resolve();
|
|
1465
1552
|
if (latest.isError || !latest.content) {
|
|
1466
1553
|
this.state.messages[pendingIdx] = { role: "assistant", content: latest.content || "", isError: !!latest.isError };
|
|
1554
|
+
this._removeStrayPendingAssistants();
|
|
1467
1555
|
this.host.notify();
|
|
1468
1556
|
this.promoteNextQueuedToRunning();
|
|
1469
1557
|
return Promise.resolve();
|
|
1470
1558
|
}
|
|
1471
1559
|
var lid = this._newLocalId();
|
|
1472
1560
|
this.state.messages[pendingIdx] = { role: "assistant", content: "", isPending: false, _localId: lid };
|
|
1561
|
+
this._removeStrayPendingAssistants();
|
|
1473
1562
|
this.host.notify();
|
|
1474
1563
|
this.promoteNextQueuedToRunning();
|
|
1475
1564
|
return this.enqueueTypewrite(pendingIdx, latest.content, lid);
|
|
1476
1565
|
}
|
|
1566
|
+
// Remove any leftover non-background pending ("Thinking…") assistant bubbles.
|
|
1567
|
+
// There is normally at most ONE such bubble at a time (promoteNext* refuses to
|
|
1568
|
+
// add a second), so any extra is a duplicate — it appears when a concurrent
|
|
1569
|
+
// history refetch re-maps the still-"running" turn into a pending placeholder
|
|
1570
|
+
// (with a real _serverItemId) while the local pending bubble (no _serverItemId)
|
|
1571
|
+
// is rescued and re-appended (see loadHistory rescue below). Each resolve path
|
|
1572
|
+
// only replaces the FIRST pending bubble, so without this a stray "Thinking…"
|
|
1573
|
+
// survives next to the reply/error. MUST run AFTER the resolved bubble has been
|
|
1574
|
+
// made non-pending and BEFORE promoteNext*() (so a freshly-promoted Thinking,
|
|
1575
|
+
// which is added only once no pending assistant remains, is preserved).
|
|
1576
|
+
_removeStrayPendingAssistants() {
|
|
1577
|
+
for (var k = this.state.messages.length - 1; k >= 0; k--) {
|
|
1578
|
+
var m = this.state.messages[k];
|
|
1579
|
+
if (m.isPending && m.role === "assistant" && !m.isBackgroundTask) this.state.messages.splice(k, 1);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// Drop the pending flags on the resolved turn's USER bubble (preserving its
|
|
1583
|
+
// content + background-task marker). Needed because a bg "Indexing:" turn's user
|
|
1584
|
+
// bubble carries isPendingInProcess; leaving it set keeps the bubble visually
|
|
1585
|
+
// stuck and keeps its bgTaskQueue entry alive forever.
|
|
1586
|
+
_clearPendingUserBubble(itemId) {
|
|
1587
|
+
var uIdx = this.state.messages.findIndex(function(m) {
|
|
1588
|
+
return m.role === "user" && m._serverItemId === itemId && (m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
|
|
1589
|
+
});
|
|
1590
|
+
if (uIdx === -1) return;
|
|
1591
|
+
var u = this.state.messages[uIdx];
|
|
1592
|
+
var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
|
|
1593
|
+
if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
|
|
1594
|
+
this.state.messages[uIdx] = cleaned;
|
|
1595
|
+
}
|
|
1477
1596
|
// If an immediate-send request for the current cache key is still in flight
|
|
1478
1597
|
// (e.g. the view unmounted then remounted mid-request), show the sending
|
|
1479
1598
|
// state, await it, then render the reply from the cache. Skipped when the
|
|
@@ -1512,6 +1631,7 @@ var ChatSession = class {
|
|
|
1512
1631
|
return m.isPending && m._serverItemId === itemId;
|
|
1513
1632
|
});
|
|
1514
1633
|
if (idx !== -1) {
|
|
1634
|
+
this._clearPendingUserBubble(itemId);
|
|
1515
1635
|
if (isErr) {
|
|
1516
1636
|
this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
|
|
1517
1637
|
this.host.notify();
|
|
@@ -1661,12 +1781,16 @@ var ChatSession = class {
|
|
|
1661
1781
|
self.state.messages.forEach(function(m) {
|
|
1662
1782
|
if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
|
|
1663
1783
|
});
|
|
1784
|
+
var mappedHasPendingAssistant = mapped.some(function(m) {
|
|
1785
|
+
return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
|
|
1786
|
+
});
|
|
1664
1787
|
var rescued = [];
|
|
1665
1788
|
for (var ri = 0; ri < self.state.messages.length; ri++) {
|
|
1666
1789
|
var mm = self.state.messages[ri];
|
|
1667
1790
|
if (mm.isBackgroundTask) continue;
|
|
1668
1791
|
if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
|
|
1669
1792
|
if (!mm._serverItemId) {
|
|
1793
|
+
if (mappedHasPendingAssistant) continue;
|
|
1670
1794
|
if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
|
|
1671
1795
|
else if (self.state.sending && mm.role === "user") {
|
|
1672
1796
|
var next = self.state.messages[ri + 1];
|
|
@@ -1824,44 +1948,49 @@ var ChatSession = class {
|
|
|
1824
1948
|
att.storagePath = member.storagePath;
|
|
1825
1949
|
}
|
|
1826
1950
|
var mime = member.file.type || self.host.getMimeType(member.file.name);
|
|
1827
|
-
return
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
url
|
|
1841
|
-
}
|
|
1842
|
-
}).then(function(ack) {
|
|
1843
|
-
if (ack && typeof ack.id === "string") {
|
|
1844
|
-
self.bgTaskQueue.push({
|
|
1845
|
-
serviceId: id.serviceId,
|
|
1846
|
-
platform: id.platform,
|
|
1847
|
-
id: ack.id,
|
|
1848
|
-
filename: member.file.name,
|
|
1951
|
+
return Promise.resolve(
|
|
1952
|
+
parseAttachmentContent(member.file, member.file.name, mime || void 0)
|
|
1953
|
+
).then(function(parsedContent) {
|
|
1954
|
+
return notifyAgentSaveAttachment({
|
|
1955
|
+
platform: id.platform,
|
|
1956
|
+
model: id.model,
|
|
1957
|
+
service: id.serviceId,
|
|
1958
|
+
owner: id.owner,
|
|
1959
|
+
userId: id.userId || id.serviceId,
|
|
1960
|
+
serviceName: id.serviceName,
|
|
1961
|
+
serviceDescription: id.serviceDescription,
|
|
1962
|
+
attachment: {
|
|
1963
|
+
name: member.file.name,
|
|
1849
1964
|
storagePath: member.storagePath,
|
|
1850
|
-
isReindex: hadExists,
|
|
1851
1965
|
mime: mime || void 0,
|
|
1852
1966
|
size: member.file.size,
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1967
|
+
url
|
|
1968
|
+
},
|
|
1969
|
+
parsedContent: parsedContent || void 0
|
|
1970
|
+
}).then(function(ack) {
|
|
1971
|
+
if (ack && typeof ack.id === "string") {
|
|
1972
|
+
self.bgTaskQueue.push({
|
|
1973
|
+
serviceId: id.serviceId,
|
|
1974
|
+
platform: id.platform,
|
|
1975
|
+
id: ack.id,
|
|
1976
|
+
filename: member.file.name,
|
|
1977
|
+
storagePath: member.storagePath,
|
|
1978
|
+
isReindex: hadExists,
|
|
1979
|
+
mime: mime || void 0,
|
|
1980
|
+
size: member.file.size,
|
|
1981
|
+
status: ack.status === "running" ? "running" : "pending",
|
|
1982
|
+
poll: ack.poll
|
|
1983
|
+
});
|
|
1984
|
+
self.drainBgTaskQueue();
|
|
1985
|
+
}
|
|
1986
|
+
}, function(e) {
|
|
1987
|
+
console.error("[chat-engine] indexing request failed", e);
|
|
1988
|
+
anyIndexFailed = true;
|
|
1989
|
+
if (!att.errorCode && !att.errorDetail) {
|
|
1990
|
+
att.errorCode = e && (e.code || e.body && e.body.code) || "";
|
|
1991
|
+
att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1865
1994
|
});
|
|
1866
1995
|
});
|
|
1867
1996
|
});
|
|
@@ -1953,6 +2082,7 @@ exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
|
|
|
1953
2082
|
exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
|
|
1954
2083
|
exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
|
|
1955
2084
|
exports.MAX_HISTORY_MESSAGES = MAX_HISTORY_MESSAGES;
|
|
2085
|
+
exports.MAX_PARSED_CONTENT_CHARS = MAX_PARSED_CONTENT_CHARS;
|
|
1956
2086
|
exports.MCP_NAME = MCP_NAME;
|
|
1957
2087
|
exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
|
|
1958
2088
|
exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
|
|
@@ -1967,6 +2097,7 @@ exports.callClaudeWithMcp = callClaudeWithMcp;
|
|
|
1967
2097
|
exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
|
|
1968
2098
|
exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
|
|
1969
2099
|
exports.chatEngineConfig = chatEngineConfig;
|
|
2100
|
+
exports.clearAttachmentParsers = clearAttachmentParsers;
|
|
1970
2101
|
exports.composeUserMessage = composeUserMessage;
|
|
1971
2102
|
exports.configureChatEngine = configureChatEngine;
|
|
1972
2103
|
exports.createInlineLinkRegex = createInlineLinkRegex;
|
|
@@ -1978,6 +2109,8 @@ exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
|
|
|
1978
2109
|
exports.extractOpenAIText = extractOpenAIText;
|
|
1979
2110
|
exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHref;
|
|
1980
2111
|
exports.filterListByClearHorizon = filterListByClearHorizon;
|
|
2112
|
+
exports.findAttachmentParser = findAttachmentParser;
|
|
2113
|
+
exports.getAttachmentParsers = getAttachmentParsers;
|
|
1981
2114
|
exports.getChatHistory = getChatHistory;
|
|
1982
2115
|
exports.getContextWindow = getContextWindow;
|
|
1983
2116
|
exports.getErrorMessage = getErrorMessage;
|
|
@@ -1994,6 +2127,8 @@ exports.mapHistoryListToMessages = mapHistoryListToMessages;
|
|
|
1994
2127
|
exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
|
|
1995
2128
|
exports.normalizeTextContent = normalizeTextContent;
|
|
1996
2129
|
exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
|
|
2130
|
+
exports.parseAttachmentContent = parseAttachmentContent;
|
|
2131
|
+
exports.registerAttachmentParser = registerAttachmentParser;
|
|
1997
2132
|
exports.safeDecodeURIComponent = safeDecodeURIComponent;
|
|
1998
2133
|
exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
|
|
1999
2134
|
exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
|