Prechádzať zdrojové kódy

feat(plugin): 0.7.0 —— books 命令,分类目录不再需要新端点

「长部的复注有哪些」现在一条命令答得出:

    wikipali books --tags dīghanikāya,ṭīkā
    → 5 部(līnatthappakāsanā ×3、sādhuvilāsinī ×2),输出直接接 toc 看章节

实现上没有用功能清单里标 ⬜ 的 tag 端点,而是靠上一个提交扩展的
book-title:整表 281 条一次拉回本地缓存,按 tag 筛选就是本地操作,筛什么
都不用再请求。多个 tag 之间是「且」。--tag-list 列出全部 211 个 tag 及各自
的书数,供发现用。

对旧版服务端的处置:检测返回里有没有 tags 字段,没有就明确报「服务端版本
较旧,分类目录功能尚未上线」并提示换最新版——而不是筛出 0 条让人以为
语料里没有这类书。这个提示在线上实测生效(book-title 的扩展尚未部署)。

规程加了 0a「需要浏览语料结构时:先找书,再看章节」,放在检索之前——
它服务的是「我想知道某类文献有哪些」这种起步阶段,与已有明确关键词的
检索是两种不同的入口。

功能覆盖清单同步:分类目录 ⬜ → ✅,待给 URL 的从 8 项减为 7 项。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 1 týždeň pred
rodič
commit
0c2bce7167

+ 7 - 6
docs/wikipali-feature-coverage.md

@@ -6,9 +6,9 @@
 > 用途:逐项落实的工作清单。**标 ⬜ 的需要提供一个能跑通的完整 API URL**——
 > 用途:逐项落实的工作清单。**标 ⬜ 的需要提供一个能跑通的完整 API URL**——
 > 照着反推参数比读控制器快,也不会猜错。
 > 照着反推参数比读控制器快,也不会猜错。
 >
 >
-> 插件版本基准:**0.6.0**(2026-08-09)
+> 插件版本基准:**0.7.0**(2026-08-09)
 
 
-**当前进度**:✅ 12 项 · ⬜ 10 项(其中 8 项需要 API URL,2 项是写入侧的文章/文集)
+**当前进度**:✅ 13 项 · ⬜ 9 项(其中 7 项需要 API URL,2 项是写入侧的文章/文集)
 
 
 **图例**
 **图例**
 
 
@@ -41,7 +41,7 @@
 
 
 | 功能 | 状态 | 实现 / 端点 | 备注 |
 | 功能 | 状态 | 实现 / 端点 | 备注 |
 |---|---|---|---|
 |---|---|---|---|
-| 分类目录(如「长部的复注有哪些」)| ⬜ | `tag` / `tags-in-chapter` / `tag-map`? | `GET /v2/tag?view=public` → 500。`GET /v2/book-title?view=public` 可用(200)。**缺参数** |
+| 分类目录(如「长部的复注有哪些」)| ✅ | `wikipali books --tags dīghanikāya,ṭīkā` → `GET /v2/book-title` | **服务端已扩展**:book-title 现在返回 toc / tags / related_name 并缓存 24 小时。不再需要 `tag` 端点。⚠ 需要服务端部署这一版 |
 | 某本书的目录 | ✅ | `wikipali toc` → `GET /v2/palitext?view=book-toc&book=&para=` | 返回整套丛书,客户端按 book 过滤 |
 | 某本书的目录 | ✅ | `wikipali toc` → `GET /v2/palitext?view=book-toc&book=&para=` | 返回整套丛书,客户端按 book 过滤 |
 | 章节内容 · 查有哪些版本 | ⚠️ | `wikipali versions` → `GET /v2/channel?view=paragraphs&book_id=&para=` | **只能按段落查**;按章节查目前用章节起始段近似 |
 | 章节内容 · 查有哪些版本 | ⚠️ | `wikipali versions` → `GET /v2/channel?view=paragraphs&book_id=&para=` | **只能按段落查**;按章节查目前用章节起始段近似 |
 | 章节内容 · 读某一版本 | ✅ | `wikipali chapter <坐标> --fetch --channel <uid>` | 先报体量(`chapter_strlen`)再取 |
 | 章节内容 · 读某一版本 | ✅ | `wikipali chapter <坐标> --fetch --channel <uid>` | 先报体量(`chapter_strlen`)再取 |
@@ -87,12 +87,12 @@
 
 
 ---
 ---
 
 
-## 三、需要提供 URL 的清单(共 8 项)
+## 三、需要提供 URL 的清单(共 7 项)
 
 
 按对研究流程的价值排序:
 按对研究流程的价值排序:
 
 
 1. **相似句** `sent-sim` —— 对读与校勘的核心能力,数据量最大(3.6 GB)
 1. **相似句** `sent-sim` —— 对读与校勘的核心能力,数据量最大(3.6 GB)
