Skill: yszheda/skill-openclaw-library-monitor (version 1.0.1)

--- name: library-book-monitor description: Monitor library book availability and get notified when books become available for borrowing. Supports Shenzhen Library and extensible for other libraries. version: 1.0.0 author: openclaw-community license: MIT tools: - bash - read - write - web_search - web_fetch triggers: - library - book monitor - 图书馆 - book availability - 借书 - 书籍监控 permissions: - network - filesystem - shell entryPoint: type: natural prompt: | You are a Library Book Monitor skill. You help users track book availability at libraries and get notified when books become available for borrowing. Capabilities: - Add books to monitoring list - Check book availability at Shenzhen Library - List all monitored books - Remove books from list - Toggle monitoring status - Start scheduled monitoring When a user asks to: - Add a book: Use bash to run "python main.py add --title <title> [--author <author>] [--monitor]" - List books: Use bash to run "python main.py list" - Check books: Use bash to run "python main.py check" - Remove a book: Use bash to run "python main.py remove <book_id>" - Toggle monitoring: Use bash to run "python main.py toggle <book_id>" - Start monitoring: Use bash to run "python main.py monitor" --- # Library Book Monitor Skill A skill to monitor library book availability and get notified when books become available for borrowing. ## When to Use Use this skill when you want to: - Track book availability at Shenzhen Library - Get notified when a book becomes available for borrowing - Monitor multiple books and their availability status - Automate library book availability checking ## Setup Before using this skill, ensure: 1. Python 3.8+ is installed 2. Dependencies are installed: `pip install -r requirements.txt` 3. Configuration file `config.yaml` is set up (copy from `config.yaml.example`) ## Commands ### Add a Book ``` Add book "Python Programming" by "Guido van Rossum" with monitoring enabled ``` ### List All Books ``` List all monitored books ``` ### Check Book Availability ``` Check availability of all books ``` ### Remove a Book ``` Remove book with ID abc123 ``` ### Toggle Monitoring ``` Toggle monitoring for book abc123 ``` ### Start Scheduled Monitoring ``` Start monitoring scheduler ``` ## Configuration The skill uses `config.yaml` for settings: ```yaml library: name: "Shenzhen Library" base_url: "https://www.szlib.org.cn/" search_url: "https://www.szlib.org.cn/opac/" timeout: 30 scheduler: enabled: true interval_minutes: 30 retry_on_failure: true max_retries: 3 notification: enabled: true methods: - console - email email: smtp_server: smtp.qq.com smtp_port: 587 sender: [REDACTED_SECRET] [REDACTED_SECRET] recipients: - [REDACTED_SECRET] ``` ## Notes - The skill supports Shenzhen Library by default - Other libraries can be added by implementing custom scrapers - Book data is stored in `book_list.json` - Please set reasonable check intervals to avoid overloading library servers

path: book_manager.py
content: import json
from pathlib import Path
from typing import List, Dict, Optional, Tuple


