endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
78 lines • 3.04 kB
JavaScript
/**
* Worldview full-text search.
*
* Unlike the story domain (which ships a pre-built `search.json` of 9271
* scenes), the worldview corpus is small (~414 PRTS items) and its body
* text requires the TextTable bridge to resolve. So instead of a shipped
* index, we build the search corpus **eagerly at first query**: for each
* PRTS item, concatenate its resolved title + body into one searchable
* string. At ~414 items this is well under 100ms and avoids a mirror-side
* build step (one less thing to keep in sync).
*
* Search is regex (case-insensitive), matching the story domain's contract.
* Invalid patterns degrade to literal substring matching, same as story.
*
* Depends on `./worldviewCore.js` for the item catalog and content resolution.
*/
import { allItems, resolveContent } from "./worldviewCore.js";
import { resolveText } from "./texts.js";
let _corpus = null;
/** Reset the search corpus. Called by the barrel orchestrator on sync. */
export function clearWorldviewSearchCache() {
_corpus = null;
}
function corpus() {
if (_corpus !== null)
return _corpus;
const docs = [];
for (const [id, item] of allItems()) {
const title = resolveText(item.name, undefined, id);
const body = resolveContent(item.contentId);
// Skip items with neither title nor body — they'd only produce empty
// matches and clutter results. (Some multimedia items have titles but
// no transcript; those stay searchable by title.)
const text = `${title}\n${body}`.trim();
if (text.length > 0) {
docs.push({ itemId: id, type: item.type, text });
}
}
_corpus = docs;
return _corpus;
}
/**
* Full-text search across all PRTS documents.
*
* Searches each item's resolved title + body. Returns hits with a snippet
* around the first match, capped at `maxResults`.
*/
export function searchWorldview(pattern, maxResults = 30) {
let re;
try {
re = new RegExp(pattern, "i");
}
catch {
const literal = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
re = new RegExp(literal, "i");
}
const out = [];
for (const doc of corpus()) {
if (re.test(doc.text)) {
const item = allItems().get(doc.itemId);
const title = item
? resolveText(item.name, undefined, doc.itemId)
: doc.itemId;
// Extract a snippet around the first match.
const matchIdx = doc.text.search(re);
const start = Math.max(0, matchIdx - 30);
const end = Math.min(doc.text.length, matchIdx + 80);
const snippet = (start > 0 ? "..." : "") +
doc.text.slice(start, end).replace(/\s+/g, " ").trim() +
(end < doc.text.length ? "..." : "");
out.push({ itemId: doc.itemId, type: doc.type, title, snippet });
if (out.length >= maxResults)
break;
}
}
return out;
}
//# sourceMappingURL=worldviewSearch.js.map