-2. **分类目录** `tag` 系列 —— 「长部的复注有哪些」这类浏览,是研究的起
+2. ~~**分类目录**~~ —— **已解决**:扩展 `book-title` 返回 tags 即可,不需要新端
 3. **单个术语查询** `system-term/{lang}/{word}` —— 现在只能靠全表缓存过滤
 3. **单个术语查询** `system-term/{lang}/{word}` —— 现在只能靠全表缓存过滤
 4. **相关章节** —— 有了相关段落,章节级对应能省大量往返
 4. **相关章节** —— 有了相关段落,章节级对应能省大量往返
 5. **相关书** —— 同上
 5. **相关书** —— 同上
@@ -109,7 +109,8 @@
 | 版本 | 内容 | 依赖 |
 | 版本 | 内容 | 依赖 |
 |---|---|---|
 |---|---|---|
 | 0.6.0 ✅ | `related`(相关段落)· `articles` / `article` / `anthology`(文章与文集读取)—— **已发布 2026-08-09** | — |
 | 0.6.0 ✅ | `related`(相关段落)· `articles` / `article` / `anthology`(文章与文集读取)—— **已发布 2026-08-09** | — |
-| 待定 | 上面 8 项,收到 URL 后按价值排 | 用户提供 URL |
+| 0.7.0 ✅ | `books`(分类目录,按 tag 找书)—— 配套服务端扩展 `book-title` 的返回 | — |
+| 待定 | 上面 7 项,收到 URL 后按价值排 | 用户提供 URL |
 | 待定 | 按章节聚合分布(`dist --by chapter`),方案见 `wikipali-research-agent-design.md` §3.7 | 方案待定 |
 | 待定 | 按章节聚合分布(`dist --by chapter`),方案见 `wikipali-research-agent-design.md` §3.7 | 方案待定 |
 | 待定 | 短语检索改走 `/v3/search`(OpenSearch),见 §3.6 | v3 调试完成 |
 | 待定 | 短语检索改走 `/v3/search`(OpenSearch),见 §3.6 | v3 调试完成 |
 | 待定 | `versions` 支持按章节查(现在只能按段落,章节用起始段近似) | 可能需要服务端支持 |
 | 待定 | `versions` 支持按章节查(现在只能按段落,章节用起始段近似) | 可能需要服务端支持 |

+ 1 - 1
plugins/wikipali/.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "wikipali",
   "name": "wikipali",
   "description": "WikiPali 巴利三藏平台的客户端:检索与阅读语料做研究(词形展开、全文检索、出处分布、按坐标取原文与译本),以及以 AI 模型身份写入句子。",
   "description": "WikiPali 巴利三藏平台的客户端:检索与阅读语料做研究(词形展开、全文检索、出处分布、按坐标取原文与译本),以及以 AI 模型身份写入句子。",
