@musnows/scriverse 0.7.8 → 0.7.10
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/dist/ai.js +202 -56
- package/dist/ai.js.map +1 -1
- package/dist/app.js +2 -2
- package/dist/app.js.map +1 -1
- package/dist/database.js +36 -2
- package/dist/database.js.map +1 -1
- package/dist/hybrid-search.js +36 -5
- package/dist/hybrid-search.js.map +1 -1
- package/dist/public/app.js +1 -1
- package/dist/public/index.html +1 -1
- package/dist/store.js +114 -27
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +5 -4
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/ai.js
CHANGED
|
@@ -7,7 +7,7 @@ import { characterExtractionHash, characterExtractionSelectionFingerprint, edita
|
|
|
7
7
|
import { PLATFORM_AI_WORK_ID } from "./database.js";
|
|
8
8
|
import { AppError, notFound } from "./errors.js";
|
|
9
9
|
import { assertOfficialGoogleVertexBaseUrl, fetchGoogleOAuthAccessToken, GoogleVertexTokenCache, maskServiceAccountHint, parseGoogleServiceAccount } from "./google-vertex-auth.js";
|
|
10
|
-
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, buildHybridSearchSnippet,
|
|
10
|
+
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, buildHybridSearchSnippet, documentParagraphLineRangesFromLines, fuseHybridSearchChannels, normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
11
11
|
import { logger, sanitizeError } from "./logger.js";
|
|
12
12
|
import { paginated, paginationSql } from "./pagination.js";
|
|
13
13
|
import { currentRequestActor } from "./request-context.js";
|
|
@@ -183,6 +183,19 @@ const RELATIONSHIP_MAX_FUZZY_SCAN_CHARACTERS = 4_000_000;
|
|
|
183
183
|
const RELATIONSHIP_MAX_FUZZY_MATCHES = 600;
|
|
184
184
|
const RELATIONSHIP_MAX_SOURCE_MATCHES = 256;
|
|
185
185
|
const RELATIONSHIP_PREFILTER_DISABLE_HINT = "请取消勾选“分析前按人物名称和拼音过滤来源”后重新预览";
|
|
186
|
+
function createHybridChapterLineRangeFallbackState() {
|
|
187
|
+
return {
|
|
188
|
+
chapters: new Map(),
|
|
189
|
+
attemptedCandidates: 0,
|
|
190
|
+
chapterLoads: 0,
|
|
191
|
+
repairedCandidates: 0,
|
|
192
|
+
invalidCandidateRows: 0,
|
|
193
|
+
missingChapters: 0,
|
|
194
|
+
chapterVersionMismatches: 0,
|
|
195
|
+
missingParagraphRanges: 0,
|
|
196
|
+
paragraphContentMismatches: 0
|
|
197
|
+
};
|
|
198
|
+
}
|
|
186
199
|
function relationshipCandidateLimitMessage(message) {
|
|
187
200
|
return `${message};${RELATIONSHIP_PREFILTER_DISABLE_HINT}`;
|
|
188
201
|
}
|
|
@@ -1621,8 +1634,12 @@ export class AiManager {
|
|
|
1621
1634
|
if (!normalizedQuery)
|
|
1622
1635
|
return [];
|
|
1623
1636
|
const requestedTypes = options.type ? new Set([options.type]) : new Set(HYBRID_SEARCH_TYPES);
|
|
1624
|
-
if (options.
|
|
1625
|
-
|
|
1637
|
+
if (options.allowedTypes) {
|
|
1638
|
+
const allowedTypes = new Set(options.allowedTypes);
|
|
1639
|
+
for (const type of requestedTypes)
|
|
1640
|
+
if (!allowedTypes.has(type))
|
|
1641
|
+
requestedTypes.delete(type);
|
|
1642
|
+
}
|
|
1626
1643
|
if (requestedTypes.size === 0)
|
|
1627
1644
|
return [];
|
|
1628
1645
|
const hasIndexedSourceTypes = [...requestedTypes].some((type) => type !== "chapter" && type !== "agent-history");
|
|
@@ -1632,8 +1649,9 @@ export class AiManager {
|
|
|
1632
1649
|
const channelLimit = Math.min(200, Math.max(50, resultLimit * 4));
|
|
1633
1650
|
const accepts = (type) => requestedTypes.has(type);
|
|
1634
1651
|
const metadataDetails = new Map();
|
|
1652
|
+
const chapterLineRangeFallbackState = createHybridChapterLineRangeFallbackState();
|
|
1635
1653
|
const metadataCandidates = [...requestedTypes].some((type) => type !== "agent-history")
|
|
1636
|
-
? this.store.search(workId, normalizedQuery).flatMap((item) => {
|
|
1654
|
+
? this.store.search(workId, normalizedQuery, requestedTypes).flatMap((item) => {
|
|
1637
1655
|
const type = String(item.type);
|
|
1638
1656
|
const itemId = String(item.id ?? "");
|
|
1639
1657
|
if (!itemId || !accepts(type))
|
|
@@ -1654,14 +1672,19 @@ export class AiManager {
|
|
|
1654
1672
|
}).slice(0, channelLimit)
|
|
1655
1673
|
: [];
|
|
1656
1674
|
const exactCandidates = [
|
|
1657
|
-
...(requestedTypes.has("chapter")
|
|
1675
|
+
...(requestedTypes.has("chapter")
|
|
1676
|
+
? this.hybridChapterMatches(workId, normalizedQuery, "exact", channelLimit, chapterLineRangeFallbackState)
|
|
1677
|
+
: []),
|
|
1658
1678
|
...(hasIndexedSourceTypes ? this.hybridIndexedSourceMatches(workId, normalizedQuery, "exact", requestedTypes, channelLimit) : []),
|
|
1659
1679
|
...(requestedTypes.has("agent-history") ? this.hybridAgentHistoryMatches(workId, normalizedQuery, channelLimit) : [])
|
|
1660
1680
|
];
|
|
1661
1681
|
const phoneticCandidates = [
|
|
1662
|
-
...(requestedTypes.has("chapter")
|
|
1682
|
+
...(requestedTypes.has("chapter")
|
|
1683
|
+
? this.hybridChapterMatches(workId, normalizedQuery, "phonetic", channelLimit, chapterLineRangeFallbackState)
|
|
1684
|
+
: []),
|
|
1663
1685
|
...(hasIndexedSourceTypes ? this.hybridIndexedSourceMatches(workId, normalizedQuery, "phonetic", requestedTypes, channelLimit) : [])
|
|
1664
1686
|
];
|
|
1687
|
+
this.logHybridChapterLineRangeFallback(workId, chapterLineRangeFallbackState);
|
|
1665
1688
|
return fuseHybridSearchChannels([
|
|
1666
1689
|
{ weight: 1.4, candidates: metadataCandidates },
|
|
1667
1690
|
{ weight: 1, candidates: exactCandidates },
|
|
@@ -1709,16 +1732,19 @@ export class AiManager {
|
|
|
1709
1732
|
};
|
|
1710
1733
|
}).filter((candidate) => candidate.id && candidate.conversationId);
|
|
1711
1734
|
}
|
|
1712
|
-
hybridChapterMatches(workId, query, matchKind, limit) {
|
|
1735
|
+
hybridChapterMatches(workId, query, matchKind, limit, fallbackState) {
|
|
1713
1736
|
let rows;
|
|
1714
1737
|
if (matchKind === "phonetic") {
|
|
1715
1738
|
const tokens = relationshipPinyinSearchTokens(query);
|
|
1716
1739
|
if (tokens.length === 0)
|
|
1717
1740
|
return [];
|
|
1718
|
-
rows = this.store.db.all(`SELECT paragraph.
|
|
1719
|
-
|
|
1741
|
+
rows = this.store.db.all(`SELECT paragraph.id AS paragraph_id, paragraph.chapter_id, paragraph.paragraph_order,
|
|
1742
|
+
paragraph.content AS paragraph_content, line_range.chapter_version AS range_chapter_version,
|
|
1743
|
+
line_range.start_line, line_range.end_line, chapter.version_no AS chapter_version,
|
|
1744
|
+
chapter.title AS chapter_title, volume.title AS volume_title
|
|
1720
1745
|
FROM chapter_paragraph_pinyin_fts pinyin
|
|
1721
1746
|
JOIN chapter_paragraph_search paragraph ON paragraph.id = pinyin.rowid
|
|
1747
|
+
LEFT JOIN chapter_paragraph_line_ranges line_range ON line_range.paragraph_id = paragraph.id
|
|
1722
1748
|
JOIN chapters chapter ON chapter.id = paragraph.chapter_id
|
|
1723
1749
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1724
1750
|
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL
|
|
@@ -1727,10 +1753,13 @@ export class AiManager {
|
|
|
1727
1753
|
LIMIT ?`, workId, ftsPhrase(tokens), limit);
|
|
1728
1754
|
}
|
|
1729
1755
|
else if ([...query].length < 3) {
|
|
1730
|
-
rows = this.store.db.all(`SELECT paragraph.
|
|
1731
|
-
|
|
1756
|
+
rows = this.store.db.all(`SELECT paragraph.id AS paragraph_id, paragraph.chapter_id, paragraph.paragraph_order,
|
|
1757
|
+
paragraph.content AS paragraph_content, line_range.chapter_version AS range_chapter_version,
|
|
1758
|
+
line_range.start_line, line_range.end_line, chapter.version_no AS chapter_version,
|
|
1759
|
+
chapter.title AS chapter_title, volume.title AS volume_title
|
|
1732
1760
|
FROM chapter_paragraph_short_terms term
|
|
1733
1761
|
JOIN chapter_paragraph_search paragraph ON paragraph.id = term.paragraph_id
|
|
1762
|
+
LEFT JOIN chapter_paragraph_line_ranges line_range ON line_range.paragraph_id = paragraph.id
|
|
1734
1763
|
JOIN chapters chapter ON chapter.id = paragraph.chapter_id
|
|
1735
1764
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1736
1765
|
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL AND term.term = ?
|
|
@@ -1738,10 +1767,13 @@ export class AiManager {
|
|
|
1738
1767
|
LIMIT ?`, workId, query, limit);
|
|
1739
1768
|
}
|
|
1740
1769
|
else {
|
|
1741
|
-
rows = this.store.db.all(`SELECT paragraph.
|
|
1742
|
-
|
|
1770
|
+
rows = this.store.db.all(`SELECT paragraph.id AS paragraph_id, paragraph.chapter_id, paragraph.paragraph_order,
|
|
1771
|
+
paragraph.content AS paragraph_content, line_range.chapter_version AS range_chapter_version,
|
|
1772
|
+
line_range.start_line, line_range.end_line, chapter.version_no AS chapter_version,
|
|
1773
|
+
chapter.title AS chapter_title, volume.title AS volume_title
|
|
1743
1774
|
FROM chapter_paragraph_search_fts fts
|
|
1744
1775
|
JOIN chapter_paragraph_search paragraph ON paragraph.id = fts.rowid
|
|
1776
|
+
LEFT JOIN chapter_paragraph_line_ranges line_range ON line_range.paragraph_id = paragraph.id
|
|
1745
1777
|
JOIN chapters chapter ON chapter.id = paragraph.chapter_id
|
|
1746
1778
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1747
1779
|
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL
|
|
@@ -1755,8 +1787,10 @@ export class AiManager {
|
|
|
1755
1787
|
const key = `chapter:${chapterId}`;
|
|
1756
1788
|
if (!chapterId || seen.has(key))
|
|
1757
1789
|
return [];
|
|
1790
|
+
const range = this.hybridChapterLineRange(workId, row, fallbackState);
|
|
1791
|
+
if (!range)
|
|
1792
|
+
return [];
|
|
1758
1793
|
seen.add(key);
|
|
1759
|
-
const range = documentParagraphLineRange(String(row.chapter_content ?? ""), Number(row.paragraph_order));
|
|
1760
1794
|
return [{
|
|
1761
1795
|
key,
|
|
1762
1796
|
type: "chapter",
|
|
@@ -1765,20 +1799,118 @@ export class AiManager {
|
|
|
1765
1799
|
subtitle: String(row.volume_title ?? ""),
|
|
1766
1800
|
snippet: buildHybridSearchSnippet(String(row.paragraph_content ?? ""), query),
|
|
1767
1801
|
matchKind,
|
|
1768
|
-
...
|
|
1802
|
+
...range
|
|
1769
1803
|
}];
|
|
1770
1804
|
});
|
|
1771
1805
|
}
|
|
1806
|
+
hybridChapterLineRange(workId, row, fallbackState) {
|
|
1807
|
+
const chapterVersion = Number(row.chapter_version);
|
|
1808
|
+
const rangeVersion = Number(row.range_chapter_version);
|
|
1809
|
+
const startLine = Number(row.start_line);
|
|
1810
|
+
const endLine = Number(row.end_line);
|
|
1811
|
+
if (Number.isSafeInteger(chapterVersion)
|
|
1812
|
+
&& chapterVersion >= 1
|
|
1813
|
+
&& rangeVersion === chapterVersion
|
|
1814
|
+
&& Number.isSafeInteger(startLine)
|
|
1815
|
+
&& Number.isSafeInteger(endLine)
|
|
1816
|
+
&& startLine >= 1
|
|
1817
|
+
&& endLine >= startLine) {
|
|
1818
|
+
return { startLine, endLine };
|
|
1819
|
+
}
|
|
1820
|
+
fallbackState.attemptedCandidates += 1;
|
|
1821
|
+
const chapterId = String(row.chapter_id ?? "");
|
|
1822
|
+
const paragraphOrder = Number(row.paragraph_order);
|
|
1823
|
+
const paragraphId = Number(row.paragraph_id);
|
|
1824
|
+
if (!chapterId || !Number.isSafeInteger(paragraphOrder) || paragraphOrder < 0 || !Number.isSafeInteger(paragraphId) || paragraphId < 1) {
|
|
1825
|
+
fallbackState.invalidCandidateRows += 1;
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
let cachedChapter = fallbackState.chapters.get(chapterId);
|
|
1829
|
+
if (!fallbackState.chapters.has(chapterId)) {
|
|
1830
|
+
fallbackState.chapterLoads += 1;
|
|
1831
|
+
const chapter = this.store.db.get("SELECT content, version_no FROM chapters WHERE id = ? AND work_id = ? AND deleted_at IS NULL", chapterId, workId);
|
|
1832
|
+
if (!chapter) {
|
|
1833
|
+
fallbackState.chapters.set(chapterId, null);
|
|
1834
|
+
cachedChapter = null;
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
const lines = chapter.content.replace(/\r\n?/gu, "\n").split("\n");
|
|
1838
|
+
cachedChapter = {
|
|
1839
|
+
chapterVersion: Number(chapter.version_no),
|
|
1840
|
+
lines,
|
|
1841
|
+
ranges: documentParagraphLineRangesFromLines(lines)
|
|
1842
|
+
};
|
|
1843
|
+
fallbackState.chapters.set(chapterId, cachedChapter);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (!cachedChapter) {
|
|
1847
|
+
fallbackState.missingChapters += 1;
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
if (cachedChapter.chapterVersion !== chapterVersion) {
|
|
1851
|
+
fallbackState.chapterVersionMismatches += 1;
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
const currentRange = cachedChapter.ranges[paragraphOrder];
|
|
1855
|
+
if (!currentRange) {
|
|
1856
|
+
fallbackState.missingParagraphRanges += 1;
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
const currentParagraph = cachedChapter.lines
|
|
1860
|
+
.slice(currentRange.startLine - 1, currentRange.endLine)
|
|
1861
|
+
.join("\n")
|
|
1862
|
+
.trim();
|
|
1863
|
+
if (currentParagraph !== String(row.paragraph_content ?? "")) {
|
|
1864
|
+
fallbackState.paragraphContentMismatches += 1;
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
this.store.db.run(`INSERT INTO chapter_paragraph_line_ranges (paragraph_id, chapter_version, start_line, end_line)
|
|
1868
|
+
VALUES (?, ?, ?, ?)
|
|
1869
|
+
ON CONFLICT(paragraph_id) DO UPDATE SET
|
|
1870
|
+
chapter_version = excluded.chapter_version,
|
|
1871
|
+
start_line = excluded.start_line,
|
|
1872
|
+
end_line = excluded.end_line`, paragraphId, chapterVersion, currentRange.startLine, currentRange.endLine);
|
|
1873
|
+
fallbackState.repairedCandidates += 1;
|
|
1874
|
+
return currentRange;
|
|
1875
|
+
}
|
|
1876
|
+
logHybridChapterLineRangeFallback(workId, state) {
|
|
1877
|
+
if (state.attemptedCandidates === 0)
|
|
1878
|
+
return;
|
|
1879
|
+
const fields = {
|
|
1880
|
+
workId,
|
|
1881
|
+
attemptedCandidates: state.attemptedCandidates,
|
|
1882
|
+
chapterLoads: state.chapterLoads,
|
|
1883
|
+
repairedCandidates: state.repairedCandidates,
|
|
1884
|
+
invalidCandidateRows: state.invalidCandidateRows,
|
|
1885
|
+
missingChapters: state.missingChapters,
|
|
1886
|
+
chapterVersionMismatches: state.chapterVersionMismatches,
|
|
1887
|
+
missingParagraphRanges: state.missingParagraphRanges,
|
|
1888
|
+
paragraphContentMismatches: state.paragraphContentMismatches
|
|
1889
|
+
};
|
|
1890
|
+
const failedCandidates = state.invalidCandidateRows
|
|
1891
|
+
+ state.missingChapters
|
|
1892
|
+
+ state.chapterVersionMismatches
|
|
1893
|
+
+ state.missingParagraphRanges
|
|
1894
|
+
+ state.paragraphContentMismatches;
|
|
1895
|
+
if (failedCandidates > 0)
|
|
1896
|
+
logger.warn("search.chapter_line_range_fallback", fields);
|
|
1897
|
+
else
|
|
1898
|
+
logger.info("search.chapter_line_range_fallback", fields);
|
|
1899
|
+
}
|
|
1772
1900
|
hybridIndexedSourceMatches(workId, query, matchKind, requestedTypes, limit) {
|
|
1773
1901
|
const tokens = matchKind === "exact" ? relationshipCharacterTokens(query) : relationshipPinyinSearchTokens(query);
|
|
1774
1902
|
if (tokens.length === 0)
|
|
1775
1903
|
return [];
|
|
1776
1904
|
const table = matchKind === "exact" ? "relationship_source_exact_fts" : "relationship_source_pinyin_fts";
|
|
1905
|
+
const sourceTypes = [...requestedTypes].filter((type) => type !== "chapter" && type !== "agent-history");
|
|
1906
|
+
if (sourceTypes.length === 0)
|
|
1907
|
+
return [];
|
|
1908
|
+
const sourceTypePlaceholders = sourceTypes.map(() => "?").join(", ");
|
|
1777
1909
|
const rows = this.store.db.all(`SELECT source.source_type, source.source_id FROM ${table} search_index
|
|
1778
1910
|
JOIN relationship_source_search source ON source.id = search_index.rowid
|
|
1779
|
-
WHERE source.work_id = ? AND ${table} MATCH ?
|
|
1911
|
+
WHERE source.work_id = ? AND source.source_type IN (${sourceTypePlaceholders}) AND ${table} MATCH ?
|
|
1780
1912
|
ORDER BY bm25(${table}), source.source_type, source.source_id
|
|
1781
|
-
LIMIT ?`, workId, ftsPhrase(tokens), limit);
|
|
1913
|
+
LIMIT ?`, workId, ...sourceTypes, ftsPhrase(tokens), limit);
|
|
1782
1914
|
return rows.flatMap((row) => {
|
|
1783
1915
|
const sourceType = String(row.source_type ?? "");
|
|
1784
1916
|
const sourceId = String(row.source_id ?? "");
|
|
@@ -3577,22 +3709,25 @@ export class AiManager {
|
|
|
3577
3709
|
logger.warn("ai.task.cancelled", { taskId, workId: task.workId });
|
|
3578
3710
|
return task;
|
|
3579
3711
|
}
|
|
3580
|
-
contextBudget(input, model) {
|
|
3712
|
+
contextBudget(input, model, existingConversation) {
|
|
3581
3713
|
const contextWindow = numberValue(model, "context_window") || DEFAULT_CONTEXT_WINDOW;
|
|
3582
3714
|
const preset = safeJsonObject(stringValue(model, "preset_json"));
|
|
3583
3715
|
const configuredOutputTokens = typeof preset.max_tokens === "number" ? preset.max_tokens : DEFAULT_MAX_TOKENS;
|
|
3584
3716
|
const outputReserveTokens = Math.max(MIN_OUTPUT_RESERVE_TOKENS, Math.min(configuredOutputTokens, Math.floor(contextWindow * 0.25), contextWindow - MIN_OUTPUT_RESERVE_TOKENS));
|
|
3585
3717
|
const availableInputTokens = Math.max(256, contextWindow - outputReserveTokens - 512);
|
|
3586
|
-
const conversation =
|
|
3587
|
-
?
|
|
3588
|
-
|
|
3718
|
+
const conversation = existingConversation === undefined
|
|
3719
|
+
? input.conversationId
|
|
3720
|
+
? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
|
|
3721
|
+
: null
|
|
3722
|
+
: existingConversation;
|
|
3589
3723
|
const renderedMemory = conversation?.summary ? renderConversationMemory(conversation.summary) : "";
|
|
3590
3724
|
const conversationTokens = conversation
|
|
3591
3725
|
? estimateAiTokens(renderedMemory) + conversation.messages.reduce((total, message) => total + estimateAiTokens(message.content), 0)
|
|
3592
3726
|
: 0;
|
|
3593
3727
|
const conversationBudgetTokens = Math.max(256, Math.floor(availableInputTokens * 0.32));
|
|
3594
3728
|
const instructionTokens = estimateAiTokens(input.instruction);
|
|
3595
|
-
const
|
|
3729
|
+
const roleplayCharacterId = this.roleplayCharacterIdFromConversation(input.workId, conversation);
|
|
3730
|
+
const functionTokens = estimateAiTokens(JSON.stringify(this.enabledAgentTools(input.workId, input.taskType, input.agentToolIds, input.conversationId, roleplayCharacterId)));
|
|
3596
3731
|
const workContextBudgetTokens = Math.max(256, availableInputTokens
|
|
3597
3732
|
- Math.min(conversationTokens, conversationBudgetTokens)
|
|
3598
3733
|
- Math.min(instructionTokens, Math.floor(availableInputTokens * 0.25))
|
|
@@ -3614,10 +3749,11 @@ export class AiManager {
|
|
|
3614
3749
|
getContextUsage(input) {
|
|
3615
3750
|
const { model } = this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
3616
3751
|
const budget = this.contextBudget(input, model);
|
|
3752
|
+
const conversation = budget.conversation;
|
|
3617
3753
|
const contextPlan = this.buildContextPlan(input, model, budget);
|
|
3618
3754
|
const context = contextPlan.context;
|
|
3619
|
-
const messages = this.buildMessages(input, context);
|
|
3620
|
-
const tools = this.enabledAgentTools(input.workId, input.taskType, input.agentToolIds, input.conversationId);
|
|
3755
|
+
const messages = this.buildMessages(input, context, conversation);
|
|
3756
|
+
const tools = this.enabledAgentTools(input.workId, input.taskType, input.agentToolIds, input.conversationId, this.roleplayCharacterIdFromConversation(input.workId, conversation));
|
|
3621
3757
|
const contextWindow = numberValue(model, "context_window") || DEFAULT_CONTEXT_WINDOW;
|
|
3622
3758
|
const messageTokens = messages.reduce((total, message) => total + estimateAiTokens(completionMessageText(message.content)), 0);
|
|
3623
3759
|
const systemPromptTokens = estimateAiTokens(completionMessageText(messages[0]?.content));
|
|
@@ -3628,7 +3764,6 @@ export class AiManager {
|
|
|
3628
3764
|
// 超窗时把可交互上下文压到剩余份额,保证五段分布之和始终等于 contextWindow。
|
|
3629
3765
|
const contextInteractionTokens = Math.max(0, contextWindow - systemPromptTokens - functionTokens - skillsTokens - remainingTokens);
|
|
3630
3766
|
const threshold = Math.min(90, Math.max(50, Number(this.store.getWorkAiSettings(input.workId).contextCompactThreshold) || 85));
|
|
3631
|
-
const conversation = budget.conversation;
|
|
3632
3767
|
const conversationUsagePercent = Number(budget.conversationUsagePercent) || 0;
|
|
3633
3768
|
const configuredOutputTokens = Number(budget.configuredOutputTokens) || DEFAULT_MAX_TOKENS;
|
|
3634
3769
|
const maxOutputUsagePercent = Math.min(100, Math.round(configuredOutputTokens / contextWindow * 100));
|
|
@@ -3715,8 +3850,7 @@ export class AiManager {
|
|
|
3715
3850
|
async prepareConversationContext(input, options = {}) {
|
|
3716
3851
|
const inspection = this.inspectConversationContext(input);
|
|
3717
3852
|
if (inspection.action === "ready") {
|
|
3718
|
-
|
|
3719
|
-
if (conversation.warningPending)
|
|
3853
|
+
if (inspection.usage.contextWarningPending === true)
|
|
3720
3854
|
this.store.setAiConversationContextWarning(input.conversationId, false);
|
|
3721
3855
|
return inspection;
|
|
3722
3856
|
}
|
|
@@ -3766,7 +3900,7 @@ export class AiManager {
|
|
|
3766
3900
|
async compactConversation(input) {
|
|
3767
3901
|
const conversation = this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId);
|
|
3768
3902
|
const { model } = this.resolveModel(input.workId, "chat", input.modelId);
|
|
3769
|
-
const budget = this.contextBudget({ ...input, taskType: "chat", instruction: "" }, model);
|
|
3903
|
+
const budget = this.contextBudget({ ...input, taskType: "chat", instruction: "" }, model, conversation);
|
|
3770
3904
|
const recentTokenBudget = Math.max(128, Math.floor(Number(budget.conversationBudgetTokens) * 0.75));
|
|
3771
3905
|
let retainedMessageCount = 0;
|
|
3772
3906
|
let retainedTokens = 0;
|
|
@@ -3827,12 +3961,17 @@ export class AiManager {
|
|
|
3827
3961
|
changed: true
|
|
3828
3962
|
};
|
|
3829
3963
|
}
|
|
3830
|
-
buildMessages(input, context) {
|
|
3831
|
-
const
|
|
3964
|
+
buildMessages(input, context, existingConversation) {
|
|
3965
|
+
const conversation = existingConversation === undefined
|
|
3966
|
+
? input.conversationId
|
|
3967
|
+
? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
|
|
3968
|
+
: null
|
|
3969
|
+
: existingConversation;
|
|
3970
|
+
const roleplayCharacterId = this.roleplayCharacterIdFromConversation(input.workId, conversation);
|
|
3832
3971
|
const roleplayPrompt = roleplayCharacterId ? this.buildRoleplaySystemPrompt(roleplayCharacterId) : "";
|
|
3833
3972
|
const platformPrompt = roleplayCharacterId ? "" : String(this.store.getPlatformAiSettings().systemPrompt ?? "").trim();
|
|
3834
3973
|
const workPrompt = roleplayCharacterId ? "" : String(this.store.getWorkAiSettings(input.workId).systemPrompt ?? "").trim();
|
|
3835
|
-
const enabledToolIds = this.enabledAgentToolIds(input.workId, input.taskType, input.agentToolIds, input.conversationId);
|
|
3974
|
+
const enabledToolIds = this.enabledAgentToolIds(input.workId, input.taskType, input.agentToolIds, input.conversationId, roleplayCharacterId);
|
|
3836
3975
|
const toolGuidance = enabledToolIds.includes("recall_self") || enabledToolIds.includes("recall_relationship")
|
|
3837
3976
|
? [
|
|
3838
3977
|
`当前可用的内部记忆能力是:${enabledToolIds.join("、")}。不要向用户提及工具、调用过程、资料库或检索结果。`,
|
|
@@ -3904,9 +4043,6 @@ export class AiManager {
|
|
|
3904
4043
|
]);
|
|
3905
4044
|
// 分析任务指令含服务端 CHAPTER/json 等标记,不能转义;分区边界仍靠外层标签约束。
|
|
3906
4045
|
const currentInstruction = wrapAiContextRegion(roleplayCharacterId ? "user_message" : "author_instruction", input.instruction, { escape: false });
|
|
3907
|
-
const conversation = input.conversationId
|
|
3908
|
-
? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
|
|
3909
|
-
: null;
|
|
3910
4046
|
if (!conversation) {
|
|
3911
4047
|
return [
|
|
3912
4048
|
{ role: "system", content: systemPrompt },
|
|
@@ -3945,7 +4081,8 @@ export class AiManager {
|
|
|
3945
4081
|
}
|
|
3946
4082
|
buildContextPlan(input, model, existingBudget, persistKeywordInjections = false) {
|
|
3947
4083
|
const budget = existingBudget ?? this.contextBudget(input, model);
|
|
3948
|
-
const
|
|
4084
|
+
const conversation = budget.conversation;
|
|
4085
|
+
const roleplayCharacterId = this.roleplayCharacterIdFromConversation(input.workId, conversation);
|
|
3949
4086
|
const settings = this.store.getWorkAiSettings(input.workId);
|
|
3950
4087
|
const configuredScope = {
|
|
3951
4088
|
...input.scope,
|
|
@@ -4013,13 +4150,18 @@ export class AiManager {
|
|
|
4013
4150
|
}
|
|
4014
4151
|
return this.mergeInstructionEntityMatches(scope, matches);
|
|
4015
4152
|
}
|
|
4016
|
-
buildContext(input, model) {
|
|
4017
|
-
return collapseAiBlankLines(this.buildContextPlan(input, model,
|
|
4153
|
+
buildContext(input, model, existingBudget) {
|
|
4154
|
+
return collapseAiBlankLines(this.buildContextPlan(input, model, existingBudget, true).context);
|
|
4018
4155
|
}
|
|
4019
4156
|
roleplayCharacterId(workId, conversationId) {
|
|
4020
4157
|
if (!conversationId)
|
|
4021
4158
|
return null;
|
|
4022
4159
|
const conversation = this.store.getAiConversationContext(conversationId, workId);
|
|
4160
|
+
return this.roleplayCharacterIdFromConversation(workId, conversation);
|
|
4161
|
+
}
|
|
4162
|
+
roleplayCharacterIdFromConversation(workId, conversation) {
|
|
4163
|
+
if (!conversation)
|
|
4164
|
+
return null;
|
|
4023
4165
|
if (conversation.roleplayCharacterId) {
|
|
4024
4166
|
const permissions = this.store.getWork(workId).modulePermissions;
|
|
4025
4167
|
if (!canReadWorkModule(permissions, "characters")) {
|
|
@@ -4058,10 +4200,12 @@ export class AiManager {
|
|
|
4058
4200
|
JSON.stringify(roleCard)
|
|
4059
4201
|
].join("\n");
|
|
4060
4202
|
}
|
|
4061
|
-
enabledAgentToolIds(workId, taskType, requestedToolIds, conversationId) {
|
|
4203
|
+
enabledAgentToolIds(workId, taskType, requestedToolIds, conversationId, roleplayCharacterIdOverride) {
|
|
4062
4204
|
if (taskType !== "chat" && requestedToolIds === undefined)
|
|
4063
4205
|
return [];
|
|
4064
|
-
const roleplayCharacterId =
|
|
4206
|
+
const roleplayCharacterId = roleplayCharacterIdOverride === undefined
|
|
4207
|
+
? this.roleplayCharacterId(workId, conversationId)
|
|
4208
|
+
: roleplayCharacterIdOverride;
|
|
4065
4209
|
const permissions = this.store.getWork(workId).modulePermissions;
|
|
4066
4210
|
if (roleplayCharacterId) {
|
|
4067
4211
|
const requested = requestedToolIds ? new Set(requestedToolIds) : null;
|
|
@@ -4085,8 +4229,9 @@ export class AiManager {
|
|
|
4085
4229
|
&& (!requested || requested.has(toolId))
|
|
4086
4230
|
&& this.canReadWithAgentTool(permissions, toolId));
|
|
4087
4231
|
}
|
|
4088
|
-
enabledAgentTools(workId, taskType, requestedToolIds, conversationId) {
|
|
4089
|
-
return this.enabledAgentToolIds(workId, taskType, requestedToolIds, conversationId
|
|
4232
|
+
enabledAgentTools(workId, taskType, requestedToolIds, conversationId, roleplayCharacterIdOverride) {
|
|
4233
|
+
return this.enabledAgentToolIds(workId, taskType, requestedToolIds, conversationId, roleplayCharacterIdOverride)
|
|
4234
|
+
.map((toolId) => AGENT_TOOL_DEFINITIONS[toolId]);
|
|
4090
4235
|
}
|
|
4091
4236
|
canReadWithAgentTool(permissions, toolId) {
|
|
4092
4237
|
if (toolId === "search_story_entities") {
|
|
@@ -4524,11 +4669,7 @@ export class AiManager {
|
|
|
4524
4669
|
if (name === "story_index") {
|
|
4525
4670
|
const { offset, limit, cursor } = args;
|
|
4526
4671
|
const work = this.store.getWork(workId);
|
|
4527
|
-
const
|
|
4528
|
-
const summaries = new Map(this.store.listCurrentChapterInsights(workId).map((item) => [String(item.chapterId), String(item.summary)]));
|
|
4529
|
-
const chapters = tree.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
|
|
4530
|
-
id: String(chapter.id), volumeTitle: String(volume.title), title: String(chapter.title), versionNo: Number(chapter.versionNo), summary: summaries.get(String(chapter.id)) ?? ""
|
|
4531
|
-
})));
|
|
4672
|
+
const chapterPage = this.store.getStoryIndexChapterPage(workId, offset, limit);
|
|
4532
4673
|
const workRecords = structuralToolResultRecords([{
|
|
4533
4674
|
id: work.id,
|
|
4534
4675
|
title: work.title,
|
|
@@ -4539,7 +4680,7 @@ export class AiManager {
|
|
|
4539
4680
|
chapterCount: work.chapterCount,
|
|
4540
4681
|
wordCount: work.wordCount
|
|
4541
4682
|
}], maximumRecordChars).map((record) => ({ ...record, _toolResultSection: "work" }));
|
|
4542
|
-
const chapterRecords = structuralToolResultRecords(chapters
|
|
4683
|
+
const chapterRecords = structuralToolResultRecords(chapterPage.chapters, maximumRecordChars)
|
|
4543
4684
|
.map((record) => ({ ...record, _toolResultSection: "chapter" }));
|
|
4544
4685
|
const result = paginateToolResultRecords([...workRecords, ...chapterRecords], cursor, (page, pagination) => {
|
|
4545
4686
|
const pageWork = page.flatMap((record) => {
|
|
@@ -4559,10 +4700,10 @@ export class AiManager {
|
|
|
4559
4700
|
data: {
|
|
4560
4701
|
...(pageWork[0] ? { work: pageWork[0] } : {}),
|
|
4561
4702
|
...(pageWork.length > 1 ? { workFragments: pageWork } : {}),
|
|
4562
|
-
totalChapters:
|
|
4703
|
+
totalChapters: chapterPage.totalChapters,
|
|
4563
4704
|
offset,
|
|
4564
4705
|
chapters: pageChapters,
|
|
4565
|
-
nextOffset: pagination.nextCursor === null && offset + limit <
|
|
4706
|
+
nextOffset: pagination.nextCursor === null && offset + limit < chapterPage.totalChapters ? offset + limit : null
|
|
4566
4707
|
},
|
|
4567
4708
|
pagination
|
|
4568
4709
|
};
|
|
@@ -4775,7 +4916,10 @@ export class AiManager {
|
|
|
4775
4916
|
});
|
|
4776
4917
|
}
|
|
4777
4918
|
async generate(input, onDelta) {
|
|
4778
|
-
const
|
|
4919
|
+
const conversation = input.conversationId
|
|
4920
|
+
? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
|
|
4921
|
+
: null;
|
|
4922
|
+
const generationRoleplayCharacterId = this.roleplayCharacterIdFromConversation(input.workId, conversation);
|
|
4779
4923
|
const { model, provider } = this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
4780
4924
|
const preset = safeJsonObject(stringValue(model, "preset_json"));
|
|
4781
4925
|
const requestedParameters = {
|
|
@@ -4785,14 +4929,15 @@ export class AiManager {
|
|
|
4785
4929
|
const configuredOutputTokens = Number(requestedParameters.max_tokens) || DEFAULT_MAX_TOKENS;
|
|
4786
4930
|
const contextCompactThreshold = Math.min(90, Math.max(50, Number(this.store.getWorkAiSettings(input.workId).contextCompactThreshold) || 85));
|
|
4787
4931
|
let effectiveInput = input;
|
|
4788
|
-
let
|
|
4789
|
-
let
|
|
4932
|
+
let effectiveBudget = this.contextBudget(effectiveInput, model, conversation);
|
|
4933
|
+
let context = this.buildContext(effectiveInput, model, effectiveBudget);
|
|
4934
|
+
let messages = this.buildMessages(effectiveInput, context, conversation);
|
|
4790
4935
|
const allowedToolIds = new Set(input.disableTools
|
|
4791
4936
|
? []
|
|
4792
|
-
: this.enabledAgentToolIds(input.workId, input.taskType, input.agentToolIds, input.conversationId));
|
|
4937
|
+
: this.enabledAgentToolIds(input.workId, input.taskType, input.agentToolIds, input.conversationId, generationRoleplayCharacterId));
|
|
4793
4938
|
let tools = input.disableTools
|
|
4794
4939
|
? []
|
|
4795
|
-
: this.enabledAgentTools(input.workId, input.taskType, input.agentToolIds, input.conversationId);
|
|
4940
|
+
: this.enabledAgentTools(input.workId, input.taskType, input.agentToolIds, input.conversationId, generationRoleplayCharacterId);
|
|
4796
4941
|
let parameters;
|
|
4797
4942
|
try {
|
|
4798
4943
|
parameters = this.constrainParametersForContext(model, messages, requestedParameters, tools);
|
|
@@ -4803,8 +4948,9 @@ export class AiManager {
|
|
|
4803
4948
|
if (tools.length === 0)
|
|
4804
4949
|
throw initialContextWindowError(error, provider, model);
|
|
4805
4950
|
effectiveInput = { ...input, agentToolIds: [] };
|
|
4806
|
-
|
|
4807
|
-
|
|
4951
|
+
effectiveBudget = this.contextBudget(effectiveInput, model, conversation);
|
|
4952
|
+
context = this.buildContext(effectiveInput, model, effectiveBudget);
|
|
4953
|
+
messages = this.buildMessages(effectiveInput, context, conversation);
|
|
4808
4954
|
tools = [];
|
|
4809
4955
|
allowedToolIds.clear();
|
|
4810
4956
|
try {
|