@zonuexe/techbook-mcp 0.2.2 → 0.2.4
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 +28 -1
- package/README.md +39 -20
- package/dist/adapters/calil.d.ts +10 -0
- package/dist/adapters/calil.d.ts.map +1 -0
- package/dist/adapters/calil.js +45 -0
- package/dist/adapters/calil.js.map +1 -0
- package/dist/adapters/openbd.d.ts +57 -0
- package/dist/adapters/openbd.d.ts.map +1 -0
- package/dist/adapters/openbd.js +87 -0
- package/dist/adapters/openbd.js.map +1 -0
- package/dist/adapters/publishers/google-books.d.ts +4 -0
- package/dist/adapters/publishers/google-books.d.ts.map +1 -0
- package/dist/adapters/publishers/google-books.js +75 -0
- package/dist/adapters/publishers/google-books.js.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
- package/dist/adapters/publishers/juse-p.d.ts +3 -0
- package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
- package/dist/adapters/publishers/juse-p.js +110 -0
- package/dist/adapters/publishers/juse-p.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +4 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.js +6 -18
- package/dist/adapters/publishers/tatsu-zine.js.map +1 -1
- package/dist/application/get-book-by-isbn.d.ts +13 -0
- package/dist/application/get-book-by-isbn.d.ts.map +1 -0
- package/dist/application/get-book-by-isbn.js +61 -0
- package/dist/application/get-book-by-isbn.js.map +1 -0
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +16 -1
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +20 -0
- package/dist/application/search-books.js.map +1 -1
- package/dist/config/credentials.d.ts +8 -0
- package/dist/config/credentials.d.ts.map +1 -0
- package/dist/config/credentials.js +32 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/main.js +15 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +10 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +16 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +43 -0
- package/dist/setup.js.map +1 -0
- package/flake.lock +61 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -36
- package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
- package/.github/workflows/test.yml +0 -72
- package/.oxlintrc.json +0 -12
- package/AGENTS.md +0 -100
- package/deno.json +0 -3
- package/src/adapters/cache/memory-cache.ts +0 -31
- package/src/adapters/cache/null-cache.ts +0 -8
- package/src/adapters/html/cheerio-parser.ts +0 -50
- package/src/adapters/http/fetch-client.ts +0 -47
- package/src/adapters/http/mock-client.ts +0 -77
- package/src/adapters/publishers/base.ts +0 -279
- package/src/adapters/publishers/book-tech.ts +0 -117
- package/src/adapters/publishers/born-digital.ts +0 -143
- package/src/adapters/publishers/coronasha.ts +0 -139
- package/src/adapters/publishers/gihyo.ts +0 -120
- package/src/adapters/publishers/impress.ts +0 -103
- package/src/adapters/publishers/lambdanote.ts +0 -146
- package/src/adapters/publishers/manatee.ts +0 -113
- package/src/adapters/publishers/maruzen-publishing.ts +0 -129
- package/src/adapters/publishers/optronics.ts +0 -113
- package/src/adapters/publishers/oreilly-japan.ts +0 -133
- package/src/adapters/publishers/peaks.ts +0 -98
- package/src/adapters/publishers/personal-media.ts +0 -168
- package/src/adapters/publishers/registry.ts +0 -38
- package/src/adapters/publishers/rutles.ts +0 -149
- package/src/adapters/publishers/saiensu.ts +0 -136
- package/src/adapters/publishers/seshop.ts +0 -121
- package/src/adapters/publishers/tatsu-zine.ts +0 -154
- package/src/adapters/publishers/techbookfest.ts +0 -179
- package/src/application/get-book-detail.ts +0 -24
- package/src/application/search-books.ts +0 -44
- package/src/domain/book.ts +0 -35
- package/src/domain/publisher.ts +0 -18
- package/src/main.ts +0 -14
- package/src/mcp/server.ts +0 -103
- package/src/mcp/tools.ts +0 -54
- package/src/ports/cache.ts +0 -5
- package/src/ports/html-parser.ts +0 -15
- package/src/ports/http.ts +0 -17
- package/tests/fixtures/book-tech-detail.html +0 -51
- package/tests/fixtures/book-tech-search.html +0 -91
- package/tests/fixtures/born-digital-detail.html +0 -62
- package/tests/fixtures/born-digital-search.html +0 -51
- package/tests/fixtures/coronasha-detail.html +0 -41
- package/tests/fixtures/coronasha-search.html +0 -61
- package/tests/fixtures/gihyo-detail.html +0 -42
- package/tests/fixtures/gihyo-search.json +0 -54
- package/tests/fixtures/impress-detail-epub.html +0 -746
- package/tests/fixtures/impress-detail-social.html +0 -689
- package/tests/fixtures/lambdanote-search.html +0 -66
- package/tests/fixtures/manatee-detail.html +0 -53
- package/tests/fixtures/manatee-search.html +0 -59
- package/tests/fixtures/maruzen-detail.html +0 -51
- package/tests/fixtures/maruzen-search.html +0 -60
- package/tests/fixtures/optronics-detail.html +0 -30
- package/tests/fixtures/optronics-search.html +0 -75
- package/tests/fixtures/oreilly-detail.html +0 -52
- package/tests/fixtures/oreilly-ebook-list.html +0 -53
- package/tests/fixtures/peaks-detail.html +0 -39
- package/tests/fixtures/peaks-top.html +0 -50
- package/tests/fixtures/personal-media-detail.html +0 -32
- package/tests/fixtures/personal-media-search.html +0 -39
- package/tests/fixtures/rutles-detail.html +0 -32
- package/tests/fixtures/rutles-search.html +0 -62
- package/tests/fixtures/saiensu-detail.html +0 -41
- package/tests/fixtures/saiensu-search.html +0 -65
- package/tests/fixtures/seshop-detail.html +0 -45
- package/tests/fixtures/seshop-search.html +0 -58
- package/tests/fixtures/tatsu-zine-detail-free.html +0 -22
- package/tests/fixtures/tatsu-zine-search.html +0 -40
- package/tests/fixtures/techbookfest-search.json +0 -73
- package/tests/unit/adapters/base.test.ts +0 -441
- package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
- package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
- package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
- package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
- package/tests/unit/adapters/publishers/impress.test.ts +0 -129
- package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
- package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
- package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
- package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
- package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
- package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
- package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
- package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
- package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
- package/tests/unit/adapters/registry.test.ts +0 -37
- package/tests/unit/application/get-book-detail.test.ts +0 -102
- package/tests/unit/application/search-books.test.ts +0 -137
- package/tsconfig.json +0 -17
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm info:*)",
|
|
5
|
-
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('version:', d.get\\('version'\\)\\); print\\('exports:', list\\(d.get\\('exports', {}\\).keys\\(\\)\\)[:20]\\)\")",
|
|
6
|
-
"WebFetch(domain:gihyo.jp)",
|
|
7
|
-
"WebFetch(domain:www.lambdanote.com)",
|
|
8
|
-
"Bash(curl:*)",
|
|
9
|
-
"Bash(python3 -c ':*)",
|
|
10
|
-
"Bash(npx vitest:*)",
|
|
11
|
-
"Bash(git mv:*)",
|
|
12
|
-
"Bash(git add:*)",
|
|
13
|
-
"Bash(git commit -m ':*)",
|
|
14
|
-
"Bash(node:*)",
|
|
15
|
-
"Bash(npm install:*)",
|
|
16
|
-
"Bash(curl -s \"https://wgn-obs.shop-pro.jp/?mode=srh&keyword=HTML\" -A \"Mozilla/5.0\")",
|
|
17
|
-
"Read(//tmp/**)",
|
|
18
|
-
"Bash(npm test:*)",
|
|
19
|
-
"Bash(python3:*)",
|
|
20
|
-
"WebFetch(domain:www.personal-media.co.jp)",
|
|
21
|
-
"Bash(grep -rn \"export.*Element\" node_modules/cheerio/dist/esm/*.d.ts)",
|
|
22
|
-
"Bash(npx tsc:*)",
|
|
23
|
-
"Bash(npx oxlint:*)",
|
|
24
|
-
"Bash(npm run:*)",
|
|
25
|
-
"Bash(curl -s \"https://book.impress.co.jp/books/1124101031\")",
|
|
26
|
-
"Bash(curl -s \"https://book.impress.co.jp/books/1125101113\")",
|
|
27
|
-
"Bash(cp /tmp/impress-detail-social.html tests/fixtures/impress-detail-social.html)",
|
|
28
|
-
"Bash(cp /tmp/impress-detail-nodrm.html tests/fixtures/impress-detail-epub.html)",
|
|
29
|
-
"Bash(wc -c tests/fixtures/impress-detail-*.html)",
|
|
30
|
-
"Bash(git -C /Users/megurine/repo/js/techbook-mcp status)",
|
|
31
|
-
"Bash(git -C /Users/megurine/repo/js/techbook-mcp diff)",
|
|
32
|
-
"Bash(git:*)",
|
|
33
|
-
"Bash(npm pkg:*)"
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: techbook-mcp-release-prep
|
|
3
|
-
description: Prepare a techbook-mcp release by bumping the version, updating the changelog, and running verification. Use when the user asks to prepare the next version, cut a release, or make sure versioned files are consistent before tagging.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# techbook-mcp Release Prep
|
|
7
|
-
|
|
8
|
-
Follow this workflow when preparing a new `techbook-mcp` release.
|
|
9
|
-
|
|
10
|
-
## Decide the Next Version
|
|
11
|
-
|
|
12
|
-
Choose the next semantic version before touching any files.
|
|
13
|
-
|
|
14
|
-
- `patch` (0.x.Y) — bug fixes, no new functionality
|
|
15
|
-
- `minor` (0.X.0) — new adapters, new features, backward-compatible changes
|
|
16
|
-
- `major` (X.0.0) — breaking changes to the MCP tool interface or `BookRecord` schema
|
|
17
|
-
|
|
18
|
-
If the user specifies a version, use it. Otherwise infer from the unreleased commits since the last tag.
|
|
19
|
-
|
|
20
|
-
## Update Versioned Files
|
|
21
|
-
|
|
22
|
-
Update these files together in one pass:
|
|
23
|
-
|
|
24
|
-
- `CHANGELOG.md`
|
|
25
|
-
- `package.json` — `"version"` field
|
|
26
|
-
- `package-lock.json` — `"version"` field in the root object (same value as `package.json`)
|
|
27
|
-
|
|
28
|
-
### CHANGELOG.md Rules
|
|
29
|
-
|
|
30
|
-
- If `CHANGELOG.md` does not exist yet, create it with the standard Keep a Changelog header.
|
|
31
|
-
- Add a new `## [x.y.z] - YYYY-MM-DD` section directly below `## [Unreleased]`.
|
|
32
|
-
- Use Keep a Changelog section headings as needed: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
|
|
33
|
-
- Group the same kinds of changes under the same heading.
|
|
34
|
-
- Write entries user-facing: what changed, not how. Skip internal refactors unless they affect users.
|
|
35
|
-
- List new publisher adapters under `Added`.
|
|
36
|
-
- Preserve the release date in every version heading.
|
|
37
|
-
- Keep the `[Unreleased]` section empty after moving its contents to the new release section.
|
|
38
|
-
- Update the `[Unreleased]` compare link and add the new release compare link at the bottom of the file.
|
|
39
|
-
- Keep version headings and bottom-of-file links consistent so releases and compare ranges remain linkable.
|
|
40
|
-
|
|
41
|
-
### CHANGELOG.md Template (first release)
|
|
42
|
-
|
|
43
|
-
```markdown
|
|
44
|
-
# Changelog
|
|
45
|
-
|
|
46
|
-
All notable changes to this project will be documented in this file.
|
|
47
|
-
|
|
48
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
49
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
50
|
-
|
|
51
|
-
## [Unreleased]
|
|
52
|
-
|
|
53
|
-
## [x.y.z] - YYYY-MM-DD
|
|
54
|
-
|
|
55
|
-
### Added
|
|
56
|
-
|
|
57
|
-
- ...
|
|
58
|
-
|
|
59
|
-
[Unreleased]: https://github.com/zonuexe/techbook-mcp/compare/vx.y.z...HEAD
|
|
60
|
-
[x.y.z]: https://github.com/zonuexe/techbook-mcp/releases/tag/vx.y.z
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Verify Before Committing
|
|
64
|
-
|
|
65
|
-
Run all checks in order:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npm test
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npm run lint
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm run build
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
All three must pass. Do not commit if any check fails.
|
|
80
|
-
|
|
81
|
-
## Commit the Release
|
|
82
|
-
|
|
83
|
-
Prefer a single release-prep commit containing:
|
|
84
|
-
|
|
85
|
-
- version bump in `package.json` and `package-lock.json`
|
|
86
|
-
- `CHANGELOG.md` update
|
|
87
|
-
|
|
88
|
-
Use this commit message format:
|
|
89
|
-
|
|
90
|
-
```text
|
|
91
|
-
Bump up version to x.y.z
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Do not include other unrelated changes in the release commit.
|
|
95
|
-
|
|
96
|
-
## Quick Checklist
|
|
97
|
-
|
|
98
|
-
- Working tree starts clean or you understand every pending change.
|
|
99
|
-
- `CHANGELOG.md` has a new `## [x.y.z] - YYYY-MM-DD` section with user-facing entries.
|
|
100
|
-
- `package.json` and `package-lock.json` both show the new version.
|
|
101
|
-
- Bottom-of-file links in `CHANGELOG.md` are consistent.
|
|
102
|
-
- `npm test` passed.
|
|
103
|
-
- `npm run lint` passed.
|
|
104
|
-
- `npm run build` passed.
|
|
105
|
-
- Commit message follows `chore: release x.y.z`.
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main, master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main, master]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
node-version: [22.x]
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
21
|
-
uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: ${{ matrix.node-version }}
|
|
24
|
-
cache: npm
|
|
25
|
-
|
|
26
|
-
- name: Install dependencies
|
|
27
|
-
run: npm ci
|
|
28
|
-
|
|
29
|
-
- name: Lint
|
|
30
|
-
run: npm run lint
|
|
31
|
-
|
|
32
|
-
- name: Type check
|
|
33
|
-
run: npx tsc --noEmit
|
|
34
|
-
|
|
35
|
-
- name: Run tests
|
|
36
|
-
run: npm test
|
|
37
|
-
|
|
38
|
-
- name: Build
|
|
39
|
-
run: npm run build
|
|
40
|
-
|
|
41
|
-
bun:
|
|
42
|
-
runs-on: ubuntu-latest
|
|
43
|
-
steps:
|
|
44
|
-
- uses: actions/checkout@v4
|
|
45
|
-
|
|
46
|
-
- uses: oven-sh/setup-bun@v2
|
|
47
|
-
|
|
48
|
-
- name: Install dependencies
|
|
49
|
-
run: bun install
|
|
50
|
-
|
|
51
|
-
- name: Run tests
|
|
52
|
-
run: bun test
|
|
53
|
-
|
|
54
|
-
deno:
|
|
55
|
-
runs-on: ubuntu-latest
|
|
56
|
-
steps:
|
|
57
|
-
- uses: actions/checkout@v4
|
|
58
|
-
|
|
59
|
-
- uses: denoland/setup-deno@v2
|
|
60
|
-
with:
|
|
61
|
-
deno-version: v2.x
|
|
62
|
-
|
|
63
|
-
- name: Install Node deps (for node_modules)
|
|
64
|
-
uses: actions/setup-node@v4
|
|
65
|
-
with:
|
|
66
|
-
node-version: 22.x
|
|
67
|
-
cache: npm
|
|
68
|
-
|
|
69
|
-
- run: npm ci
|
|
70
|
-
|
|
71
|
-
- name: Run tests
|
|
72
|
-
run: deno test --allow-read --no-check --sloppy-imports tests/
|
package/.oxlintrc.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
|
3
|
-
"plugins": ["typescript", "unicorn"],
|
|
4
|
-
"rules": {
|
|
5
|
-
"no-unused-vars": "error",
|
|
6
|
-
"no-console": "warn",
|
|
7
|
-
"unicorn/no-array-for-each": "error",
|
|
8
|
-
"unicorn/prefer-string-slice": "error",
|
|
9
|
-
"typescript/consistent-type-imports": "error"
|
|
10
|
-
},
|
|
11
|
-
"ignorePatterns": ["dist/**", "node_modules/**"]
|
|
12
|
-
}
|
package/AGENTS.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# techbook-mcp — AI エージェント向けガイド
|
|
2
|
-
|
|
3
|
-
日本語技術書の書誌情報を出版社公式サイト・APIから取得するMCPサーバー。
|
|
4
|
-
詳細な設計は [docs/design-doc.md](docs/design-doc.md) を参照。
|
|
5
|
-
|
|
6
|
-
## コミットメッセージ規約
|
|
7
|
-
|
|
8
|
-
Conventional Commits は使わない。コミットメッセージは端的な日本語または英語の命令形で書く。
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
robots.txt チェックを追加し結果を6時間キャッシュする
|
|
12
|
-
Add robots.txt check with 6-hour cache
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 開発コマンド
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install
|
|
19
|
-
npm test # ユニットテスト実行 (node:test)
|
|
20
|
-
npm run build # TypeScript コンパイル → dist/
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## コーディング規約
|
|
24
|
-
|
|
25
|
-
- **新しいアダプターを追加するときは必ずテストも書く**(`tests/unit/adapters/publishers/{id}.test.ts`)
|
|
26
|
-
- テストは `MockHttpClient` + `NullCacheStore` + `CheerioHtmlParser` の組み合わせで書く
|
|
27
|
-
- フィクスチャHTMLは `tests/fixtures/` に配置し、実サイトの構造を忠実に再現する
|
|
28
|
-
- `fetchText()` はキャッシュ・ヘッダーを内包するため、アダプター内では直接 `deps.http.get()` を呼ばない
|
|
29
|
-
- Referer ヘッダーが必要なサイトは `fetchText(url, deps, { Referer: "..." })` の第3引数を使う
|
|
30
|
-
- 著者名から役割語(著・訳・編・監修・監訳など)を除去すること
|
|
31
|
-
- 価格は税込み整数(円)で `BookRecord.price` に格納する
|
|
32
|
-
- `publisher` フィールドには実際の出版社名を入れる(ストアプラットフォーム名ではない)
|
|
33
|
-
|
|
34
|
-
## 新しい出版社アダプターを追加するとき
|
|
35
|
-
|
|
36
|
-
`docs/design-doc.md` の「新しいアダプターの追加手順」を参照。要点は以下:
|
|
37
|
-
|
|
38
|
-
1. `src/adapters/publishers/{id}.ts` — `PublisherAdapter` インターフェースを実装
|
|
39
|
-
2. `tests/fixtures/{id}-search.html` + `{id}-detail.html` — 実サイトHTMLのスナップショット
|
|
40
|
-
3. `tests/unit/adapters/publishers/{id}.test.ts` — `MockHttpClient` でユニットテスト
|
|
41
|
-
4. `src/adapters/publishers/base.ts` — 必要に応じて `EBOOK_STORE_PATTERNS` に追加
|
|
42
|
-
5. `src/adapters/publishers/registry.ts` — `DEFAULT_PUBLISHERS` に登録
|
|
43
|
-
|
|
44
|
-
## DRM 分類の判断基準
|
|
45
|
-
|
|
46
|
-
新しいストアを `EBOOK_STORE_PATTERNS` に追加する際の判断順:
|
|
47
|
-
1. **free** — 公式が明言、または購入して透かし等がないことを確認済み
|
|
48
|
-
2. **social** — 購入者情報(メールアドレス等)が埋め込まれるが技術的制限なし
|
|
49
|
-
3. **password_pdf** — PDFにパスワードがかかる(標準ビューアで開ける)
|
|
50
|
-
4. **drm** — 専用ビューアーが必要、または上記いずれでもない場合
|
|
51
|
-
|
|
52
|
-
## アーキテクチャ上の制約
|
|
53
|
-
|
|
54
|
-
- ポート (`HttpClient`, `HtmlParser`, `CacheStore`) はインターフェースのみ。実装を直接 import しない
|
|
55
|
-
- `DrmType` に新しい値を追加するときは `src/domain/book.ts` と `src/mcp/server.ts` の両方を更新する
|
|
56
|
-
|
|
57
|
-
## テスト方針
|
|
58
|
-
|
|
59
|
-
テストフレームワークは `node:test` + `node:assert/strict` を使う(vitest は使わない)。
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { describe, it } from "node:test";
|
|
63
|
-
import assert from "node:assert/strict";
|
|
64
|
-
|
|
65
|
-
// 標準的なテストセットアップ
|
|
66
|
-
function makeDeps(http: MockHttpClient) {
|
|
67
|
-
return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// GET のモック(URL前方一致)
|
|
71
|
-
const http = new MockHttpClient()
|
|
72
|
-
.addResponse("https://example.com/search", { status: 200, body: html });
|
|
73
|
-
|
|
74
|
-
// POST のモック (GraphQL等)
|
|
75
|
-
const http = new MockHttpClient()
|
|
76
|
-
.addPostResponse("https://api.example.com/graphql", { status: 200, body: json });
|
|
77
|
-
|
|
78
|
-
// vitest → node:assert の主な対応
|
|
79
|
-
// expect(x).toBe(y) → assert.strictEqual(x, y)
|
|
80
|
-
// expect(x).toEqual(y) → assert.deepStrictEqual(x, y)
|
|
81
|
-
// expect(x).toMatchObject(y) → assert.partialDeepStrictEqual(x, y)
|
|
82
|
-
// expect(x).toHaveLength(n) → assert.strictEqual(x.length, n)
|
|
83
|
-
// expect(x).toContain(s) → assert.ok(x.includes(s))
|
|
84
|
-
// expect(x).toMatch(/r/) → assert.match(x, /r/)
|
|
85
|
-
// await expect(p).rejects.toThrow("msg") → await assert.rejects(p, /msg/)
|
|
86
|
-
|
|
87
|
-
// モック関数: node:test の mock.fn() は Bun 非対応のため、テストファイル内に
|
|
88
|
-
// ローカルで mockFn() ヘルパーを定義して使う(Node.js・Bun・Deno 共通で動作)
|
|
89
|
-
// vi.fn().mockResolvedValue(v) → mockFn(async () => v)
|
|
90
|
-
// fn.toHaveBeenCalledOnce() → assert.strictEqual(fn.mock.callCount(), 1)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## よくある落とし穴
|
|
94
|
-
|
|
95
|
-
- **EUC-JP サイト**: `shop.rutles.net` はクエリを EUC-JP エンコードしないとヒットしない → `iconv-lite` を使用
|
|
96
|
-
- **XSRF-TOKEN**: `techbookfest.org` GraphQL はダブルサブミットCookieパターン必須
|
|
97
|
-
- **Referer 必須**: `maruzen-publishing.co.jp` の検索は Referer なしで 403
|
|
98
|
-
- **機関向けストア除外**: `kw.maruzen.co.jp`(Knowledge Worker)は個人向けではないため除外
|
|
99
|
-
- **ローカルフィルタ型**: `oreilly-japan` と `peaks` は検索APIがなくトップページ/一覧をローカルフィルタ
|
|
100
|
-
- **著者のみ検索不可**: ローカルフィルタ型アダプターは `!query.title` のとき `[]` を返す(HTTP呼ばない)
|
package/deno.json
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { CacheStore } from "../../ports/cache.js";
|
|
2
|
-
|
|
3
|
-
interface CacheEntry {
|
|
4
|
-
value: string;
|
|
5
|
-
expiresAt?: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class MemoryCacheStore implements CacheStore {
|
|
9
|
-
private readonly store = new Map<string, CacheEntry>();
|
|
10
|
-
|
|
11
|
-
async get(key: string): Promise<string | null> {
|
|
12
|
-
const entry = this.store.get(key);
|
|
13
|
-
if (!entry) return null;
|
|
14
|
-
if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
|
|
15
|
-
this.store.delete(key);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
return entry.value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async set(key: string, value: string, ttlSeconds?: number): Promise<void> {
|
|
22
|
-
this.store.set(key, {
|
|
23
|
-
value,
|
|
24
|
-
expiresAt: ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : undefined,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async delete(key: string): Promise<void> {
|
|
29
|
-
this.store.delete(key);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { CacheStore } from "../../ports/cache.js";
|
|
2
|
-
|
|
3
|
-
/** テスト・デバッグ用。キャッシュを一切行わない。 */
|
|
4
|
-
export class NullCacheStore implements CacheStore {
|
|
5
|
-
async get(_key: string): Promise<null> { return null; }
|
|
6
|
-
async set(_key: string, _value: string, _ttlSeconds?: number): Promise<void> {}
|
|
7
|
-
async delete(_key: string): Promise<void> {}
|
|
8
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import * as cheerio from "cheerio";
|
|
2
|
-
import type { Element } from "domhandler";
|
|
3
|
-
import type { HtmlParser, HtmlDocument, HtmlElement } from "../../ports/html-parser.js";
|
|
4
|
-
|
|
5
|
-
class CheerioElement implements HtmlElement {
|
|
6
|
-
constructor(
|
|
7
|
-
private readonly $: cheerio.CheerioAPI,
|
|
8
|
-
private readonly el: Element,
|
|
9
|
-
) {}
|
|
10
|
-
|
|
11
|
-
text(): string {
|
|
12
|
-
return this.$(this.el).text().trim();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
html(): string | null {
|
|
16
|
-
return this.$(this.el).html();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
attr(name: string): string | undefined {
|
|
20
|
-
return this.$(this.el).attr(name);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
find(selector: string): HtmlElement[] {
|
|
24
|
-
return this.$(this.el)
|
|
25
|
-
.find(selector)
|
|
26
|
-
.toArray()
|
|
27
|
-
.map(el => new CheerioElement(this.$, el as Element));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
class CheerioDocument implements HtmlDocument {
|
|
32
|
-
constructor(private readonly $: cheerio.CheerioAPI) {}
|
|
33
|
-
|
|
34
|
-
select(selector: string): HtmlElement[] {
|
|
35
|
-
return this.$(selector)
|
|
36
|
-
.toArray()
|
|
37
|
-
.map(el => new CheerioElement(this.$, el as Element));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
selectOne(selector: string): HtmlElement | null {
|
|
41
|
-
return this.select(selector)[0] ?? null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class CheerioHtmlParser implements HtmlParser {
|
|
46
|
-
parse(html: string): HtmlDocument {
|
|
47
|
-
const $ = cheerio.load(html);
|
|
48
|
-
return new CheerioDocument($);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { HttpClient, RequestOptions, HttpResponse } from "../../ports/http.js";
|
|
2
|
-
|
|
3
|
-
class FetchHttpResponse implements HttpResponse {
|
|
4
|
-
constructor(private readonly response: Response) {}
|
|
5
|
-
|
|
6
|
-
get status(): number {
|
|
7
|
-
return this.response.status;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
get url(): string {
|
|
11
|
-
return this.response.url;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
text(): Promise<string> {
|
|
15
|
-
return this.response.text();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
header(name: string): string | null {
|
|
19
|
-
return this.response.headers.get(name);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class FetchHttpClient implements HttpClient {
|
|
24
|
-
async get(url: string, options?: RequestOptions): Promise<HttpResponse> {
|
|
25
|
-
const init: RequestInit = {
|
|
26
|
-
headers: options?.headers,
|
|
27
|
-
};
|
|
28
|
-
if (options?.timeout !== undefined) {
|
|
29
|
-
init.signal = AbortSignal.timeout(options.timeout);
|
|
30
|
-
}
|
|
31
|
-
const response = await fetch(url, init);
|
|
32
|
-
return new FetchHttpResponse(response);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async post(url: string, body: string, options?: RequestOptions): Promise<HttpResponse> {
|
|
36
|
-
const init: RequestInit = {
|
|
37
|
-
method: "POST",
|
|
38
|
-
body,
|
|
39
|
-
headers: { "Content-Type": "application/json", ...options?.headers },
|
|
40
|
-
};
|
|
41
|
-
if (options?.timeout !== undefined) {
|
|
42
|
-
init.signal = AbortSignal.timeout(options.timeout);
|
|
43
|
-
}
|
|
44
|
-
const response = await fetch(url, init);
|
|
45
|
-
return new FetchHttpResponse(response);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { HttpClient, RequestOptions, HttpResponse } from "../../ports/http.js";
|
|
2
|
-
|
|
3
|
-
export interface MockResponseData {
|
|
4
|
-
status: number;
|
|
5
|
-
body: string;
|
|
6
|
-
headers?: Record<string, string>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
class MockHttpResponse implements HttpResponse {
|
|
10
|
-
constructor(
|
|
11
|
-
private readonly data: MockResponseData,
|
|
12
|
-
private readonly requestUrl: string,
|
|
13
|
-
) {}
|
|
14
|
-
|
|
15
|
-
get status(): number { return this.data.status; }
|
|
16
|
-
get url(): string { return this.requestUrl; }
|
|
17
|
-
async text(): Promise<string> { return this.data.body; }
|
|
18
|
-
header(name: string): string | null {
|
|
19
|
-
return this.data.headers?.[name.toLowerCase()] ?? null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class MockHttpClient implements HttpClient {
|
|
24
|
-
private readonly handlers = new Map<string, MockResponseData>();
|
|
25
|
-
private readonly postHandlers = new Map<string, MockResponseData>();
|
|
26
|
-
private readonly _calls: string[] = [];
|
|
27
|
-
|
|
28
|
-
/** GET: URL の前方一致でレスポンスを登録する */
|
|
29
|
-
addResponse(urlPrefix: string, data: MockResponseData): this {
|
|
30
|
-
this.handlers.set(urlPrefix, data);
|
|
31
|
-
return this;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** POST: URL の前方一致でレスポンスを登録する */
|
|
35
|
-
addPostResponse(urlPrefix: string, data: MockResponseData): this {
|
|
36
|
-
this.postHandlers.set(urlPrefix, data);
|
|
37
|
-
return this;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get calls(): readonly string[] {
|
|
41
|
-
return this._calls;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async get(url: string, _options?: RequestOptions): Promise<HttpResponse> {
|
|
45
|
-
this._calls.push(url);
|
|
46
|
-
|
|
47
|
-
// 完全一致を優先
|
|
48
|
-
if (this.handlers.has(url)) {
|
|
49
|
-
return new MockHttpResponse(this.handlers.get(url)!, url);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 前方一致
|
|
53
|
-
for (const [prefix, data] of this.handlers) {
|
|
54
|
-
if (url.startsWith(prefix)) {
|
|
55
|
-
return new MockHttpResponse(data, url);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw new Error(`MockHttpClient: no handler for GET: ${url}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async post(url: string, _body: string, _options?: RequestOptions): Promise<HttpResponse> {
|
|
63
|
-
this._calls.push(url);
|
|
64
|
-
|
|
65
|
-
if (this.postHandlers.has(url)) {
|
|
66
|
-
return new MockHttpResponse(this.postHandlers.get(url)!, url);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const [prefix, data] of this.postHandlers) {
|
|
70
|
-
if (url.startsWith(prefix)) {
|
|
71
|
-
return new MockHttpResponse(data, url);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
throw new Error(`MockHttpClient: no handler for POST: ${url}`);
|
|
76
|
-
}
|
|
77
|
-
}
|