@zonuexe/techbook-mcp 0.2.4 → 0.3.1
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/CHANGELOG.md +38 -1
- package/README.md +15 -2
- package/dist/adapters/http/fetch-client.d.ts.map +1 -1
- package/dist/adapters/http/fetch-client.js +18 -1
- package/dist/adapters/http/fetch-client.js.map +1 -1
- package/dist/adapters/openbd.d.ts.map +1 -1
- package/dist/adapters/openbd.js +18 -5
- package/dist/adapters/openbd.js.map +1 -1
- package/dist/adapters/publishers/base.d.ts +2 -1
- package/dist/adapters/publishers/base.d.ts.map +1 -1
- package/dist/adapters/publishers/base.js +8 -3
- package/dist/adapters/publishers/base.js.map +1 -1
- package/dist/adapters/publishers/cq-publishing.d.ts +3 -0
- package/dist/adapters/publishers/cq-publishing.d.ts.map +1 -0
- package/dist/adapters/publishers/cq-publishing.js +120 -0
- package/dist/adapters/publishers/cq-publishing.js.map +1 -0
- package/dist/adapters/publishers/google-books.d.ts.map +1 -1
- package/dist/adapters/publishers/google-books.js +1 -0
- package/dist/adapters/publishers/google-books.js.map +1 -1
- package/dist/adapters/publishers/isbn-publisher-codes.js +1 -1
- package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -1
- package/dist/adapters/publishers/juse-p.js +2 -2
- package/dist/adapters/publishers/juse-p.js.map +1 -1
- package/dist/adapters/publishers/leanpub.d.ts +3 -0
- package/dist/adapters/publishers/leanpub.d.ts.map +1 -0
- package/dist/adapters/publishers/leanpub.js +96 -0
- package/dist/adapters/publishers/leanpub.js.map +1 -0
- package/dist/adapters/publishers/oreilly-japan.d.ts.map +1 -1
- package/dist/adapters/publishers/oreilly-japan.js +8 -2
- package/dist/adapters/publishers/oreilly-japan.js.map +1 -1
- package/dist/adapters/publishers/peaks.d.ts.map +1 -1
- package/dist/adapters/publishers/peaks.js +3 -2
- package/dist/adapters/publishers/peaks.js.map +1 -1
- package/dist/adapters/publishers/personal-media.d.ts.map +1 -1
- package/dist/adapters/publishers/personal-media.js +3 -2
- package/dist/adapters/publishers/personal-media.js.map +1 -1
- package/dist/adapters/publishers/pragprog.d.ts +3 -0
- package/dist/adapters/publishers/pragprog.d.ts.map +1 -0
- package/dist/adapters/publishers/pragprog.js +120 -0
- package/dist/adapters/publishers/pragprog.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +6 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/techbookfest.d.ts.map +1 -1
- package/dist/adapters/publishers/techbookfest.js +2 -1
- package/dist/adapters/publishers/techbookfest.js.map +1 -1
- package/dist/application/concurrency.d.ts +16 -0
- package/dist/application/concurrency.d.ts.map +1 -0
- package/dist/application/concurrency.js +42 -0
- package/dist/application/concurrency.js.map +1 -0
- package/dist/application/get-book-by-isbn.d.ts +0 -9
- package/dist/application/get-book-by-isbn.d.ts.map +1 -1
- package/dist/application/get-book-by-isbn.js +44 -6
- package/dist/application/get-book-by-isbn.js.map +1 -1
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +3 -0
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts +16 -5
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +46 -9
- package/dist/application/search-books.js.map +1 -1
- package/dist/domain/authors.d.ts +7 -0
- package/dist/domain/authors.d.ts.map +1 -0
- package/dist/domain/authors.js +22 -0
- package/dist/domain/authors.js.map +1 -0
- package/dist/domain/book.d.ts +2 -0
- package/dist/domain/book.d.ts.map +1 -1
- package/dist/domain/isbn.d.ts +8 -0
- package/dist/domain/isbn.d.ts.map +1 -0
- package/dist/domain/isbn.js +16 -0
- package/dist/domain/isbn.js.map +1 -0
- package/dist/domain/publisher.d.ts +16 -0
- package/dist/domain/publisher.d.ts.map +1 -1
- package/dist/domain/text-match.d.ts +32 -0
- package/dist/domain/text-match.d.ts.map +1 -0
- package/dist/domain/text-match.js +84 -0
- package/dist/domain/text-match.js.map +1 -0
- package/dist/main.js +0 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +40 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +9 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/docs/design-doc.md +127 -7
- package/package.json +14 -15
- package/flake.lock +0 -61
|
@@ -2,6 +2,7 @@ import { checkRobotsTxt } from "../adapters/publishers/base.js";
|
|
|
2
2
|
import { fetchOpenBDBooks, openBDEntryToBookRecord } from "../adapters/openbd.js";
|
|
3
3
|
import { fetchCalilBook } from "../adapters/calil.js";
|
|
4
4
|
import { findAdapterIdByIsbn } from "../adapters/publishers/isbn-publisher-codes.js";
|
|
5
|
+
import { dedupeAuthors } from "../domain/authors.js";
|
|
5
6
|
/**
|
|
6
7
|
* ISBNから書籍情報を取得する。
|
|
7
8
|
*
|
|
@@ -9,17 +10,54 @@ import { findAdapterIdByIsbn } from "../adapters/publishers/isbn-publisher-codes
|
|
|
9
10
|
* 2. ストアリンクが既知アダプターと一致する場合は出版社サイトから詳細取得を試みる
|
|
10
11
|
* 3. ISBN出版者記号から対応アダプターを特定し、出版社サイトで検索して詳細取得を試みる
|
|
11
12
|
* 4. 取得できない場合は openBD データをそのまま返す
|
|
12
|
-
* 5. openBD
|
|
13
|
+
* 5. openBD にも存在しない場合:
|
|
14
|
+
* a. ISBN から詳細URLを決定的に引けるアダプター(detailUrlForIsbn)で詳細取得を試みる
|
|
15
|
+
* (O'Reilly の電子書籍専売・販売終了の旧刊救済。openBD/カーリル未収録のため)
|
|
16
|
+
* b. それも不可ならカーリルから書誌情報を取得する(廃業出版社など)
|
|
13
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* ISBN出版者記号からアダプターを特定し、detailUrlForIsbn で構成した詳細ページから直接取得する。
|
|
20
|
+
* openBD・カーリル・検索一覧いずれにも出ない旧刊(販売終了・電子書籍専売)の救済経路。
|
|
21
|
+
* 取得できなければ undefined を返す。
|
|
22
|
+
*/
|
|
23
|
+
async function fetchDetailByIsbnCode(isbn, publishers, deps) {
|
|
24
|
+
const adapterId = findAdapterIdByIsbn(isbn);
|
|
25
|
+
if (!adapterId)
|
|
26
|
+
return undefined;
|
|
27
|
+
const publisher = publishers.find(p => p.id === adapterId);
|
|
28
|
+
const url = publisher?.detailUrlForIsbn?.(isbn);
|
|
29
|
+
if (!publisher || !url)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!(await checkRobotsTxt(url, deps)))
|
|
32
|
+
return undefined;
|
|
33
|
+
try {
|
|
34
|
+
const book = await publisher.getDetail(url, deps);
|
|
35
|
+
book.language ??= publisher.language ?? "ja";
|
|
36
|
+
return book;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
14
42
|
export async function getBookByIsbn(isbn, publishers, deps) {
|
|
15
43
|
const normalizedIsbn = isbn.replace(/-/g, "");
|
|
44
|
+
// 言語の既定を刻み、著者の重複を除く(openBD/カーリル/国内出版社はいずれも日本語)
|
|
45
|
+
const stamp = (book, lang = "ja") => {
|
|
46
|
+
book.language ??= lang;
|
|
47
|
+
book.authors = dedupeAuthors(book.authors);
|
|
48
|
+
return book;
|
|
49
|
+
};
|
|
16
50
|
const openBDMap = await fetchOpenBDBooks([normalizedIsbn], deps);
|
|
17
51
|
const entry = openBDMap.get(normalizedIsbn);
|
|
18
52
|
if (!entry) {
|
|
19
|
-
// openBD
|
|
53
|
+
// openBD 未収録でも、ISBN から詳細URLを決定的に引けるアダプター(O'Reilly 旧刊等)を先に試す
|
|
54
|
+
const byAdapter = await fetchDetailByIsbnCode(normalizedIsbn, publishers, deps);
|
|
55
|
+
if (byAdapter)
|
|
56
|
+
return stamp(byAdapter);
|
|
57
|
+
// それも不可ならカーリルをフォールバックとして試みる(廃業出版社など)
|
|
20
58
|
const calilBook = await fetchCalilBook(normalizedIsbn, deps);
|
|
21
59
|
if (calilBook)
|
|
22
|
-
return calilBook;
|
|
60
|
+
return stamp(calilBook);
|
|
23
61
|
throw new Error(`書誌情報が見つかりません: ${isbn}`);
|
|
24
62
|
}
|
|
25
63
|
// hanmoto.storelink が既知アダプターの baseUrl と前方一致する場合は
|
|
@@ -31,7 +69,7 @@ export async function getBookByIsbn(isbn, publishers, deps) {
|
|
|
31
69
|
const allowed = await checkRobotsTxt(storelink, deps);
|
|
32
70
|
if (allowed) {
|
|
33
71
|
try {
|
|
34
|
-
return await publisher.getDetail(storelink, deps);
|
|
72
|
+
return stamp(await publisher.getDetail(storelink, deps), publisher.language ?? "ja");
|
|
35
73
|
}
|
|
36
74
|
catch {
|
|
37
75
|
// 出版社サイトからの取得失敗は無視して次のフォールバックへ
|
|
@@ -49,13 +87,13 @@ export async function getBookByIsbn(isbn, publishers, deps) {
|
|
|
49
87
|
const matched = results.find(r => r.isbn && r.isbn.replace(/-/g, "") === normalizedIsbn)
|
|
50
88
|
?? results[0];
|
|
51
89
|
if (matched)
|
|
52
|
-
return matched;
|
|
90
|
+
return stamp(matched, publisher.language ?? "ja");
|
|
53
91
|
}
|
|
54
92
|
catch {
|
|
55
93
|
// 出版社サイトからの取得失敗は無視して openBD データで返す
|
|
56
94
|
}
|
|
57
95
|
}
|
|
58
96
|
}
|
|
59
|
-
return openBDEntryToBookRecord(entry);
|
|
97
|
+
return stamp(openBDEntryToBookRecord(entry));
|
|
60
98
|
}
|
|
61
99
|
//# sourceMappingURL=get-book-by-isbn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-book-by-isbn.js","sourceRoot":"","sources":["../../src/application/get-book-by-isbn.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gDAAgD,CAAC;
|
|
1
|
+
{"version":3,"file":"get-book-by-isbn.js","sourceRoot":"","sources":["../../src/application/get-book-by-isbn.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-book-detail.d.ts","sourceRoot":"","sources":["../../src/application/get-book-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"get-book-detail.d.ts","sourceRoot":"","sources":["../../src/application/get-book-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAK9E,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,UAAU,CAAC,CAgCrB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { checkRobotsTxt } from "../adapters/publishers/base.js";
|
|
2
2
|
import { fetchOpenBDBooks, enrichWithOpenBD } from "../adapters/openbd.js";
|
|
3
|
+
import { dedupeAuthors } from "../domain/authors.js";
|
|
3
4
|
export async function getBookDetail(url, publishers, deps) {
|
|
4
5
|
const publisher = publishers.find(p => url.startsWith(p.baseUrl));
|
|
5
6
|
if (!publisher) {
|
|
@@ -11,6 +12,8 @@ export async function getBookDetail(url, publishers, deps) {
|
|
|
11
12
|
throw new Error(`robots.txt によりアクセスが禁止されています: ${url}`);
|
|
12
13
|
}
|
|
13
14
|
const book = await publisher.getDetail(url, deps);
|
|
15
|
+
book.language ??= publisher.language ?? "ja";
|
|
16
|
+
book.authors = dedupeAuthors(book.authors);
|
|
14
17
|
// ISBNが特定できる場合はopenBDで欠損フィールドを補完
|
|
15
18
|
if (book.isbn !== undefined) {
|
|
16
19
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-book-detail.js","sourceRoot":"","sources":["../../src/application/get-book-detail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"get-book-detail.js","sourceRoot":"","sources":["../../src/application/get-book-detail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAW,EACX,UAAuC,EACvC,IAAmB;IAEnB,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAClE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,6BAA6B,GAAG,IAAI;YACpC,UAAU,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACtD,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,QAAQ,IAAI,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE3C,iCAAiC;IACjC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;YAC5D,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { BookRecord, SearchQuery } from "../domain/book.js";
|
|
2
2
|
import type { PublisherAdapter, PublisherDeps } from "../domain/publisher.js";
|
|
3
|
+
/** 1出版社あたりの検索タイムアウト。遅い1社が全体をブロックしないための上限 */
|
|
4
|
+
export declare const SEARCH_TIMEOUT_MS = 12000;
|
|
5
|
+
/** 同時に叩く出版社サイト数の上限(サイト負荷・レート制限への配慮) */
|
|
6
|
+
export declare const SEARCH_CONCURRENCY = 6;
|
|
7
|
+
/** 検索結果の書籍。クエリとの一致度 matchScore(0..1)付き */
|
|
8
|
+
export type ScoredBook = BookRecord & {
|
|
9
|
+
matchScore: number;
|
|
10
|
+
};
|
|
11
|
+
export type SearchErrorType = "robots" | "timeout" | "http" | "other";
|
|
12
|
+
export interface SearchError {
|
|
13
|
+
publisherId: string;
|
|
14
|
+
type: SearchErrorType;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
3
17
|
export interface SearchBooksResult {
|
|
4
|
-
books:
|
|
5
|
-
errors:
|
|
6
|
-
publisherId: string;
|
|
7
|
-
message: string;
|
|
8
|
-
}>;
|
|
18
|
+
books: ScoredBook[];
|
|
19
|
+
errors: SearchError[];
|
|
9
20
|
}
|
|
10
21
|
export declare function searchBooks(query: SearchQuery, publishers: readonly PublisherAdapter[], deps: PublisherDeps): Promise<SearchBooksResult>;
|
|
11
22
|
//# sourceMappingURL=search-books.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-books.d.ts","sourceRoot":"","sources":["../../src/application/search-books.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"search-books.d.ts","sourceRoot":"","sources":["../../src/application/search-books.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAO9E,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,QAAS,CAAC;AACxC,uCAAuC;AACvC,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,0CAA0C;AAC1C,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAWD,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAoE5B"}
|
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
import { checkRobotsTxt } from "../adapters/publishers/base.js";
|
|
2
2
|
import { fetchOpenBDBooks, enrichWithOpenBD } from "../adapters/openbd.js";
|
|
3
|
+
import { matchScore } from "../domain/text-match.js";
|
|
4
|
+
import { dedupeAuthors } from "../domain/authors.js";
|
|
5
|
+
import { mapWithConcurrency, withTimeout, TimeoutError } from "./concurrency.js";
|
|
6
|
+
/** 1出版社あたりの検索タイムアウト。遅い1社が全体をブロックしないための上限 */
|
|
7
|
+
export const SEARCH_TIMEOUT_MS = 12_000;
|
|
8
|
+
/** 同時に叩く出版社サイト数の上限(サイト負荷・レート制限への配慮) */
|
|
9
|
+
export const SEARCH_CONCURRENCY = 6;
|
|
10
|
+
/** 失敗理由を種別に分類する(errors の静音化・集約のため) */
|
|
11
|
+
function classifyError(reason) {
|
|
12
|
+
if (reason instanceof TimeoutError)
|
|
13
|
+
return { type: "timeout", message: reason.message };
|
|
14
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
15
|
+
if (message.includes("robots.txt"))
|
|
16
|
+
return { type: "robots", message };
|
|
17
|
+
if (/^HTTP \d/.test(message))
|
|
18
|
+
return { type: "http", message };
|
|
19
|
+
return { type: "other", message };
|
|
20
|
+
}
|
|
3
21
|
export async function searchBooks(query, publishers, deps) {
|
|
4
|
-
const
|
|
22
|
+
const matched = query.publisherId
|
|
5
23
|
? publishers.filter(p => p.id === query.publisherId)
|
|
6
24
|
: publishers;
|
|
7
|
-
|
|
25
|
+
// 大規模出版社を先に、小規模・専門サイト (scale: "minor") を後に回す。
|
|
26
|
+
// 並列度が限られるなか、ヒット率の高い大手にスロットを優先的に割り当てる。
|
|
27
|
+
const targets = [
|
|
28
|
+
...matched.filter(p => p.scale !== "minor"),
|
|
29
|
+
...matched.filter(p => p.scale === "minor"),
|
|
30
|
+
];
|
|
31
|
+
const results = await mapWithConcurrency(targets, SEARCH_CONCURRENCY, async (p) => {
|
|
8
32
|
const allowed = await checkRobotsTxt(p.baseUrl, deps);
|
|
9
33
|
if (!allowed)
|
|
10
34
|
throw new Error(`robots.txt によりアクセスが禁止されています: ${p.baseUrl}`);
|
|
11
|
-
return p.search(query, deps);
|
|
12
|
-
})
|
|
35
|
+
return withTimeout(p.search(query, deps), SEARCH_TIMEOUT_MS);
|
|
36
|
+
});
|
|
13
37
|
const books = [];
|
|
14
38
|
const errors = [];
|
|
15
39
|
for (let i = 0; i < results.length; i++) {
|
|
16
40
|
const result = results[i];
|
|
17
41
|
const publisher = targets[i];
|
|
18
42
|
if (result.status === "fulfilled") {
|
|
43
|
+
// 言語が未設定の書籍に出版社の既定言語(省略時 "ja")を刻む
|
|
44
|
+
for (const book of result.value) {
|
|
45
|
+
book.language ??= publisher.language ?? "ja";
|
|
46
|
+
}
|
|
19
47
|
books.push(...result.value);
|
|
20
48
|
}
|
|
21
49
|
else {
|
|
22
|
-
|
|
23
|
-
? result.reason.message
|
|
24
|
-
: String(result.reason);
|
|
25
|
-
errors.push({ publisherId: publisher.id, message });
|
|
50
|
+
errors.push({ publisherId: publisher.id, ...classifyError(result.reason) });
|
|
26
51
|
}
|
|
27
52
|
}
|
|
28
53
|
// ISBNが特定できる書籍をopenBDで一括補完
|
|
@@ -44,6 +69,18 @@ export async function searchBooks(query, publishers, deps) {
|
|
|
44
69
|
// openBD の取得失敗は無視して出版社から取得できた情報を返す
|
|
45
70
|
}
|
|
46
71
|
}
|
|
47
|
-
|
|
72
|
+
// クエリとの一致度を付与し、著者の重複を除いて、ベストマッチ順に並べる
|
|
73
|
+
const hasQuery = Boolean(query.title || query.author);
|
|
74
|
+
const scored = books
|
|
75
|
+
.map(book => ({
|
|
76
|
+
...book,
|
|
77
|
+
authors: dedupeAuthors(book.authors),
|
|
78
|
+
matchScore: Math.round(matchScore(query, book) * 1000) / 1000,
|
|
79
|
+
}))
|
|
80
|
+
// クエリ語があるのに一致度ゼロ=検索サイトが返す新着フォールバック等の無関係本。
|
|
81
|
+
// 「該当なし」と「誤ヒット」を呼び出し側が区別できるよう、ここで除外して空にする。
|
|
82
|
+
.filter(book => !hasQuery || book.matchScore > 0)
|
|
83
|
+
.sort((a, b) => b.matchScore - a.matchScore);
|
|
84
|
+
return { books: scored, errors };
|
|
48
85
|
}
|
|
49
86
|
//# sourceMappingURL=search-books.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-books.js","sourceRoot":"","sources":["../../src/application/search-books.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"search-books.js","sourceRoot":"","sources":["../../src/application/search-books.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authors.d.ts","sourceRoot":"","sources":["../../src/domain/authors.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAYlE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalizeForMatch } from "./text-match.js";
|
|
2
|
+
/**
|
|
3
|
+
* 著者配列から重複を除く(出現順を保持)。
|
|
4
|
+
* 比較キーは normalizeForMatch なので "吉川 邦夫" と "吉川邦夫" のような表記ゆれも同一視する。
|
|
5
|
+
* 表示は最初に現れた表記を採用する。空文字は除去する。
|
|
6
|
+
*/
|
|
7
|
+
export function dedupeAuthors(authors) {
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const author of authors) {
|
|
11
|
+
const name = author.trim();
|
|
12
|
+
if (!name)
|
|
13
|
+
continue;
|
|
14
|
+
const key = normalizeForMatch(name);
|
|
15
|
+
if (key === "" || seen.has(key))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(key);
|
|
18
|
+
out.push(name);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=authors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authors.js","sourceRoot":"","sources":["../../src/domain/authors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,GAAG,KAAK,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC1C,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/domain/book.d.ts
CHANGED
|
@@ -15,10 +15,12 @@ export interface BookRecord {
|
|
|
15
15
|
authors: string[];
|
|
16
16
|
publisher: string;
|
|
17
17
|
publishedAt?: string;
|
|
18
|
+
language?: string;
|
|
18
19
|
isbn?: string;
|
|
19
20
|
asin?: string;
|
|
20
21
|
url: string;
|
|
21
22
|
price?: number;
|
|
23
|
+
currency?: string;
|
|
22
24
|
coverImageUrl?: string;
|
|
23
25
|
description?: string;
|
|
24
26
|
tags?: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"book.d.ts","sourceRoot":"","sources":["../../src/domain/book.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,KAAK,CAAC;AAEjE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
|
1
|
+
{"version":3,"file":"book.d.ts","sourceRoot":"","sources":["../../src/domain/book.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,KAAK,CAAC;AAEjE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** ハイフン・空白・"ISBN" 接頭辞を除去して数字列(ISBN-10 は末尾 X 可)に正規化する */
|
|
2
|
+
export declare function normalizeIsbn(text: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* 文字列が ISBN-10 / ISBN-13 として妥当な形か判定する(桁数・接頭辞のみ。チェックディジットは検証しない)。
|
|
5
|
+
* search_books の title に ISBN が入力されたケースを ISBN 経路へ振り分けるために使う。
|
|
6
|
+
*/
|
|
7
|
+
export declare function looksLikeIsbn(text: string): boolean;
|
|
8
|
+
//# sourceMappingURL=isbn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isbn.d.ts","sourceRoot":"","sources":["../../src/domain/isbn.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** ハイフン・空白・"ISBN" 接頭辞を除去して数字列(ISBN-10 は末尾 X 可)に正規化する */
|
|
2
|
+
export function normalizeIsbn(text) {
|
|
3
|
+
return text
|
|
4
|
+
.replace(/isbn/i, "")
|
|
5
|
+
.replace(/[\s-]/g, "")
|
|
6
|
+
.toUpperCase();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 文字列が ISBN-10 / ISBN-13 として妥当な形か判定する(桁数・接頭辞のみ。チェックディジットは検証しない)。
|
|
10
|
+
* search_books の title に ISBN が入力されたケースを ISBN 経路へ振り分けるために使う。
|
|
11
|
+
*/
|
|
12
|
+
export function looksLikeIsbn(text) {
|
|
13
|
+
const s = normalizeIsbn(text);
|
|
14
|
+
return /^(?:97[89]\d{10}|\d{9}[\dX])$/.test(s);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=isbn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isbn.js","sourceRoot":"","sources":["../../src/domain/isbn.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI;SACR,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;SACpB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,WAAW,EAAE,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,+BAA+B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -11,7 +11,23 @@ export interface PublisherAdapter {
|
|
|
11
11
|
readonly id: string;
|
|
12
12
|
readonly name: string;
|
|
13
13
|
readonly baseUrl: string;
|
|
14
|
+
/** この出版社の書籍の既定言語(ISO 639-1)。省略時はアプリ層で "ja" とみなす */
|
|
15
|
+
readonly language?: string;
|
|
16
|
+
/**
|
|
17
|
+
* 検索時のスケジューリング/キャッシュ戦略のヒント。
|
|
18
|
+
* - 省略 = 大規模出版社(優先的にスケジュールし、通常 TTL でキャッシュ)
|
|
19
|
+
* - "minor" = 小規模・専門/ローカルフィルタ型(大規模の後に回し、カタログを長 TTL で全キャッシュ)
|
|
20
|
+
*/
|
|
21
|
+
readonly scale?: "minor";
|
|
14
22
|
search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]>;
|
|
15
23
|
getDetail(url: string, deps: PublisherDeps): Promise<BookRecord>;
|
|
24
|
+
/**
|
|
25
|
+
* ISBN から詳細ページの URL を決定的に構成できるアダプター向け(任意)。
|
|
26
|
+
* 詳細ページが ISBN ベースの安定 URL を持つサイト(例 O'Reilly の `/books/{isbn}/`)で実装する。
|
|
27
|
+
* openBD・カーリル未収録かつ検索一覧にも出ない旧刊(販売終了・電子書籍専売)を
|
|
28
|
+
* `get_book_by_isbn` のフォールバックで直接引くために使う([[docs/design-doc.md「カバレッジの制約」]])。
|
|
29
|
+
* URL を構成できない ISBN は undefined を返す。
|
|
30
|
+
*/
|
|
31
|
+
detailUrlForIsbn?(isbn: string): string | undefined;
|
|
16
32
|
}
|
|
17
33
|
//# sourceMappingURL=publisher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../../src/domain/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACvE,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../../src/domain/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACvE,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjE;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACrD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BookRecord, SearchQuery } from "./book.js";
|
|
2
|
+
/**
|
|
3
|
+
* 照合用にテキストを正規化する。
|
|
4
|
+
*
|
|
5
|
+
* - NFKC で全角・半角を統一("Rust" → "Rust"、"(" → "(")
|
|
6
|
+
* - 小文字化
|
|
7
|
+
* - 装飾・区切り記号(【】〔〕()()[]、・ー― ~: 空白など)を除去
|
|
8
|
+
*
|
|
9
|
+
* PDF奥付から抜いた表記ゆれのある title/author を、出版社サイトの
|
|
10
|
+
* 表記とできるだけ一致させるための前処理。
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeForMatch(text: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* クエリ書名と候補書名の一致度(0..1)。
|
|
15
|
+
*
|
|
16
|
+
* 正規化後に完全一致すれば 1。そうでなければクエリを空白でトークン分割し、
|
|
17
|
+
* 候補書名に含まれるトークンの割合でスコアを付ける(完全一致を上回らないよう 0.9 で頭打ち)。
|
|
18
|
+
* トークン方式により純日本語の部分文字列("メール技術 教科書" → "メール技術の教科書")も拾える。
|
|
19
|
+
* どのトークンも含まれなければ 0 = 無関係(フォールバック書籍の除外に使う)。
|
|
20
|
+
*/
|
|
21
|
+
export declare function titleMatchScore(query: string, candidate: string): number;
|
|
22
|
+
/** クエリ著者名と候補著者リストの一致度(0..1)。最も一致する1名を採用 */
|
|
23
|
+
export declare function authorMatchScore(query: string, candidates: readonly string[]): number;
|
|
24
|
+
/**
|
|
25
|
+
* クエリと書籍レコードの総合一致度(0..1)。
|
|
26
|
+
*
|
|
27
|
+
* - title・author 両方が指定された場合は平均
|
|
28
|
+
* - 片方のみ指定ならそのスコア
|
|
29
|
+
* - どちらも未指定なら 0(スコア付けの意味がない)
|
|
30
|
+
*/
|
|
31
|
+
export declare function matchScore(query: SearchQuery, book: BookRecord): number;
|
|
32
|
+
//# sourceMappingURL=text-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-match.d.ts","sourceRoot":"","sources":["../../src/domain/text-match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEzD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtD;AAcD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAWxE;AAED,2CAA2C;AAC3C,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CASrF;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMvE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 照合用にテキストを正規化する。
|
|
3
|
+
*
|
|
4
|
+
* - NFKC で全角・半角を統一("Rust" → "Rust"、"(" → "(")
|
|
5
|
+
* - 小文字化
|
|
6
|
+
* - 装飾・区切り記号(【】〔〕()()[]、・ー― ~: 空白など)を除去
|
|
7
|
+
*
|
|
8
|
+
* PDF奥付から抜いた表記ゆれのある title/author を、出版社サイトの
|
|
9
|
+
* 表記とできるだけ一致させるための前処理。
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeForMatch(text) {
|
|
12
|
+
return text
|
|
13
|
+
.normalize("NFKC")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
// 装飾括弧・区切り・長音/ダッシュ・約物・空白をまとめて除去
|
|
16
|
+
.replace(/[\s【】〔〕「」『』《》[\](){}(){}<><>、。,.・,.:;:;!!??~〜~\--—―ー_/\\|=+*'"`]/g, "");
|
|
17
|
+
}
|
|
18
|
+
/** 1 を最良とする 0..1 の包含スコア。短い側が長い側に完全包含されるほど高い */
|
|
19
|
+
function containmentScore(a, b) {
|
|
20
|
+
if (!a || !b)
|
|
21
|
+
return 0;
|
|
22
|
+
if (a === b)
|
|
23
|
+
return 1;
|
|
24
|
+
const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a];
|
|
25
|
+
if (longer.includes(shorter)) {
|
|
26
|
+
// 完全一致(1)に対し、長さ比で部分一致を割り引く(短すぎる偶然一致を抑制)
|
|
27
|
+
return 0.5 + 0.5 * (shorter.length / longer.length);
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* クエリ書名と候補書名の一致度(0..1)。
|
|
33
|
+
*
|
|
34
|
+
* 正規化後に完全一致すれば 1。そうでなければクエリを空白でトークン分割し、
|
|
35
|
+
* 候補書名に含まれるトークンの割合でスコアを付ける(完全一致を上回らないよう 0.9 で頭打ち)。
|
|
36
|
+
* トークン方式により純日本語の部分文字列("メール技術 教科書" → "メール技術の教科書")も拾える。
|
|
37
|
+
* どのトークンも含まれなければ 0 = 無関係(フォールバック書籍の除外に使う)。
|
|
38
|
+
*/
|
|
39
|
+
export function titleMatchScore(query, candidate) {
|
|
40
|
+
const cand = normalizeForMatch(candidate);
|
|
41
|
+
const q = normalizeForMatch(query);
|
|
42
|
+
if (!q || !cand)
|
|
43
|
+
return 0;
|
|
44
|
+
if (q === cand)
|
|
45
|
+
return 1;
|
|
46
|
+
const tokens = query.split(/\s+/).map(normalizeForMatch).filter(Boolean);
|
|
47
|
+
if (tokens.length === 0)
|
|
48
|
+
return 0;
|
|
49
|
+
const matched = tokens.filter(t => cand.includes(t)).length;
|
|
50
|
+
if (matched === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
return Math.min(0.9, 0.9 * (matched / tokens.length));
|
|
53
|
+
}
|
|
54
|
+
/** クエリ著者名と候補著者リストの一致度(0..1)。最も一致する1名を採用 */
|
|
55
|
+
export function authorMatchScore(query, candidates) {
|
|
56
|
+
const q = normalizeForMatch(query);
|
|
57
|
+
if (!q)
|
|
58
|
+
return 0;
|
|
59
|
+
let best = 0;
|
|
60
|
+
for (const c of candidates) {
|
|
61
|
+
best = Math.max(best, containmentScore(q, normalizeForMatch(c)));
|
|
62
|
+
if (best === 1)
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return best;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* クエリと書籍レコードの総合一致度(0..1)。
|
|
69
|
+
*
|
|
70
|
+
* - title・author 両方が指定された場合は平均
|
|
71
|
+
* - 片方のみ指定ならそのスコア
|
|
72
|
+
* - どちらも未指定なら 0(スコア付けの意味がない)
|
|
73
|
+
*/
|
|
74
|
+
export function matchScore(query, book) {
|
|
75
|
+
const scores = [];
|
|
76
|
+
if (query.title)
|
|
77
|
+
scores.push(titleMatchScore(query.title, book.title));
|
|
78
|
+
if (query.author)
|
|
79
|
+
scores.push(authorMatchScore(query.author, book.authors));
|
|
80
|
+
if (scores.length === 0)
|
|
81
|
+
return 0;
|
|
82
|
+
return scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=text-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-match.js","sourceRoot":"","sources":["../../src/domain/text-match.ts"],"names":[],"mappings":"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"}
|
package/dist/main.js
CHANGED
|
File without changes
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import type { PublisherAdapter, PublisherDeps } from "../domain/publisher.js";
|
|
3
|
+
import type { SearchError } from "../application/search-books.js";
|
|
4
|
+
/**
|
|
5
|
+
* 出版社ごとのエラーを種別でまとめて静音化する。
|
|
6
|
+
* 18社横断で大量に並ぶ errors を、種別×対象出版社の数件に集約する。
|
|
7
|
+
*/
|
|
8
|
+
export declare function summarizeErrors(errors: SearchError[]): Record<string, unknown>[];
|
|
3
9
|
export declare function createServer(publishers: readonly PublisherAdapter[], deps: PublisherDeps): Server;
|
|
4
10
|
export declare function startServer(publishers: readonly PublisherAdapter[], deps: PublisherDeps): Promise<void>;
|
|
5
11
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAMnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAMnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAgClE;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAahF;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,IAAI,EAAE,aAAa,GAClB,MAAM,CAiFR;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -4,6 +4,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
4
4
|
import { searchBooks } from "../application/search-books.js";
|
|
5
5
|
import { getBookDetail } from "../application/get-book-detail.js";
|
|
6
6
|
import { getBookByIsbn } from "../application/get-book-by-isbn.js";
|
|
7
|
+
import { looksLikeIsbn } from "../domain/isbn.js";
|
|
8
|
+
import { VERSION } from "../version.js";
|
|
7
9
|
import { TOOLS } from "./tools.js";
|
|
8
10
|
// --- 出力フォーマット ---
|
|
9
11
|
const DRM_LABELS = {
|
|
@@ -20,10 +22,34 @@ function formatBook(book) {
|
|
|
20
22
|
return book;
|
|
21
23
|
return { ...book, ebookStores: book.ebookStores.map(formatEbookStore) };
|
|
22
24
|
}
|
|
25
|
+
const ERROR_TYPE_LABELS = {
|
|
26
|
+
robots: "robots.txt によりアクセス禁止",
|
|
27
|
+
timeout: "タイムアウト",
|
|
28
|
+
http: "HTTPエラー",
|
|
29
|
+
other: "その他のエラー",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 出版社ごとのエラーを種別でまとめて静音化する。
|
|
33
|
+
* 18社横断で大量に並ぶ errors を、種別×対象出版社の数件に集約する。
|
|
34
|
+
*/
|
|
35
|
+
export function summarizeErrors(errors) {
|
|
36
|
+
const byType = new Map();
|
|
37
|
+
for (const e of errors) {
|
|
38
|
+
const list = byType.get(e.type) ?? [];
|
|
39
|
+
list.push(e.publisherId);
|
|
40
|
+
byType.set(e.type, list);
|
|
41
|
+
}
|
|
42
|
+
return [...byType.entries()].map(([type, publishers]) => ({
|
|
43
|
+
type,
|
|
44
|
+
label: ERROR_TYPE_LABELS[type],
|
|
45
|
+
count: publishers.length,
|
|
46
|
+
publishers,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
23
49
|
export function createServer(publishers, deps) {
|
|
24
50
|
const server = new Server({
|
|
25
51
|
name: "@zonuexe/techbook-mcp",
|
|
26
|
-
version:
|
|
52
|
+
version: VERSION,
|
|
27
53
|
}, {
|
|
28
54
|
capabilities: { tools: {} },
|
|
29
55
|
});
|
|
@@ -34,16 +60,26 @@ export function createServer(publishers, deps) {
|
|
|
34
60
|
const { name, arguments: args = {} } = request.params;
|
|
35
61
|
switch (name) {
|
|
36
62
|
case "search_books": {
|
|
63
|
+
const title = typeof args["title"] === "string" ? args["title"] : undefined;
|
|
64
|
+
const author = typeof args["author"] === "string" ? args["author"] : undefined;
|
|
65
|
+
// title に ISBN が入力された場合は全社横断せず ISBN 経路へ振り分ける(最短・確実)
|
|
66
|
+
if (title && !author && looksLikeIsbn(title)) {
|
|
67
|
+
const book = await getBookByIsbn(title, publishers, deps);
|
|
68
|
+
const output = { books: [{ ...formatBook(book), matchScore: 1 }] };
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
37
73
|
const query = {
|
|
38
|
-
title
|
|
39
|
-
author
|
|
74
|
+
title,
|
|
75
|
+
author,
|
|
40
76
|
publisherId: typeof args["publisher"] === "string" ? args["publisher"] : undefined,
|
|
41
77
|
limit: typeof args["limit"] === "number" ? Math.min(args["limit"], 50) : 10,
|
|
42
78
|
};
|
|
43
79
|
const { books, errors } = await searchBooks(query, publishers, deps);
|
|
44
80
|
const output = { books: books.map(formatBook) };
|
|
45
81
|
if (errors.length > 0)
|
|
46
|
-
output["errors"] = errors;
|
|
82
|
+
output["errors"] = summarizeErrors(errors);
|
|
47
83
|
return {
|
|
48
84
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
49
85
|
};
|