class Book:
    """书籍类"""

    def __init__(
        self,
        title: str = "",
        isbn: str = "",
        author: str = "",
        book_id: Optional[str] = None,
    ):
        self.book_id = book_id or self._generate_id()
        self.title = title
        self.isbn = isbn
        self.author = author
        self.available: bool = False
        self.locations: List[str] = []
        self.last_checked: Optional[str] = None
        self.monitor: bool = False

    def _generate_id(self) -> str:
        """生成书籍ID"""
        import uuid

        return str(uuid.uuid4())[:8]

    def to_dict(self) -> dict:
        """转换为字典"""
        return {
            "book_id": self.book_id,
            "title": self.title,
            "isbn": self.isbn,
            "author": self.author,
            "available": self.available,
            "locations": self.locations,
            "last_checked": self.last_checked,
            "monitor": self.monitor,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Book":
        """从字典创建书籍对象"""
        book = cls(
            title=data.get("title", ""),
            isbn=data.get("isbn", ""),
            author=data.get("author", ""),
            book_id=data.get("book_id"),
        )
        book.available = data.get("available", False)
        book.locations = data.get("locations", [])
        book.last_checked = data.get("last_checked")
        book.monitor = data.get("monitor", False)
        return book


class BookManager:
    """书单管理器"""

    def __init__(self, file_path: str = "book_list.json"):
        self.file_path = Path(file_path)
        self.books: Dict[str, Book] = {}
        self._load_books()

    def _load_books(self):
        """加载书单"""
        if self.file_path.exists():
            try:
                with open(self.file_path, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    for book_data in data.get("books", []):
                        book = Book.from_dict(book_data)
                        self.books[book.book_id] = book
            except (json.JSONDecodeError, KeyError):
                pass

    def _save_books(self):
        """保存书单"""
        data = {"books": [book.to_dict() for book in self.books.values()]}
        with open(self.file_path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

    def add_book(self, book: Book) -> Tuple[str, bool]:
        """添加书籍，已存在则更新
        返回: (book_id, is_new)
        """
        # 检查是否已存在相同书籍（通过ISBN或标题+作者）
        existing_book = self._find_existing_book(book)
        if existing_book:
            # 更新现有书籍
            existing_book.title = book.title or existing_book.title
            existing_book.isbn = book.isbn or existing_book.isbn
            existing_book.author = book.author or existing_book.author
            existing_book.monitor = book.monitor  # 监控状态按新的设置
            self._save_books()
            return existing_book.book_id, False  # 不是新的

        # 添加新书籍
        self.books[book.book_id] = book
        self._save_books()
        return book.book_id, True  # 是新的

    def _find_existing_book(self, book: Book) -> Optional[Book]:
        """查找是否已存在相同书籍"""
        for existing_book in self.books.values():
            # 如果有ISBN，优先用ISBN匹配
            if book.isbn and existing_book.isbn == book.isbn:
                return existing_book
            # 否则用标题匹配
            if book.title and existing_book.title == book.title:
                return existing_book
        return None

    def remove_book(self, book_id: str) -> bool:
        """移除书籍"""
        if book_id in self.books:
            del self.books[book_id]
            self._save_books()
            return True
        return False

    def get_book(self, book_id: str) -> Optional[Book]:
        """获取书籍"""
        return self.books.get(book_id)

    def get_all_books(self) -> List[Book]:
        """获取所有书籍"""
        return list(self.books.values())

    def get_monitoring_books(self) -> List[Book]:
        """获取正在监控的书籍"""
        return [book for book in self.books.values() if book.monitor]

    def update_book(self, book: Book):
        """更新书籍信息"""
        if book.book_id in self.books:
            self.books[book.book_id] = book
            self._save_books()

    def toggle_monitor(self, book_id: str) -> Optional[bool]:
        """切换监控状态"""
        book = self.get_book(book_id)
        if book:
            book.monitor = not book.monitor
            self._save_books()
            return book.monitor
        return None


path: config.py
content: import yaml
import os
from pathlib import Path


class Config:
    """配置管理类"""

    def __init__(self, config_file: str = "config.yaml"):
        self.config_file = Path(config_file)
        self.config = self._load_config()

    def _load_config(self) -> dict:
        """加载配置文件"""
        if not self.config_file.exists():
            raise FileNotFoundError(f"配置文件不存在: {self.config_file}")

        with open(self.config_file, "r", encoding="utf-8") as f:
            return yaml.safe_load(f)

    def get(self, key: str, default=None):
        """获取配置项"""
        keys = key.split(".")
        value = self.config

        for k in keys:
            if isinstance(value, dict) and k in value:
                value = value[k]
            else:
                return default

        return value

    @property
    def library(self) -> dict:
        """图书馆配置"""
        return self.get("library", {})

    @property
    def scheduler(self) -> dict:
        """定时任务配置"""
        return self.get("scheduler", {})

    @property
    def notification(self) -> dict:
        """提醒配置"""
        return self.get("notification", {})

    @property
    def storage(self) -> dict:
        """存储配置"""
        return self.get("storage", {})


path: library_scraper.py
content: import requests
from typing import List, Optional, Tuple
from datetime import datetime
from urllib.parse import quote
from book_manager import Book


class LibraryScraper:
    """图书馆网站爬虫基类"""

    def __init__(self, config: dict):
        self.config = config
        self.base_url = config.get("base_url", "")
        self.search_url = config.get("search_url", "")
        self.timeout = config.get("timeout", 30)
        self.session = requests.Session()
        self.session.headers.update(
            {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                "Accept": "application/json, text/plain, */*",
                "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
                "Referer": "https://www.szlib.org.cn/opac/",
            }
        )
        # 先访问搜索页面建立 session
        self._init_session()

    def _init_session(self):
        """初始化 session，访问搜索页面"""
        try:
            self.session.get("https://www.szlib.org.cn/opac/", timeout=self.timeout)
        except Exception as e:
            print(f"初始化 session 失败: {e}")

    def search_book(self, book: Book) -> Tuple[bool, List[str], str]:
        """
        搜索书籍并返回可借状态和馆藏位置
        返回: (是否可借, 馆藏位置列表, 最后检查时间)
        """
        raise NotImplementedError("子类必须实现 search_book 方法")

    def update_book_status(self, book: Book) -> Tuple[Book, bool]:
        """更新书籍状态"""
        available, locations, check_time = self.search_book(book)
        status_changed = book.available != available
        book.available = available
        book.locations = locations
        book.last_checked = check_time
        return book, status_changed


class ShenzhenLibraryScraper(LibraryScraper):
    """深圳图书馆爬虫"""

    def __init__(self, config: dict):
        super().__init__(config)
        self.api_base = "https://www.szlib.org.cn/api/opacservice"

    def search_book(self, book: Book) -> Tuple[bool, List[str], str]:
        """搜索深圳图书馆书籍"""
        check_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        keyword = book.isbn or book.title or f"{book.author} {book.title}"

        if not keyword:
            return False, [], check_time

        try:
            print(f"正在搜索: {keyword}")

            # 第一步：搜索书籍
            record_id, table_name = self._search_book_by_keyword(keyword)
            if not record_id:
                print(f"未找到书籍: {keyword}")
                return False, [], check_time

            print(f"找到书籍，recordId: {record_id}, tableName: {table_name}")

            # 第二步：获取馆藏信息
            locations = self._get_holding_info(record_id, table_name)
            available = len(locations) > 0

            return available, locations, check_time

        except requests.RequestException as e:
            print(f"搜索失败: {e}")
            return False, [], check_time

    def _search_book_by_keyword(
        self, keyword: str
    ) -> Tuple[Optional[str], Optional[str]]:
        """通过关键词搜索书籍"""
        try:
            encoded_keyword = quote(keyword)
            url = (
                f"{self.api_base}/getQueryResult?"
                f"v_index=title&"
                f"v_value={encoded_keyword}&"
                f"library=all&"
                f"v_tablearray=bibliosm,serbibm,apabibibm,mmbibm,&"
                f"cirtype=&"
                f"sortfield=ptitle&"
                f"sorttype=desc&"
                f"pageNum=10&"
                f"v_page=1&"
                f"v_startpubyear=&"
                f"v_endpubyear=&"
                f"v_secondquery=&"
                f"client_id=t1"
            )

            response = self.session.get(url, timeout=self.timeout)
            response.raise_for_status()
            data = response.json()

            if data.get("data"):
                docs = data["data"].get("docs", [])
                if docs:
                    first_doc = docs[0]
                    record_id = first_doc.get("recordid")
                    table_name = first_doc.get("tablename")
                    return str(record_id) if record_id else None, table_name

            return None, None

        except Exception as e:
            print(f"搜索书籍时出错: {e}")
            return None, None

    def _get_holding_info(self, record_id: str, table_name: str) -> List[str]:
        """获取馆藏信息"""
        locations = []
        try:
            url = f"{self.api_base}/getpreholding"
            params = {
                "metaTable": table_name,
                "metaId": record_id,
                "library": "all",
                "client_id": "t1",
            }

            response = self.session.get(url, params=params, timeout=self.timeout)
            response.raise_for_status()
            data = response.json()

            can_loan_list = data.get("CanLoanBook", [])
            for loan_info in can_loan_list:
                record_list = loan_info.get("recordList", [])
                for record in record_list:
                    location = record.get("local", "")
                    if location and location not in locations:
                        locations.append(location)

            return locations

        except Exception as e:
            print(f"获取馆藏信息时出错: {e}")
            return locations


path: main.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
图书馆借书查询系统
- 查询书籍可借状态
- 定时监控不可借书籍
- 可借时提醒用户
"""

import sys
import io
import argparse

# 修复 Windows 控制台编码问题
if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")

from config import Config
from book_manager import BookManager, Book
from scheduler import BookMonitor


def add_book(
    config: Config,
    title: str = "",
    isbn: str = "",
    author: str = "",
    monitor: bool = False,
):
    """添加书籍"""
    book_manager = BookManager(config.storage.get("book_list_file", "book_list.json"))

    book = Book(title=title, isbn=isbn, author=author)
    book.monitor = monitor

    book_id, is_new = book_manager.add_book(book)
    if is_new:
        print(f"[OK] 已添加书籍: {book.title}")
    else:
        print(f"[OK] 书籍已存在，已更新: {book.title}")
    print(f"   书籍ID: {book_id}")
    print(f"   监控状态: {'开启' if monitor else '关闭'}")


def list_books(config: Config):
    """列出所有书籍"""
    book_manager = BookManager(config.storage.get("book_list_file", "book_list.json"))
    books = book_manager.get_all_books()

    if not books:
        print("书单为空")
        return

    print(f"\n书单 ({len(books)} 本书):")
    print("-" * 80)

    for i, book in enumerate(books, 1):
        status = "[可借]" if book.available else "[不可借]"
        monitor = "[监控中]" if book.monitor else "       "

        print(f"\n{i}. {monitor} {status} {book.title}")
        print(f"   ID: {book.book_id}")
        if book.author:
            print(f"   作者: {book.author}")
        if book.isbn:
            print(f"   ISBN: {book.isbn}")
        if book.locations:
            print(f"   馆藏位置 ({len(book.locations)} 处):")
            for j, loc in enumerate(book.locations, 1):
                print(f"     {j}. {loc}")
        if book.last_checked:
            print(f"   最后检查: {book.last_checked}")
    print("-" * 80)


def remove_book(config: Config, book_id: str):
    """移除书籍"""
    book_manager = BookManager(config.storage.get("book_list_file", "book_list.json"))

    if book_manager.remove_book(book_id):
        print(f"[OK] 已移除书籍: {book_id}")
    else:
        print(f"[ERROR] 未找到书籍: {book_id}")


def toggle_monitor(config: Config, book_id: str):
    """切换监控状态"""
    book_manager = BookManager(config.storage.get("book_list_file", "book_list.json"))
    status = book_manager.toggle_monitor(book_id)

    if status is not None:
        book = book_manager.get_book(book_id)
        print(f"[OK] {book.title} 监控状态: {'开启' if status else '关闭'}")
    else:
        print(f"[ERROR] 未找到书籍: {book_id}")


def check_now(config: Config):
    """立即检查所有书籍"""
    monitor = BookMonitor(config)
    monitor.check_all_books()


def start_monitor(config: Config):
    """启动监控"""
    monitor = BookMonitor(config)
    monitor.start()


def main():
    parser = argparse.ArgumentParser(
        description="图书馆借书查询系统",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  python main.py add --title "Python编程" --author "Guido" --monitor
  python main.py list
  python main.py check
  python main.py monitor
        """,
    )

    subparsers = parser.add_subparsers(title="命令", dest="command")

    # 添加书籍
    add_parser = subparsers.add_parser("add", help="添加书籍")
    add_parser.add_argument("--title", "-t", default="", help="书名")
    add_parser.add_argument("--isbn", "-i", default="", help="ISBN")
    add_parser.add_argument("--author", "-a", default="", help="作者")
    add_parser.add_argument("--monitor", "-m", action="store_true", help="开启监控")

    # 列出书籍
    subparsers.add_parser("list", help="列出所有书籍")

    # 移除书籍
    remove_parser = subparsers.add_parser("remove", help="移除书籍")
    remove_parser.add_argument("book_id", help="书籍ID")

    # 切换监控
    monitor_parser = subparsers.add_parser("toggle", help="切换书籍监控状态")
    monitor_parser.add_argument("book_id", help="书籍ID")

    # 立即检查
    subparsers.add_parser("check", help="立即检查所有书籍状态")

    # 启动监控
    subparsers.add_parser("monitor", help="启动定时监控")

    args = parser.parse_args()

    # 加载配置
    try:
        config = Config()
    except FileNotFoundError as e:
        print(f"[ERROR] 错误: {e}")
        print("请复制 config.yaml.example 到 config.yaml 并修改配置")
        sys.exit(1)

    # 执行命令
    if args.command == "add":
        if not any([args.title, args.isbn, args.author]):
            print("[ERROR] 请至少提供书名、ISBN或作者之一")
            sys.exit(1)
        add_book(config, args.title, args.isbn, args.author, args.monitor)

    elif args.command == "list":
        list_books(config)

    elif args.command == "remove":
        remove_book(config, args.book_id)

    elif args.command == "toggle":
        toggle_monitor(config, args.book_id)

    elif args.command == "check":
        check_now(config)

    elif args.command == "monitor":
        start_monitor(config)

    else:
        parser.print_help()


if __name__ == "__main__":
    main()


path: notifier.py
content: from typing import List
from book_manager import Book


class Notifier:
    """提醒器基类"""

    def send(self, message: str):
        """发送提醒"""
        raise NotImplementedError("子类必须实现 send 方法")

    def send_book_available(self, book: Book):
        """发送书籍可借提醒"""
        message = self._format_available_message(book)
        self.send(message)

    def _format_available_message(self, book: Book) -> str:
        """格式化书籍可借消息"""
        locations_str = (
            "\n  - ".join(book.locations) if book.locations else "暂无位置信息"
        )
        return f"""
[书籍可借提醒]

书名: {book.title}
作者: {book.author}
ISBN: {book.isbn}
可借状态: [可借]
馆藏位置:
  - {locations_str}

查询时间: {book.last_checked}
""".strip()


class ConsoleNotifier(Notifier):
    """控制台提醒器"""

    def send(self, message: str):
        print("\n" + "=" * 50)
        print(message)
        print("=" * 50 + "\n")


class EmailNotifier(Notifier):
    """邮件提醒器"""

    def __init__(self, config: dict):
        self.config = config
        self.smtp_server = config.get("smtp_server", "")
        self.smtp_port = config.get("smtp_port", 587)
        self.sender = config.get("sender", "")
        self.[REDACTED_SECRET]"password", "")
        self.recipients = config.get("recipients", [])

    def send(self, message: str):
        if not all([self.smtp_server, self.sender, self.password, self.recipients]):
            print("邮件配置不完整，跳过邮件发送")
            return

        try:
            import smtplib
            from email.mime.text import MIMEText
            from email.mime.multipart import MIMEMultipart

            msg = MIMEMultipart()
            msg["From"] = self.sender
            msg["To"] = ", ".join(self.recipients)
            msg["Subject"] = "[图书馆] 书籍可借提醒"

            msg.attach(MIMEText(message, "plain", "utf-8"))

            with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
                server.starttls()
                server.login(self.sender, self.password)
                server.send_message(msg)

            print("邮件提醒发送成功")
        except Exception as e:
            print(f"邮件发送失败: {e}")


class NotificationManager:
    """提醒管理器"""

    def __init__(self, config: dict):
        self.config = config
        self.notifiers: List[Notifier] = []
        self._init_notifiers()

    def _init_notifiers(self):
        """初始化提醒器"""
        methods = self.config.get("methods", ["console"])

        if "console" in methods:
            self.notifiers.append(ConsoleNotifier())

        if "email" in methods and "email" in self.config:
            self.notifiers.append(EmailNotifier(self.config["email"]))

    def send(self, message: str):
        """通过所有渠道发送提醒"""
        for notifier in self.notifiers:
            notifier.send(message)

    def send_book_available(self, book: Book):
        """发送书籍可借提醒"""
        for notifier in self.notifiers:
            notifier.send_book_available(book)


path: package.json
content: {
  "name": "@openclaw-community/skill-library-book-monitor",
  "version": "1.0.0",
  "description": "Monitor library book availability and get notified when books become available for borrowing. Supports Shenzhen Library and extensible for other libraries.",
  "keywords": [
    "openclaw",
    "skill",
    "library",
    "book",
    "monitor",
    "availability",
    "shenzhen-library",
    "notification",
    "python"
  ],
  "author": "openclaw-community",
  "license": "MIT",
  "homepage": "https://github.com/openclaw-community/skill-library-book-monitor",
  "repository": {
    "type": "git",
    "url": "https://github.com/openclaw-community/skill-library-book-monitor.git"
  },
  "bugs": {
    "url": "https://github.com/openclaw-community/skill-library-book-monitor/issues"
  },
  "files": [
    "main.py",
    "requirements.txt",
    "__init__.py",
    "config.py",
    "book_manager.py",
    "library_scraper.py",
    "notifier.py",
    "scheduler.py",
    "config.yaml.example",
    "SKILL.md",
    "README.md",
    "LICENSE"
  ],
  "engines": {
    "node": ">=18.0.0"
  },
  "publishConfig": {
    "access": "public"
  },
  "openclaw": {
    "skill": true,
    "minVersion": "1.0.0",
    "category": "productivity",
    "triggers": [
      "library",
      "book monitor",
      "图书馆",
      "book availability",
      "借书",
      "书籍监控"
    ]
  }
}

path: README.md
content: # Library Book Monitor Skill

An OpenClaw skill to monitor library book availability and get notified when books become available for borrowing.

## Features

- 📚 **Book Management** - Add, remove, and track books you want to borrow
- 🔍 **Availability Check** - Query book availability at Shenzhen Library
- ⏰ **Scheduled Monitoring** - Automatically check book status on a schedule
- 🔔 **Smart Notifications** - Get notified when books become available
- ⚙️ **Configurable** - Support for different library configurations

## Installation

```bash
npx clawhub@latest install openclaw-community/skill-library-book-monitor
```

Or install via npm:

```bash
npm install -g @openclaw-community/skill-library-book-monitor
```

## Prerequisites

Before using this skill, ensure you have:

1. Python 3.8+ installed
2. Required Python dependencies:
   ```bash
   pip install requests pyyaml
   ```
3. Configuration file set up (see Configuration section)

## Usage

### Adding a Book

```
Add book "Python Programming" by "Guido van Rossum" with monitoring enabled
```

### Listing All Books

```
List all monitored books
```

### Checking Availability

```
Check availability of all books
```

### Removing a Book

```
Remove book with ID abc123
```

### Toggling Monitoring

```
Toggle monitoring for book abc123
```

### Starting Scheduled Monitoring

```
Start monitoring scheduler
```

## Configuration

Create a `config.yaml` file in your project directory:

```yaml
library:
  name: "Shenzhen Library"
  base_url: "https://www.szlib.org.cn/"
  search_url: "https://www.szlib.org.cn/opac/"
  timeout: 30

scheduler:
  enabled: true
  interval_minutes: 30
  retry_on_failure: true
  max_retries: 3

notification:
  enabled: true
  methods:
    - console
    - email
  email:
    smtp_server: smtp.qq.com
    smtp_port: 587
    sender: [REDACTED_SECRET]
    [REDACTED_SECRET]
    recipients:
      - [REDACTED_SECRET]
```

## How It Works

1. **Book Addition**: When you add a book, the skill stores the book information and searches the library catalog
2. **Availability Check**: The skill queries the library's API to check if the book is available for borrowing
3. **Location Tracking**: If available, the skill records which library locations have copies
4. **Scheduled Monitoring**: The skill can automatically recheck availability on a schedule
5. **Notifications**: When a book becomes available, the skill sends notifications via configured channels

## Extending to Other Libraries

The skill is designed to be extensible. To add support for a new library:

1. Create a new scraper class inheriting from `LibraryScraper`
2. Implement the `search_book` method
3. Add the new scraper to the configuration

Example:

```python
class MyLibraryScraper(LibraryScraper):
    def search_book(self, book: Book) -> Tuple[bool, List[str], str]:
        # Implement library-specific search logic
        pass
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License - see the LICENSE file for details.

## Acknowledgments

- OpenClaw community for the skill framework
- Shenzhen Library for providing the API
- Contributors and users of this skill

## Support

If you encounter any issues or have questions:

1. Check the [Issues](https://github.com/openclaw-community/skill-library-book-monitor/issues) page
2. Join the [OpenClaw Discord](https://discord.gg/openclaw)
3. Submit a new issue with details about your problem

---

**Happy Reading!** 📚

path: requirements.txt
content: requests>=2.31.0
beautifulsoup4>=4.12.0
lxml>=4.9.0
schedule>=1.2.0
python-dotenv>=1.0.0
pyyaml>=6.0


path: scheduler.py
content: import schedule
import time
from typing import Optional
from datetime import datetime
from config import Config
from book_manager import BookManager, Book
from library_scraper import LibraryScraper, ShenzhenLibraryScraper
from notifier import NotificationManager


class BookMonitor:
    """书籍监控器"""

    def __init__(self, config: Config):
        self.config = config
        self.book_manager = BookManager(
            config.storage.get("book_list_file", "book_list.json")
        )
        self.scraper = self._init_scraper()
        self.notifier = NotificationManager(config.notification)
        self.running = False

    def _init_scraper(self) -> LibraryScraper:
        """初始化爬虫"""
        library_name = self.config.library.get("name", "").lower()

        if "深圳" in library_name or "shenzhen" in library_name:
            return ShenzhenLibraryScraper(self.config.library)
        else:
            # 默认使用深圳图书馆爬虫
            print(f"使用默认爬虫: 深圳图书馆")
            return ShenzhenLibraryScraper(self.config.library)

    def check_all_books(self):
        """检查所有书籍状态"""
        print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始检查书籍状态...")

        books = self.book_manager.get_all_books()
        if not books:
            print("书单为空，跳过检查")
            return

        updated_count = 0
        available_count = 0

        for book in books:
            try:
                updated_book, status_changed = self.scraper.update_book_status(book)
                self.book_manager.update_book(updated_book)

                # 显示详细信息
                status = "[可借]" if updated_book.available else "[不可借]"
                print(f"\n  {updated_book.title} {status}")
                if updated_book.author:
                    print(f"    作者: {updated_book.author}")
                if updated_book.isbn:
                    print(f"    ISBN: {updated_book.isbn}")
                if updated_book.locations:
                    print(f"    馆藏位置 ({len(updated_book.locations)} 处):")
                    for i, loc in enumerate(updated_book.locations, 1):
                        print(f"      {i}. {loc}")
                if updated_book.last_checked:
                    print(f"    最后检查: {updated_book.last_checked}")

                if status_changed and updated_book.available:
                    # 状态变为可借，发送提醒
                    print(f"\n    [通知] {updated_book.title} 现在可借了！")
                    if updated_book.monitor:
                        self.notifier.send_book_available(updated_book)
                        available_count += 1

                updated_count += 1

            except Exception as e:
                print(f"\n  - 检查 {book.title} 时出错: {e}")

        print(f"\n检查完成！更新了 {updated_count} 本书，{available_count} 本新书可借")

    def check_monitoring_books(self):
        """仅检查正在监控的书籍"""
        print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 检查监控中的书籍...")

        books = self.book_manager.get_monitoring_books()
        if not books:
            print("没有正在监控的书籍")
            return

        for book in books:
            try:
                updated_book, status_changed = self.scraper.update_book_status(book)
                self.book_manager.update_book(updated_book)

                if status_changed and updated_book.available:
                    print(f"\n[通知] {updated_book.title} 现在可借了！")
                    self.notifier.send_book_available(updated_book)

            except Exception as e:
                print(f"\n检查 {book.title} 时出错: {e}")

    def start(self):
        """启动定时任务"""
        if not self.config.scheduler.get("enabled", True):
            print("定时任务未启用")
            return

        interval = self.config.scheduler.get("interval_minutes", 30)
        print(f"启动定时监控，每 {interval} 分钟检查一次...")

        # 先立即执行一次
        self.check_all_books()

        # 设置定时任务
        schedule.every(interval).minutes.do(self.check_monitoring_books)

        self.running = True
        try:
            while self.running:
                schedule.run_pending()
                time.sleep(60)
        except KeyboardInterrupt:
            print("\n监控已停止")

    def stop(self):
        """停止监控"""
        self.running = False


path: _meta.json
content: {
  "name": "library-book-monitor",
  "version": "1.0.0",
  "description": "Monitor library book availability and get notified when books become available for borrowing",
  "author": "openclaw-community",
  "category": "productivity",
  "license": "MIT",
  "keywords": [
    "library",
    "book",
    "monitor",
    "availability",
    "shenzhen-library",
    "notification"
  ],
  "triggers": [
    "library",
    "book monitor",
    "图书馆",
    "book availability",
    "借书",
    "书籍监控"
  ],
  "tools": [
    "bash",
    "read",
    "write",
    "web_search",
    "web_fetch"
  ],
  "permissions": [
    "network",
    "filesystem",
    "shell"
  ],
  "minOpenclawVersion": "1.0.0",
  "documentation": {
    "readme": "README.md",
    "skill": "SKILL.md",
    "license": "LICENSE"
  }
}

path: __init__.py
content: """OpenClaw Library Book Monitor Skill

图书馆图书借阅监控技能
"""

__version__ = "1.0.0"
__author__ = "OpenClaw"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)