-  "version": "0.6.0",
+  "version": "0.7.0",
   "author": {
   "author": {
     "name": "visuddhinanda",
     "name": "visuddhinanda",
     "url": "https://github.com/visuddhinanda"
     "url": "https://github.com/visuddhinanda"

+ 9 - 0
plugins/wikipali/lib/cli.py

@@ -131,6 +131,15 @@ def build_parser():
     p.add_argument('--offset', type=int, default=0)
     p.add_argument('--offset', type=int, default=0)
     p.set_defaults(func=cmd_read.cmd_anthology)
     p.set_defaults(func=cmd_read.cmd_anthology)
 
 
+    p = add('books', '分类目录:按 tag 找书,如「长部的复注有哪些」')
+    p.add_argument('keyword', nargs='?', help='按书名/toc 过滤')
+    p.add_argument('--tags', help='按 tag 筛,逗号分隔是**且**,如 dīghanikāya,ṭīkā')
+    p.add_argument('--tag-list', action='store_true', help='列出全部 tag 及各自的书数')
+    p.add_argument('--show-tags', action='store_true', help='每本书都列出它的 tag')
+    p.add_argument('--limit', type=int, default=40)
+    p.add_argument('--refresh', action='store_true', help='强制重新拉取书目清单(有本地缓存)')
+    p.set_defaults(func=cmd_read.cmd_books)
+
     # -- 写 ----------------------------------------------------------------
     # -- 写 ----------------------------------------------------------------
     p = add('ensure-model', '幂等地建立模型记录并取模型身份 token', needs_json=False)
     p = add('ensure-model', '幂等地建立模型记录并取模型身份 token', needs_json=False)
     p.add_argument('--name', help='模型标识,如 claude-opus-5(会成为句子作者署名)')
     p.add_argument('--name', help='模型标识,如 claude-opus-5(会成为句子作者署名)')

+ 94 - 0
plugins/wikipali/lib/cmd_read.py

@@ -699,3 +699,97 @@ def cmd_anthology(args):
 
 
     emit(args, rows, render)
     emit(args, rows, render)
     return 0
     return 0
+
+
+# ---------------------------------------------------------------------------
+# books —— 分类目录:按 tag 找书
+# ---------------------------------------------------------------------------
+
+
+def books_cache_path():
+    import os
+    from creds import CREDS_DIR
+    return os.path.join(CREDS_DIR, 'cache', 'book-titles.json')
+
+
+def fetch_books(client, refresh=False):
+    """书目清单整表拉一次缓存在本地。服务端也缓存 24 小时,这里再缓存一层是为了
+    让按 tag 筛选变成本地操作——281 条全量在手,筛什么都不用再请求。"""
+    import os
+    path = books_cache_path()
+    if os.path.exists(path) and not refresh:
+        try:
+            with open(path, encoding='utf-8') as fh:
+                return json.load(fh)
+        except (OSError, ValueError):
+            pass
+    try:
+        data = client.call('GET', 'v2/book-title', timeout=READ_TIMEOUT)
+    except ApiError as exc:
+        raise explain_api_error(exc, '取书目清单')
+    rows = (data or {}).get('rows') or []
+    if rows and 'tags' not in rows[0]:
+        raise WpError(
+            '该站点返回的书目清单里没有 tags/toc 字段——服务端版本较旧,'
+            '分类目录功能尚未上线。\n'
+            '可以换最新版试试:wikipali --api next books …'
+        )
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    with open(path, 'w', encoding='utf-8') as fh:
+        json.dump(rows, fh, ensure_ascii=False)
+    return rows
+
+
+def cmd_books(args):
+    client = make_client(args)
+    rows = fetch_books(client, refresh=args.refresh)
+
+    if args.tag_list:
+        counter = {}
+        for r in rows:
+            for t in r.get('tags') or []:
+                counter[t] = counter.get(t, 0) + 1
+
+        def render_tags():
+            print(f'{len(counter)} 个 tag(后面是有该 tag 的书数):\n')
+            for name, n in sorted(counter.items(), key=lambda kv: (-kv[1], kv[0]))[: args.limit]:
+                print(f'  {n:>4}  {name}')
+            print('\n多个 tag 用逗号连接是**且**的关系:'
+                  'wikipali books --tags dīghanikāya,ṭīkā')
+        emit(args, counter, render_tags)
+        return 0
+
+    hits = rows
+    if args.tags:
+        want = [t.strip() for t in args.tags.split(',') if t.strip()]
+        hits = [r for r in hits if all(t in (r.get('tags') or []) for t in want)]
+    if args.keyword:
+        kw = args.keyword.lower()
+        hits = [r for r in hits
+                if kw in str(r.get('title', '')).lower() or kw in str(r.get('toc', '')).lower()]
+
+    def render():
+        scope = []
+        if args.tags:
+            scope.append(f'tags={args.tags}')
+        if args.keyword:
+            scope.append(f'关键词={args.keyword}')
+        print(f'{len(hits)} 部书(全部 {len(rows)} 部)'
+              + (f'  [{" ".join(scope)}]' if scope else ''))
+        if not hits:
+            print('\n没有匹配的书。用 --tag-list 看有哪些 tag;多个 tag 之间是「且」。')
+            return
+        print()
+        for r in hits[: args.limit]:
+            cs = f'  {r["related_name"]}' if r.get('related_name') else ''
+            print(f'  {r.get("book")}:{r.get("paragraph"):<6} {str(r.get("toc"))[:38]:<40}{cs}')
+            if args.show_tags:
+                print(f'      {" ".join(r.get("tags") or [])}')
+        if len(hits) > args.limit:
+            print(f'  …… 其余 {len(hits) - args.limit} 部(--limit 调整)')
+        if hits:
+            first = hits[0]
+            print(f'\n看某本书的章节:wikipali toc {first.get("book")}:{first.get("paragraph")}')
+
+    emit(args, hits, render)
+    return 0

+ 15 - 1
plugins/wikipali/skills/research/SKILL.md

@@ -28,7 +28,21 @@ skill 共用的规矩,必须遵守。** 端点细节见 `references/api-read.m
 
 
 ## 流程
 ## 流程
 
 
-### 0. 先看有没有人写过
+### 0a. 需要浏览语料结构时:先找书,再看章节
+
+```bash
+wikipali books --tag-list                      # 有哪些分类 tag
+wikipali books --tags dīghanikāya,ṭīkā         # 长部的复注有哪些(多个 tag 是「且」)
+wikipali toc 185:3                             # 那本书的章节目录
+```
+
+`books` 按 tag 筛(`mūla` / `aṭṭhakathā` / `ṭīkā` / 各部尼柯耶 / 各种论书),
+输出直接接 `toc` 看章节。适合「我想知道某类文献有哪些」这种起步阶段,
+而不是已经有明确关键词的检索。
+
+⚠ 该功能需要服务端较新版本;旧版会明确报「分类目录功能尚未上线」。
+
+### 0b. 先看有没有人写过
 
 
 ```bash
 ```bash
 wikipali articles 别住          # 平台上的二手研究
 wikipali articles 别住          # 平台上的二手研究