prts-mcp-ts 0.1.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 ADDED
@@ -0,0 +1,76 @@
1
+ # PRTS MCP Server — TypeScript 实现
2
+
3
+ 明日方舟同人创作辅助 MCP Server,TypeScript 版本。通过 **Streamable HTTP 传输**(单端点 `/mcp`)对外提供服务,适合部署在个人服务器或云环境,供他人通过 HTTP 接入。
4
+
5
+ 提供工具集:`search_prts` / `read_prts_page` / `get_operator_archives` / `get_operator_voicelines`
6
+
7
+ ---
8
+
9
+ ## 快速开始(Docker)
10
+
11
+ ```bash
12
+ # 从仓库根目录构建(需先预置数据,详见下方)
13
+ docker build -f ts/Dockerfile -t prts-mcp-ts .
14
+
15
+ # 运行(named volume 持久化游戏数据,推荐)
16
+ docker run -d -p 3000:3000 -v prts-mcp-ts-data:/data/gamedata prts-mcp-ts
17
+ ```
18
+
19
+ 服务启动后 MCP 端点为 `http://<host>:3000/mcp`,健康检查端点为 `http://<host>:3000/health`。
20
+
21
+ ### 接入 MCP 客户端
22
+
23
+ 在客户端配置中选择 **Streamable HTTP** 传输类型,端点填写:
24
+
25
+ ```
26
+ http://localhost:3000/mcp
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 快速试用(npx)
32
+
33
+ 无需克隆仓库,直接运行:
34
+
35
+ ```bash
36
+ npx prts-mcp-ts
37
+ ```
38
+
39
+ 服务启动后 MCP 端点为 `http://localhost:3000/mcp`。
40
+
41
+ ## 本地开发
42
+
43
+ ```bash
44
+ cd ts
45
+ npm install
46
+ npm run dev # tsx 直接运行,支持热重载
47
+ npm run build # 编译到 dist/
48
+ npm start # 运行编译后的版本
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 数据机制
54
+
55
+ 服务器启动时自动从 [ArknightsGameData](https://github.com/Kengxxiao/ArknightsGameData) 同步干员数据,结果缓存至挂载的 volume(`/data/gamedata`)。镜像内置 bundled 数据作为网络不可用时的离线保底。
56
+
57
+ ---
58
+
59
+ ## 环境变量
60
+
61
+ | 变量 | 默认值 | 说明 |
62
+ |------|--------|------|
63
+ | `PORT` | `3000` | 监听端口 |
64
+ | `HOST` | `0.0.0.0` | 监听地址 |
65
+ | `GAMEDATA_PATH` | 未设置 | 设置后指向自定义数据目录,**auto-sync 被禁用** |
66
+ | `GITHUB_TOKEN` | 空 | 用于提高 GitHub API 限额,降低限流风险 |
67
+
68
+ ---
69
+
70
+ ## 预置 bundled 数据(本地构建推荐)
71
+
72
+ ```bash
73
+ pip install -e python/
74
+ python python/scripts/fetch_gamedata.py
75
+ docker build -f ts/Dockerfile -t prts-mcp-ts .
76
+ ```
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PRTS Wiki API client with rate limiting.
3
+ * Mirrors python/src/prts_mcp/api/prts_wiki.py.
4
+ */
5
+ export interface SearchResult {
6
+ title: string;
7
+ snippet: string;
8
+ }
9
+ /**
10
+ * Search PRTS wiki and return a list of { title, snippet } objects.
11
+ * Mirrors prts_wiki.search_prts().
12
+ */
13
+ export declare function searchPrts(query: string, limit?: number): Promise<SearchResult[]>;
14
+ /**
15
+ * Fetch plain-text extract for a PRTS wiki page.
16
+ * Mirrors prts_wiki.read_page().
17
+ */
18
+ export declare function readPage(title: string): Promise<string>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * PRTS Wiki API client with rate limiting.
3
+ * Mirrors python/src/prts_mcp/api/prts_wiki.py.
4
+ */
5
+ import { PRTS_API_ENDPOINT, RATE_LIMIT_INTERVAL, USER_AGENT, } from "../config.js";
6
+ import { stripWikitext } from "../utils/sanitizer.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Rate limiter
9
+ // ---------------------------------------------------------------------------
10
+ // Tracks the earliest time the next request is allowed to fire.
11
+ // Updated immediately (before any await) so concurrent callers each
12
+ // reserve a distinct slot — avoiding the check-then-act race.
13
+ let nextAllowedTime = 0;
14
+ async function rateLimit() {
15
+ const now = Date.now();
16
+ const intervalMs = RATE_LIMIT_INTERVAL * 1000;
17
+ // Reserve a slot: advance nextAllowedTime by one interval.
18
+ const slot = Math.max(now, nextAllowedTime);
19
+ nextAllowedTime = slot + intervalMs;
20
+ const waitMs = slot - now;
21
+ if (waitMs > 0) {
22
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
23
+ }
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // API helpers
27
+ // ---------------------------------------------------------------------------
28
+ const DEFAULT_HEADERS = { "User-Agent": USER_AGENT };
29
+ async function prtsGet(params) {
30
+ await rateLimit();
31
+ const url = new URL(PRTS_API_ENDPOINT);
32
+ for (const [k, v] of Object.entries(params)) {
33
+ url.searchParams.set(k, String(v));
34
+ }
35
+ const res = await fetch(url.toString(), {
36
+ headers: DEFAULT_HEADERS,
37
+ signal: AbortSignal.timeout(15_000),
38
+ });
39
+ if (!res.ok)
40
+ throw new Error(`PRTS API error: HTTP ${res.status}`);
41
+ return res.json();
42
+ }
43
+ /**
44
+ * Search PRTS wiki and return a list of { title, snippet } objects.
45
+ * Mirrors prts_wiki.search_prts().
46
+ */
47
+ export async function searchPrts(query, limit = 5) {
48
+ const data = (await prtsGet({
49
+ action: "query",
50
+ list: "search",
51
+ srsearch: query,
52
+ srlimit: limit,
53
+ format: "json",
54
+ }));
55
+ return (data.query?.search ?? []).map((item) => ({
56
+ title: item.title,
57
+ snippet: stripWikitext(item.snippet ?? ""),
58
+ }));
59
+ }
60
+ /**
61
+ * Fetch plain-text extract for a PRTS wiki page.
62
+ * Mirrors prts_wiki.read_page().
63
+ */
64
+ export async function readPage(title) {
65
+ const data = (await prtsGet({
66
+ action: "query",
67
+ titles: title,
68
+ prop: "extracts",
69
+ explaintext: "1",
70
+ format: "json",
71
+ }));
72
+ const pages = data.query?.pages ?? {};
73
+ for (const page of Object.values(pages)) {
74
+ if (page.extract)
75
+ return stripWikitext(page.extract);
76
+ }
77
+ return `页面 '${title}' 未找到或内容为空。`;
78
+ }
79
+ //# sourceMappingURL=prtsWiki.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prtsWiki.js","sourceRoot":"","sources":["../../src/api/prtsWiki.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Runtime configuration for PRTS-MCP (TypeScript).
3
+ *
4
+ * Path design mirrors python/src/prts_mcp/config.py:
5
+ *
6
+ * DEFAULT_GAMEDATA_PATH — where auto-sync writes data at runtime.
7
+ * Priority (highest → lowest):
8
+ * 1. GAMEDATA_PATH env var — user-supplied; auto-sync is DISABLED.
9
+ * 2. /data/gamedata — fixed volume mount-point inside Docker
10
+ * (detected via PRTS_MCP_ROOT == "/app").
11
+ * 3. Per-user data dir — ~/.local/share/prts-mcp/ (Linux/macOS)
12
+ * or %LOCALAPPDATA%\prts-mcp\ (Windows).
13
+ *
14
+ * BUNDLED_GAMEDATA_PATH — read-only fallback baked into the Docker image.
15
+ * Always /app/data/gamedata inside the container.
16
+ */
17
+ export declare const PRTS_API_ENDPOINT = "https://prts.wiki/api.php";
18
+ export declare const USER_AGENT = "PRTS-MCP-Bot/0.1 (Arknights fan-creation helper)";
19
+ /** Minimum seconds between PRTS API requests. */
20
+ export declare const RATE_LIMIT_INTERVAL = 1.5;
21
+ /** Bundled data baked into the image at build time. */
22
+ export declare const BUNDLED_GAMEDATA_PATH = "/app/data/gamedata";
23
+ export declare const DEFAULT_GAMEDATA_PATH: string;
24
+ export interface Config {
25
+ /** Sync write target (volume or user dir). */
26
+ gamedataPath: string;
27
+ /** True when GAMEDATA_PATH was explicitly set by the user. */
28
+ isCustomGamedata: boolean;
29
+ /** Primary excel path (under gamedataPath). */
30
+ excelPath: string;
31
+ /** Bundled excel path (read-only fallback, only exists inside Docker). */
32
+ bundledExcelPath: string;
33
+ /**
34
+ * The path operator.ts should actually read from.
35
+ * Prefers the sync path when complete; falls back to bundled data; null
36
+ * when neither location has data.
37
+ */
38
+ effectiveExcelPath: string | null;
39
+ }
40
+ export declare function hasOperatorData(cfg: Config): boolean;
41
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Runtime configuration for PRTS-MCP (TypeScript).
3
+ *
4
+ * Path design mirrors python/src/prts_mcp/config.py:
5
+ *
6
+ * DEFAULT_GAMEDATA_PATH — where auto-sync writes data at runtime.
7
+ * Priority (highest → lowest):
8
+ * 1. GAMEDATA_PATH env var — user-supplied; auto-sync is DISABLED.
9
+ * 2. /data/gamedata — fixed volume mount-point inside Docker
10
+ * (detected via PRTS_MCP_ROOT == "/app").
11
+ * 3. Per-user data dir — ~/.local/share/prts-mcp/ (Linux/macOS)
12
+ * or %LOCALAPPDATA%\prts-mcp\ (Windows).
13
+ *
14
+ * BUNDLED_GAMEDATA_PATH — read-only fallback baked into the Docker image.
15
+ * Always /app/data/gamedata inside the container.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ // ---------------------------------------------------------------------------
21
+ // Constants
22
+ // ---------------------------------------------------------------------------
23
+ export const PRTS_API_ENDPOINT = "https://prts.wiki/api.php";
24
+ export const USER_AGENT = "PRTS-MCP-Bot/0.1 (Arknights fan-creation helper)";
25
+ /** Minimum seconds between PRTS API requests. */
26
+ export const RATE_LIMIT_INTERVAL = 1.5;
27
+ const REQUIRED_OPERATOR_FILES = [
28
+ "character_table.json",
29
+ "handbook_info_table.json",
30
+ "charword_table.json",
31
+ ];
32
+ /** Fixed volume mount-point inside Docker. */
33
+ const DOCKER_VOLUME_PATH = "/data/gamedata";
34
+ /** Bundled data baked into the image at build time. */
35
+ export const BUNDLED_GAMEDATA_PATH = "/app/data/gamedata";
36
+ // ---------------------------------------------------------------------------
37
+ // Path resolution
38
+ // ---------------------------------------------------------------------------
39
+ function resolveDefaultGamedataPath() {
40
+ // Inside Docker (PRTS_MCP_ROOT == "/app") use the fixed volume path.
41
+ if (process.env["PRTS_MCP_ROOT"] === "/app")
42
+ return DOCKER_VOLUME_PATH;
43
+ // Outside Docker: per-user data directory.
44
+ if (process.platform === "win32") {
45
+ const base = process.env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local");
46
+ return join(base, "prts-mcp", "gamedata");
47
+ }
48
+ const base = process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share");
49
+ return join(base, "prts-mcp", "gamedata");
50
+ }
51
+ export const DEFAULT_GAMEDATA_PATH = resolveDefaultGamedataPath();
52
+ function excelPath(gamedataRoot) {
53
+ return join(gamedataRoot, "zh_CN", "gamedata", "excel");
54
+ }
55
+ function filesComplete(excel) {
56
+ return REQUIRED_OPERATOR_FILES.every((f) => existsSync(join(excel, f)));
57
+ }
58
+ export function hasOperatorData(cfg) {
59
+ return cfg.effectiveExcelPath !== null;
60
+ }
61
+ export function loadConfig() {
62
+ const isCustomGamedata = "GAMEDATA_PATH" in process.env;
63
+ const gamedataPath = isCustomGamedata
64
+ ? process.env["GAMEDATA_PATH"]
65
+ : DEFAULT_GAMEDATA_PATH;
66
+ const ep = excelPath(gamedataPath);
67
+ const bep = excelPath(BUNDLED_GAMEDATA_PATH);
68
+ let effectiveExcelPath = null;
69
+ if (filesComplete(ep))
70
+ effectiveExcelPath = ep;
71
+ else if (filesComplete(bep))
72
+ effectiveExcelPath = bep;
73
+ return {
74
+ gamedataPath,
75
+ isCustomGamedata,
76
+ excelPath: ep,
77
+ bundledExcelPath: bep,
78
+ effectiveExcelPath,
79
+ };
80
+ }
81
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,2BAA2B,CAAC;AAC7D,MAAM,CAAC,MAAM,UAAU,GAAG,kDAAkD,CAAC;AAC7E,iDAAiD;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC,MAAM,uBAAuB,GAAG;IAC9B,sBAAsB;IACtB,0BAA0B;IAC1B,qBAAqB;CACb,CAAC;AAEX,8CAA8C;AAC9C,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAE5C,uDAAuD;AACvD,MAAM,CAAC,MAAM,qBAAqB,GAAG,oBAAoB,CAAC;AAE1D,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,SAAS,0BAA0B;IACjC,qEAAqE;IACrE,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,MAAM;QAAE,OAAO,kBAAkB,CAAC;IAEvE,2CAA2C;IAC3C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,IAAI,GACR,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QACrE,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,IAAI,GACR,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,0BAA0B,EAAE,CAAC;AAElE,SAAS,SAAS,CAAC,YAAoB;IACrC,OAAO,IAAI,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC;AAuBD,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,GAAG,CAAC,kBAAkB,KAAK,IAAI,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,gBAAgB,GAAG,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC;IACxD,MAAM,YAAY,GAAG,gBAAgB;QACnC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAE;QAC/B,CAAC,CAAC,qBAAqB,CAAC;IAE1B,MAAM,EAAE,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,SAAS,CAAC,qBAAqB,CAAC,CAAC;IAE7C,IAAI,kBAAkB,GAAkB,IAAI,CAAC;IAC7C,IAAI,aAAa,CAAC,EAAE,CAAC;QAAE,kBAAkB,GAAG,EAAE,CAAC;SAC1C,IAAI,aAAa,CAAC,GAAG,CAAC;QAAE,kBAAkB,GAAG,GAAG,CAAC;IAEtD,OAAO;QACL,YAAY;QACZ,gBAAgB;QAChB,SAAS,EAAE,EAAE;QACb,gBAAgB,EAAE,GAAG;QACrB,kBAAkB;KACnB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Operator data reader — loads and formats game data from local JSON files.
3
+ * Mirrors python/src/prts_mcp/data/operator.py.
4
+ *
5
+ * JSON files are large (character_table.json ~4 MB) so they are loaded
6
+ * lazily on first call and cached in module-level variables.
7
+ */
8
+ /** Return formatted archive text for an operator by Chinese name. */
9
+ export declare function getOperatorArchives(name: string): string;
10
+ /** Return formatted voice-line text for an operator by Chinese name. */
11
+ export declare function getOperatorVoicelines(name: string): string;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Operator data reader — loads and formats game data from local JSON files.
3
+ * Mirrors python/src/prts_mcp/data/operator.py.
4
+ *
5
+ * JSON files are large (character_table.json ~4 MB) so they are loaded
6
+ * lazily on first call and cached in module-level variables.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { loadConfig, hasOperatorData } from "../config.js";
11
+ let _characterTable = null;
12
+ let _handbookTable = null;
13
+ let _charwordTable = null;
14
+ let _nameToId = null;
15
+ // ---------------------------------------------------------------------------
16
+ // Internal helpers
17
+ // ---------------------------------------------------------------------------
18
+ function missingDataMessage() {
19
+ const cfg = loadConfig();
20
+ return ("干员数据暂不可用。" +
21
+ "容器启动时的 auto-sync 可能仍在进行中,请稍后重试;" +
22
+ "若持续出现此提示,请检查网络连接或提供 GITHUB_TOKEN 以降低限速风险。" +
23
+ `(当前同步目标路径:${cfg.excelPath})`);
24
+ }
25
+ function loadJson(filePath) {
26
+ if (!existsSync(filePath)) {
27
+ throw new Error(`干员数据文件不存在:${filePath}。` +
28
+ "数据目录可能为空,或挂载路径有误(GAMEDATA_PATH 应指向 ArknightsGameData 仓库根目录)。");
29
+ }
30
+ return JSON.parse(readFileSync(filePath, "utf-8"));
31
+ }
32
+ function excelFile(name) {
33
+ const ep = loadConfig().effectiveExcelPath;
34
+ if (ep === null)
35
+ throw new Error("effectiveExcelPath is null");
36
+ return join(ep, name);
37
+ }
38
+ function getCharacterTable() {
39
+ if (_characterTable === null) {
40
+ _characterTable = loadJson(excelFile("character_table.json"));
41
+ }
42
+ if (_characterTable === undefined)
43
+ throw new Error("character_table failed");
44
+ return _characterTable;
45
+ }
46
+ function getHandbookTable() {
47
+ if (_handbookTable === null) {
48
+ _handbookTable = loadJson(excelFile("handbook_info_table.json"));
49
+ }
50
+ if (_handbookTable === undefined)
51
+ throw new Error("handbook_table failed");
52
+ return _handbookTable;
53
+ }
54
+ function getCharwordTable() {
55
+ if (_charwordTable === null) {
56
+ _charwordTable = loadJson(excelFile("charword_table.json"));
57
+ }
58
+ if (_charwordTable === undefined)
59
+ throw new Error("charword_table failed");
60
+ return _charwordTable;
61
+ }
62
+ function resolveCharId(name) {
63
+ if (_nameToId === null) {
64
+ const ct = getCharacterTable();
65
+ _nameToId = new Map(Object.entries(ct)
66
+ .filter(([, info]) => info.name)
67
+ .map(([cid, info]) => [info.name, cid]));
68
+ }
69
+ return _nameToId.get(name) ?? null;
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Public API
73
+ // ---------------------------------------------------------------------------
74
+ /** Return formatted archive text for an operator by Chinese name. */
75
+ export function getOperatorArchives(name) {
76
+ const cfg = loadConfig();
77
+ if (!hasOperatorData(cfg))
78
+ return missingDataMessage();
79
+ let charId;
80
+ try {
81
+ charId = resolveCharId(name);
82
+ }
83
+ catch (err) {
84
+ return err instanceof Error ? err.message : String(err);
85
+ }
86
+ if (charId === null) {
87
+ return `未找到干员 '${name}'。请使用游戏内中文名称(如'阿米娅')。`;
88
+ }
89
+ let handbook;
90
+ try {
91
+ handbook = getHandbookTable();
92
+ }
93
+ catch (err) {
94
+ return err instanceof Error ? err.message : String(err);
95
+ }
96
+ const entry = handbook.handbookDict?.[charId];
97
+ if (!entry)
98
+ return `干员 '${name}' 暂无档案数据。`;
99
+ const sections = [];
100
+ for (const story of entry.storyTextAudio ?? []) {
101
+ const title = story.storyTitle ?? "";
102
+ const texts = (story.stories ?? [])
103
+ .map((s) => s.storyText ?? "")
104
+ .filter(Boolean);
105
+ if (texts.length > 0) {
106
+ sections.push(`### ${title}\n` + texts.join("\n"));
107
+ }
108
+ }
109
+ if (sections.length === 0)
110
+ return `干员 '${name}' 档案内容为空。`;
111
+ return `# ${name} - 干员档案\n\n` + sections.join("\n\n");
112
+ }
113
+ /** Return formatted voice-line text for an operator by Chinese name. */
114
+ export function getOperatorVoicelines(name) {
115
+ const cfg = loadConfig();
116
+ if (!hasOperatorData(cfg))
117
+ return missingDataMessage();
118
+ let charId;
119
+ try {
120
+ charId = resolveCharId(name);
121
+ }
122
+ catch (err) {
123
+ return err instanceof Error ? err.message : String(err);
124
+ }
125
+ if (charId === null) {
126
+ return `未找到干员 '${name}'。请使用游戏内中文名称(如'阿米娅')。`;
127
+ }
128
+ let charwords;
129
+ try {
130
+ charwords = getCharwordTable();
131
+ }
132
+ catch (err) {
133
+ return err instanceof Error ? err.message : String(err);
134
+ }
135
+ const lines = [];
136
+ for (const entry of Object.values(charwords.charWords ?? {})) {
137
+ if (entry.charId === charId && entry.voiceText) {
138
+ const title = entry.voiceTitle ?? "未知";
139
+ lines.push(`**${title}**: ${entry.voiceText}`);
140
+ }
141
+ }
142
+ if (lines.length === 0)
143
+ return `干员 '${name}' 暂无语音数据。`;
144
+ return `# ${name} - 语音记录\n\n` + lines.join("\n");
145
+ }
146
+ //# sourceMappingURL=operator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator.js","sourceRoot":"","sources":["../../src/data/operator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAa3D,IAAI,eAAe,GAA+C,IAAI,CAAC;AACvE,IAAI,cAAc,GAA8B,IAAI,CAAC;AACrD,IAAI,cAAc,GAA8B,IAAI,CAAC;AACrD,IAAI,SAAS,GAA+B,IAAI,CAAC;AAiCjD,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,SAAS,kBAAkB;IACzB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,OAAO,CACL,WAAW;QACX,iCAAiC;QACjC,2CAA2C;QAC3C,aAAa,GAAG,CAAC,SAAS,GAAG,CAC9B,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAI,QAAgB;IACnC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,aAAa,QAAQ,GAAG;YACtB,8DAA8D,CACjE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAM,CAAC;AAC1D,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,kBAAkB,CAAC;IAC3C,IAAI,EAAE,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,iBAAiB;IACxB,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,eAAe,GAAG,QAAQ,CACxB,SAAS,CAAC,sBAAsB,CAAC,CAClC,CAAC;IACJ,CAAC;IACD,IAAI,eAAe,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC7E,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,SAAS,gBAAgB;IACvB,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,cAAc,GAAG,QAAQ,CACvB,SAAS,CAAC,0BAA0B,CAAC,CACtC,CAAC;IACJ,CAAC;IACD,IAAI,cAAc,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3E,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,SAAS,gBAAgB;IACvB,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,cAAc,GAAG,QAAQ,CAAgB,SAAS,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,cAAc,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3E,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAAC;QAC/B,SAAS,GAAG,IAAI,GAAG,CACjB,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;aACf,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;aAC/B,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAK,EAAE,GAAG,CAAC,CAAC,CAC3C,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AACrC,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,qEAAqE;AACrE,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC;QAAE,OAAO,kBAAkB,EAAE,CAAC;IAEvD,IAAI,MAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,UAAU,IAAI,uBAAuB,CAAC;IAC/C,CAAC;IAED,IAAI,QAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,QAAQ,GAAG,gBAAgB,EAAE,CAAC;IAChC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,IAAI,WAAW,CAAC;IAE1C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;aAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC;aAC7B,MAAM,CAAC,OAAO,CAAC,CAAC;QACnB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC,OAAO,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,IAAI,WAAW,CAAC;IACzD,OAAO,KAAK,IAAI,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACxD,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC;QAAE,OAAO,kBAAkB,EAAE,CAAC;IAEvD,IAAI,MAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,UAAU,IAAI,uBAAuB,CAAC;IAC/C,CAAC;IAED,IAAI,SAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,SAAS,GAAG,gBAAgB,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC;QAC7D,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC;YACvC,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,OAAO,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,IAAI,WAAW,CAAC;IACtD,OAAO,KAAK,IAAI,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GitHub-backed data sync for PRTS-MCP (TypeScript implementation).
3
+ *
4
+ * Mirrors the behaviour of python/src/prts_mcp/data/sync.py exactly:
5
+ * - Checks the upstream commit SHA via the GitHub Commits API.
6
+ * - Downloads required game data files only when the upstream has changed.
7
+ * - Falls back gracefully to cached / bundled data when the network is
8
+ * unavailable.
9
+ * - Skips the upstream check entirely when cached data is fresher than the TTL.
10
+ */
11
+ export declare const GAMEDATA_FILES: readonly string[];
12
+ /** Describes an upstream GitHub repository and the files required from it. */
13
+ export interface RepoSpec {
14
+ owner: string;
15
+ repo: string;
16
+ branch: string;
17
+ files: readonly string[];
18
+ /** Absolute path to the local directory where files are written. */
19
+ localRoot: string;
20
+ }
21
+ export type SyncStatus = "updated" | "up_to_date" | "offline_fallback" | "no_data";
22
+ export interface SyncResult {
23
+ spec: RepoSpec;
24
+ status: SyncStatus;
25
+ commitSha: string | null;
26
+ error: string | null;
27
+ }
28
+ /**
29
+ * Return the latest commit SHA from GitHub, or `null` on any failure.
30
+ */
31
+ export declare function checkUpstreamSha(spec: RepoSpec, timeoutMs?: number): Promise<string | null>;
32
+ /**
33
+ * Download all required files atomically, then write cache metadata.
34
+ *
35
+ * Uses a write-to-tmp-then-rename pattern so partially downloaded files
36
+ * never appear to the data loader as complete.
37
+ */
38
+ export declare function downloadFiles(spec: RepoSpec, sha: string, timeoutMs?: number): Promise<void>;
39
+ /**
40
+ * Check upstream and download files if needed.
41
+ *
42
+ * Decision tree (mirrors Python sync.py):
43
+ * 1. Cache is fresh AND files exist → up_to_date (skip API call)
44
+ * 2. Call GitHub commits API:
45
+ * a. Network failure:
46
+ * files present → offline_fallback
47
+ * no files → no_data
48
+ * b. SHA matches cache AND files present → up_to_date (refresh fetchedAt)
49
+ * c. Otherwise → downloadFiles()
50
+ * success → updated
51
+ * failure → files present → offline_fallback / no files → no_data
52
+ */
53
+ export declare function syncRepo(spec: RepoSpec): Promise<SyncResult>;
54
+ /** Sync each RepoSpec sequentially and return all results. */
55
+ export declare function syncAll(specs: RepoSpec[]): Promise<SyncResult[]>;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * GitHub-backed data sync for PRTS-MCP (TypeScript implementation).
3
+ *
4
+ * Mirrors the behaviour of python/src/prts_mcp/data/sync.py exactly:
5
+ * - Checks the upstream commit SHA via the GitHub Commits API.
6
+ * - Downloads required game data files only when the upstream has changed.
7
+ * - Falls back gracefully to cached / bundled data when the network is
8
+ * unavailable.
9
+ * - Skips the upstream check entirely when cached data is fresher than the TTL.
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { mkdir, readFile, rename, unlink, writeFile, } from "node:fs/promises";
13
+ import { dirname, join } from "node:path";
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ export const GAMEDATA_FILES = [
18
+ "zh_CN/gamedata/excel/character_table.json",
19
+ "zh_CN/gamedata/excel/handbook_info_table.json",
20
+ "zh_CN/gamedata/excel/charword_table.json",
21
+ ];
22
+ const GITHUB_UA = "PRTS-MCP-Bot/0.1 (Arknights fan-creation helper)";
23
+ /** Skip the upstream SHA check if cached data is fresher than this (seconds). */
24
+ const CACHE_TTL_SECONDS = 3600;
25
+ // ---------------------------------------------------------------------------
26
+ // Internal helpers
27
+ // ---------------------------------------------------------------------------
28
+ function githubHeaders() {
29
+ const headers = { "User-Agent": GITHUB_UA };
30
+ const token = process.env["GITHUB_TOKEN"];
31
+ if (token)
32
+ headers["Authorization"] = `Bearer ${token}`;
33
+ return headers;
34
+ }
35
+ function cacheMetaPath(spec) {
36
+ return join(spec.localRoot, "cache_meta.json");
37
+ }
38
+ async function loadCacheMeta(spec) {
39
+ try {
40
+ const text = await readFile(cacheMetaPath(spec), "utf-8");
41
+ return JSON.parse(text);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ async function saveCacheMeta(spec, meta) {
48
+ const p = cacheMetaPath(spec);
49
+ await mkdir(dirname(p), { recursive: true });
50
+ await writeFile(p, JSON.stringify(meta, null, 2), "utf-8");
51
+ }
52
+ function cacheIsFresh(cache) {
53
+ try {
54
+ const ageMs = Date.now() - new Date(cache.fetchedAt).getTime();
55
+ return ageMs < CACHE_TTL_SECONDS * 1000;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ function filesPresent(spec) {
62
+ return spec.files.every((f) => existsSync(join(spec.localRoot, f)));
63
+ }
64
+ function errorMessage(err) {
65
+ return err instanceof Error ? err.message : String(err);
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Public API
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * Return the latest commit SHA from GitHub, or `null` on any failure.
72
+ */
73
+ export async function checkUpstreamSha(spec, timeoutMs = 10_000) {
74
+ const url = `https://api.github.com/repos/${spec.owner}/${spec.repo}/commits/${spec.branch}`;
75
+ try {
76
+ const res = await fetch(url, {
77
+ headers: githubHeaders(),
78
+ signal: AbortSignal.timeout(timeoutMs),
79
+ });
80
+ if (!res.ok)
81
+ throw new Error(`HTTP ${res.status}`);
82
+ const data = (await res.json());
83
+ return data.sha;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * Download all required files atomically, then write cache metadata.
91
+ *
92
+ * Uses a write-to-tmp-then-rename pattern so partially downloaded files
93
+ * never appear to the data loader as complete.
94
+ */
95
+ export async function downloadFiles(spec, sha, timeoutMs = 60_000) {
96
+ const tmpPairs = [];
97
+ try {
98
+ for (const filePath of spec.files) {
99
+ const url = `https://raw.githubusercontent.com/${spec.owner}/${spec.repo}/${spec.branch}/${filePath}`;
100
+ const res = await fetch(url, {
101
+ headers: githubHeaders(),
102
+ signal: AbortSignal.timeout(timeoutMs),
103
+ });
104
+ if (!res.ok)
105
+ throw new Error(`HTTP ${res.status} for ${filePath}`);
106
+ const dest = join(spec.localRoot, filePath);
107
+ const tmp = dest + ".tmp";
108
+ await mkdir(dirname(dest), { recursive: true });
109
+ // Write raw bytes to preserve exact encoding from upstream.
110
+ await writeFile(tmp, Buffer.from(await res.arrayBuffer()));
111
+ tmpPairs.push([tmp, dest]);
112
+ }
113
+ // All downloads succeeded — atomically rename into place.
114
+ for (const [tmp, dest] of tmpPairs) {
115
+ await rename(tmp, dest);
116
+ }
117
+ tmpPairs.length = 0;
118
+ await saveCacheMeta(spec, {
119
+ repo: `${spec.owner}/${spec.repo}`,
120
+ branch: spec.branch,
121
+ commitSha: sha,
122
+ fetchedAt: new Date().toISOString(),
123
+ files: [...spec.files],
124
+ });
125
+ }
126
+ catch (err) {
127
+ // Clean up any temp files on failure.
128
+ for (const [tmp] of tmpPairs) {
129
+ try {
130
+ await unlink(tmp);
131
+ }
132
+ catch {
133
+ // best-effort cleanup
134
+ }
135
+ }
136
+ throw err;
137
+ }
138
+ }
139
+ /**
140
+ * Check upstream and download files if needed.
141
+ *
142
+ * Decision tree (mirrors Python sync.py):
143
+ * 1. Cache is fresh AND files exist → up_to_date (skip API call)
144
+ * 2. Call GitHub commits API:
145
+ * a. Network failure:
146
+ * files present → offline_fallback
147
+ * no files → no_data
148
+ * b. SHA matches cache AND files present → up_to_date (refresh fetchedAt)
149
+ * c. Otherwise → downloadFiles()
150
+ * success → updated
151
+ * failure → files present → offline_fallback / no files → no_data
152
+ */
153
+ export async function syncRepo(spec) {
154
+ const cache = await loadCacheMeta(spec);
155
+ const filesOk = filesPresent(spec);
156
+ // Fast path: cache is fresh, no need to hit the API.
157
+ if (cache !== null && filesOk && cacheIsFresh(cache)) {
158
+ return {
159
+ spec,
160
+ status: "up_to_date",
161
+ commitSha: cache.commitSha,
162
+ error: null,
163
+ };
164
+ }
165
+ const upstreamSha = await checkUpstreamSha(spec);
166
+ if (upstreamSha === null) {
167
+ return filesOk
168
+ ? {
169
+ spec,
170
+ status: "offline_fallback",
171
+ commitSha: cache?.commitSha ?? null,
172
+ error: "Network unavailable",
173
+ }
174
+ : {
175
+ spec,
176
+ status: "no_data",
177
+ commitSha: null,
178
+ error: "Network unavailable and no cached data",
179
+ };
180
+ }
181
+ if (cache !== null && cache.commitSha === upstreamSha && filesOk) {
182
+ // Refresh fetchedAt so the TTL resets from now.
183
+ await saveCacheMeta(spec, { ...cache, fetchedAt: new Date().toISOString() });
184
+ return {
185
+ spec,
186
+ status: "up_to_date",
187
+ commitSha: upstreamSha,
188
+ error: null,
189
+ };
190
+ }
191
+ try {
192
+ await downloadFiles(spec, upstreamSha);
193
+ return { spec, status: "updated", commitSha: upstreamSha, error: null };
194
+ }
195
+ catch (err) {
196
+ const error = errorMessage(err);
197
+ return filesOk
198
+ ? {
199
+ spec,
200
+ status: "offline_fallback",
201
+ commitSha: cache?.commitSha ?? null,
202
+ error,
203
+ }
204
+ : { spec, status: "no_data", commitSha: null, error };
205
+ }
206
+ }
207
+ /** Sync each RepoSpec sequentially and return all results. */
208
+ export async function syncAll(specs) {
209
+ const results = [];
210
+ for (const spec of specs) {
211
+ results.push(await syncRepo(spec));
212
+ }
213
+ return results;
214
+ }
215
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/data/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EACL,KAAK,EACL,QAAQ,EACR,MAAM,EACN,MAAM,EACN,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,2CAA2C;IAC3C,+CAA+C;IAC/C,0CAA0C;CAC3C,CAAC;AAEF,MAAM,SAAS,GAAG,kDAAkD,CAAC;AAErE,iFAAiF;AACjF,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAuC/B,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,SAAS,aAAa;IACpB,MAAM,OAAO,GAA2B,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IACpE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1C,IAAI,KAAK;QAAE,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;IACxD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,aAAa,CAAC,IAAc;IACnC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AACjD,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAc;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAc,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,IAAc,EACd,IAAe;IAEf,MAAM,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,MAAM,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,YAAY,CAAC,KAAgB;IACpC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;QAC/D,OAAO,KAAK,GAAG,iBAAiB,GAAG,IAAI,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,IAAc;IAClC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,YAAY,CAAC,GAAY;IAChC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAc,EACd,SAAS,GAAG,MAAM;IAElB,MAAM,GAAG,GAAG,gCAAgC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC;IAC7F,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,OAAO,EAAE,aAAa,EAAE;YACxB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC;QACnD,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAc,EACd,GAAW,EACX,SAAS,GAAG,MAAM;IAElB,MAAM,QAAQ,GAAuC,EAAE,CAAC;IACxD,IAAI,CAAC;QACH,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAClC,MAAM,GAAG,GAAG,qCAAqC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;YACtG,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,OAAO,EAAE,aAAa,EAAE;gBACxB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;aACvC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,QAAQ,QAAQ,EAAE,CAAC,CAAC;YAEnE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YAC5C,MAAM,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC;YAC1B,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,4DAA4D;YAC5D,MAAM,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;YAC3D,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAC7B,CAAC;QAED,0DAA0D;QAC1D,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;YACnC,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAEpB,MAAM,aAAa,CAAC,IAAI,EAAE;YACxB,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE;YAClC,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;SACvB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,sCAAsC;QACtC,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAc;IAC3C,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEnC,qDAAqD;IACrD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QACrD,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,KAAK,EAAE,IAAI;SACZ,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAEjD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,OAAO,OAAO;YACZ,CAAC,CAAC;gBACE,IAAI;gBACJ,MAAM,EAAE,kBAAkB;gBAC1B,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,IAAI;gBACnC,KAAK,EAAE,qBAAqB;aAC7B;YACH,CAAC,CAAC;gBACE,IAAI;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI;gBACf,KAAK,EAAE,wCAAwC;aAChD,CAAC;IACR,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,SAAS,KAAK,WAAW,IAAI,OAAO,EAAE,CAAC;QACjE,gDAAgD;QAChD,MAAM,aAAa,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC7E,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,WAAW;YACtB,KAAK,EAAE,IAAI;SACZ,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,aAAa,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACvC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC1E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,OAAO;YACZ,CAAC,CAAC;gBACE,IAAI;gBACJ,MAAM,EAAE,kBAAkB;gBAC1B,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,IAAI;gBACnC,KAAK;aACN;YACH,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC1D,CAAC;AACH,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAiB;IAC7C,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import express from "express";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import { z } from "zod";
7
+ import { loadConfig } from "./config.js";
8
+ import { searchPrts, readPage } from "./api/prtsWiki.js";
9
+ import { getOperatorArchives, getOperatorVoicelines } from "./data/operator.js";
10
+ import { syncAll, GAMEDATA_FILES } from "./data/sync.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Logging
13
+ // ---------------------------------------------------------------------------
14
+ function log(level, msg) {
15
+ const ts = new Date().toISOString();
16
+ process.stderr.write(`${ts} ${level} prts_mcp.server: ${msg}\n`);
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // MCP Server factory — one instance per session
20
+ // ---------------------------------------------------------------------------
21
+ function createMcpServer() {
22
+ const server = new McpServer({
23
+ name: "PRTS_Wiki_Assistant",
24
+ version: "0.1.0",
25
+ });
26
+ server.tool("search_prts", "搜索 PRTS 明日方舟中文维基词条。返回匹配的词条标题和摘要。", { query: z.string(), limit: z.number().int().min(1).max(20).default(5) }, async ({ query, limit }) => {
27
+ const results = await searchPrts(query, limit);
28
+ if (results.length === 0) {
29
+ return { content: [{ type: "text", text: `未找到与 '${query}' 相关的词条。` }] };
30
+ }
31
+ const text = results
32
+ .map((r) => `**${r.title}**\n${r.snippet}`)
33
+ .join("\n\n---\n\n");
34
+ return { content: [{ type: "text", text }] };
35
+ });
36
+ server.tool("read_prts_page", "读取 PRTS 维基指定词条的纯文本内容。", { page_title: z.string() }, async ({ page_title }) => {
37
+ const text = await readPage(page_title);
38
+ return { content: [{ type: "text", text }] };
39
+ });
40
+ server.tool("get_operator_archives", '获取指定干员的档案资料(使用游戏内中文名,如"阿米娅")。', { operator_name: z.string() }, ({ operator_name }) => {
41
+ const text = getOperatorArchives(operator_name);
42
+ return { content: [{ type: "text", text }] };
43
+ });
44
+ server.tool("get_operator_voicelines", '获取指定干员的语音记录(使用游戏内中文名,如"阿米娅")。', { operator_name: z.string() }, ({ operator_name }) => {
45
+ const text = getOperatorVoicelines(operator_name);
46
+ return { content: [{ type: "text", text }] };
47
+ });
48
+ return server;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Startup data sync (fire-and-forget)
52
+ // ---------------------------------------------------------------------------
53
+ function runStartupSync() {
54
+ const cfg = loadConfig();
55
+ if (cfg.isCustomGamedata) {
56
+ log("INFO", `GAMEDATA_PATH is custom (${cfg.gamedataPath}); auto-sync disabled.`);
57
+ return;
58
+ }
59
+ const specs = [
60
+ {
61
+ owner: "Kengxxiao",
62
+ repo: "ArknightsGameData",
63
+ branch: "master",
64
+ files: GAMEDATA_FILES,
65
+ localRoot: cfg.gamedataPath,
66
+ },
67
+ ];
68
+ syncAll(specs)
69
+ .then((results) => {
70
+ for (const r of results) {
71
+ const sha = r.commitSha ? r.commitSha.slice(0, 8) : "unknown";
72
+ if (r.status === "updated") {
73
+ log("INFO", `Data updated from GitHub (${r.spec.repo} @ ${sha}).`);
74
+ }
75
+ else if (r.status === "up_to_date") {
76
+ log("INFO", `Data is up to date (${r.spec.repo} @ ${sha}).`);
77
+ }
78
+ else if (r.status === "offline_fallback") {
79
+ log("WARN", `Network unavailable; using cached data (${r.spec.repo} @ ${sha}). Error: ${r.error}`);
80
+ }
81
+ else {
82
+ log("ERROR", `Sync failed for ${r.spec.repo} — no data. Error: ${r.error}`);
83
+ }
84
+ }
85
+ })
86
+ .catch((err) => {
87
+ log("ERROR", `Startup sync threw unexpectedly: ${err instanceof Error ? err.message : String(err)}`);
88
+ });
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Express + StreamableHTTP
92
+ // ---------------------------------------------------------------------------
93
+ const app = express();
94
+ // Parse JSON bodies — StreamableHTTP transport accepts req.body as parsedBody.
95
+ app.use(express.json());
96
+ const transports = new Map();
97
+ app.all("/mcp", async (req, res) => {
98
+ const sessionId = req.headers["mcp-session-id"];
99
+ let transport = sessionId ? transports.get(sessionId) : undefined;
100
+ if (!transport) {
101
+ const newTransport = new StreamableHTTPServerTransport({
102
+ sessionIdGenerator: () => randomUUID(),
103
+ onsessioninitialized: (id) => {
104
+ transports.set(id, newTransport);
105
+ log("INFO", `Session ${id} initialized.`);
106
+ },
107
+ });
108
+ newTransport.onclose = () => {
109
+ if (newTransport.sessionId) {
110
+ transports.delete(newTransport.sessionId);
111
+ log("INFO", `Session ${newTransport.sessionId} closed.`);
112
+ }
113
+ };
114
+ try {
115
+ const server = createMcpServer();
116
+ await server.connect(newTransport);
117
+ }
118
+ catch (err) {
119
+ log("ERROR", `Failed to connect MCP server to transport: ${err instanceof Error ? err.message : String(err)}`);
120
+ res.status(500).json({ error: "Internal server error" });
121
+ return;
122
+ }
123
+ transport = newTransport;
124
+ }
125
+ // Pass req.body explicitly so the transport uses the already-parsed body
126
+ // rather than attempting to re-read the consumed stream.
127
+ await transport.handleRequest(req, res, req.body);
128
+ });
129
+ app.get("/health", (_req, res) => {
130
+ res.json({ status: "ok" });
131
+ });
132
+ // ---------------------------------------------------------------------------
133
+ // Entry point
134
+ // ---------------------------------------------------------------------------
135
+ const PORT = Number(process.env["PORT"] ?? 3000);
136
+ const HOST = process.env["HOST"] ?? "0.0.0.0";
137
+ runStartupSync();
138
+ app.listen(PORT, HOST, () => {
139
+ log("INFO", `PRTS MCP Server listening on ${HOST}:${PORT} (StreamableHTTP at /mcp)`);
140
+ });
141
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,cAAc,EAAiB,MAAM,gBAAgB,CAAC;AAExE,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,GAAG,CAAC,KAAgC,EAAE,GAAW;IACxD,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,KAAK,qBAAqB,GAAG,IAAI,CAAC,CAAC;AACnE,CAAC;AAED,8EAA8E;AAC9E,gDAAgD;AAChD,8EAA8E;AAE9E,SAAS,eAAe;IACtB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CACT,aAAa,EACb,kCAAkC,EAClC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EACxE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,KAAK,UAAU,EAAE,CAAC,EAAE,CAAC;QACzE,CAAC;QACD,MAAM,IAAI,GAAG,OAAO;aACjB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;aAC1C,IAAI,CAAC,aAAa,CAAC,CAAC;QACvB,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB,uBAAuB,EACvB,EAAE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAC1B,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,CAAC;QACxC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,uBAAuB,EACvB,+BAA+B,EAC/B,EAAE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAC7B,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE;QACpB,MAAM,IAAI,GAAG,mBAAmB,CAAC,aAAa,CAAC,CAAC;QAChD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,yBAAyB,EACzB,+BAA+B,EAC/B,EAAE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAC7B,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE;QACpB,MAAM,IAAI,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,sCAAsC;AACtC,8EAA8E;AAE9E,SAAS,cAAc;IACrB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACzB,GAAG,CAAC,MAAM,EAAE,4BAA4B,GAAG,CAAC,YAAY,wBAAwB,CAAC,CAAC;QAClF,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAe;QACxB;YACE,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,mBAAmB;YACzB,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE,cAAc;YACrB,SAAS,EAAE,GAAG,CAAC,YAAY;SAC5B;KACF,CAAC;IAEF,OAAO,CAAC,KAAK,CAAC;SACX,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAChB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9D,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC3B,GAAG,CAAC,MAAM,EAAE,6BAA6B,CAAC,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC,CAAC;YACrE,CAAC;iBAAM,IAAI,CAAC,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;gBACrC,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC,CAAC;YAC/D,CAAC;iBAAM,IAAI,CAAC,CAAC,MAAM,KAAK,kBAAkB,EAAE,CAAC;gBAC3C,GAAG,CAAC,MAAM,EAAE,2CAA2C,CAAC,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACrG,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,sBAAsB,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC;IACH,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,GAAG,CAAC,OAAO,EAAE,oCAAoC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvG,CAAC,CAAC,CAAC;AACP,CAAC;AAED,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,+EAA+E;AAC/E,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAyC,CAAC;AAEpE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACjC,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAC;IACtE,IAAI,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,YAAY,GAAG,IAAI,6BAA6B,CAAC;YACrD,kBAAkB,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE;YACtC,oBAAoB,EAAE,CAAC,EAAE,EAAE,EAAE;gBAC3B,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;gBACjC,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,eAAe,CAAC,CAAC;YAC5C,CAAC;SACF,CAAC,CAAC;QACH,YAAY,CAAC,OAAO,GAAG,GAAG,EAAE;YAC1B,IAAI,YAAY,CAAC,SAAS,EAAE,CAAC;gBAC3B,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;gBAC1C,GAAG,CAAC,MAAM,EAAE,WAAW,YAAY,CAAC,SAAS,UAAU,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC,CAAC;QACF,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;YACjC,MAAM,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,OAAO,EAAE,8CAA8C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC/G,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;YACzD,OAAO;QACT,CAAC;QACD,SAAS,GAAG,YAAY,CAAC;IAC3B,CAAC;IAED,yEAAyE;IACzE,yDAAyD;IACzD,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;AACjD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;AAE9C,cAAc,EAAE,CAAC;AAEjB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;IAC1B,GAAG,CAAC,MAAM,EAAE,gCAAgC,IAAI,IAAI,IAAI,2BAA2B,CAAC,CAAC;AACvF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Wikitext sanitiser — strips markup and returns plain text.
3
+ * Mirrors python/src/prts_mcp/utils/sanitizer.py exactly.
4
+ */
5
+ /** Remove common wikitext markup and return plain text. */
6
+ export declare function stripWikitext(text: string): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Wikitext sanitiser — strips markup and returns plain text.
3
+ * Mirrors python/src/prts_mcp/utils/sanitizer.py exactly.
4
+ */
5
+ /** Remove common wikitext markup and return plain text. */
6
+ export function stripWikitext(text) {
7
+ // Remove {{template}} blocks (non-greedy, single-level)
8
+ text = text.replace(/\{\{[^}]*\}\}/g, "");
9
+ // Remove [[File:...]] / [[文件:...]] image links
10
+ text = text.replace(/\[\[(File|文件|Image|图像):[^\]]*\]\]/g, "");
11
+ // Convert [[link|display]] -> display, [[link]] -> link
12
+ text = text.replace(/\[\[[^|\]]*\|([^\]]+)\]\]/g, "$1");
13
+ text = text.replace(/\[\[([^\]]+)\]\]/g, "$1");
14
+ // Remove HTML tags
15
+ text = text.replace(/<[^>]+>/g, "");
16
+ // Remove category links
17
+ text = text.replace(/\[\[(Category|分类):[^\]]*\]\]/g, "");
18
+ // Collapse multiple blank lines
19
+ text = text.replace(/\n{3,}/g, "\n\n");
20
+ return text.trim();
21
+ }
22
+ //# sourceMappingURL=sanitizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizer.js","sourceRoot":"","sources":["../../src/utils/sanitizer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,wDAAwD;IACxD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IAC1C,+CAA+C;IAC/C,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,oCAAoC,EAAE,EAAE,CAAC,CAAC;IAC9D,wDAAwD;IACxD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,4BAA4B,EAAE,IAAI,CAAC,CAAC;IACxD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;IAC/C,mBAAmB;IACnB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACpC,wBAAwB;IACxB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,+BAA+B,EAAE,EAAE,CAAC,CAAC;IACzD,gCAAgC;IAChC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "prts-mcp-ts",
3
+ "version": "0.1.0",
4
+ "description": "MCP Server for Arknights PRTS Wiki & game data (TypeScript / Streamable HTTP)",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "prts-mcp-ts": "./dist/server.js"
9
+ },
10
+ "files": [
11
+ "dist/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=22.0.0"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/server.ts",
19
+ "start": "node dist/server.js",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "tsc"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/3aKHP/prts-mcp.git",
26
+ "directory": "ts"
27
+ },
28
+ "volta": {
29
+ "node": "24.14.0"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
+ "express": "^5.2.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/express": "^5.0.6",
37
+ "@types/node": "^24.0.0",
38
+ "tsx": "^4.21.0",
39
+ "typescript": "^5.8.0"
40
+ }
41
+ }