endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
84 lines • 2.99 kB
JavaScript
/**
* Story full-text search.
*
* Uses the pre-built `search.json` index (concatenated text per scene) to
* answer regex queries across all 9271 dialogue scenes without loading
* each conv file. Returns matching entries with the conv key and a
* snippet around the match.
*
* Depends on `./storyCore.js` for the store accessor and catalog entries.
*/
import { store, entries } from "./storyCore.js";
// ---------------------------------------------------------------------------
// Search caches (private to this module)
// ---------------------------------------------------------------------------
let _searchIndex = null;
let _entryMap = null;
/** Reset the search caches. Called by the barrel orchestrator. */
export function clearSearchCache() {
_searchIndex = null;
_entryMap = null;
}
function searchIndex() {
if (_searchIndex !== null)
return _searchIndex;
try {
const raw = store().readJson("search.json");
_searchIndex = raw.entries ?? [];
}
catch {
// search.json is optional — if missing, search returns empty.
_searchIndex = [];
}
return _searchIndex;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Full-text search across all dialogue scenes.
*
* Uses the pre-built search.json index (concatenated text per scene).
* Returns matching entries with the conv key and a snippet around the
* match.
*/
export function searchStories(pattern, maxResults = 30) {
let re;
try {
re = new RegExp(pattern, "i");
}
catch {
const literal = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
re = new RegExp(literal, "i");
}
const idx = searchIndex();
// Build key→entry lookup lazily and cache it (9271 entries — pointless
// to rebuild on every search call since the catalog only changes on
// sync, which clears this cache via clearStoryCaches).
if (_entryMap === null) {
_entryMap = new Map();
for (const e of entries())
_entryMap.set(e.key, e);
}
const entryMap = _entryMap;
const out = [];
for (const se of idx) {
if (re.test(se.x)) {
const entry = entryMap.get(se.k);
if (entry) {
// Extract a snippet around the first match.
const matchIdx = se.x.search(re);
const start = Math.max(0, matchIdx - 30);
const end = Math.min(se.x.length, matchIdx + 80);
const snippet = (start > 0 ? "..." : "") +
se.x.slice(start, end).replace(/\s+/g, " ").trim() +
(end < se.x.length ? "..." : "");
out.push({ key: se.k, snippet, entry });
if (out.length >= maxResults)
break;
}
}
}
return out;
}
//# sourceMappingURL=storySearch.js.map