This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw-skills/skills/obsidian-kb/src/config.py
T
OpenClaw Agent a69225ff2b 知乎提取器:从 Playwright 迁移到 DrissionPage 方案
- 使用 DrissionPage 替代 Playwright
- 实现 Cookie 管理和反爬绕过
- 支持回答/文章/问题三种内容类型
- 灵活配置(文件/环境变量/配置文件)
- 更新文档和配置文件
2026-02-08 05:56:27 +08:00

354 lines
11 KiB
Python

"""
Configuration management for Obsidian KB Skill.
"""
import os
import yaml
from pathlib import Path
from typing import Optional, Dict, Any
# Global config instance
_config_instance: Optional['Config'] = None
def get_config(config_path: Optional[str] = None) -> 'Config':
"""Get global config instance."""
global _config_instance
if _config_instance is None or config_path:
_config_instance = Config(config_path)
return _config_instance
class Config:
"""Configuration manager."""
DEFAULT_CONFIG = {
'ai': {
'provider': 'openai',
'model': 'gpt-4',
'api_key': None,
},
'storage': {
'inbox_dir': 'inbox',
'topics_dir': 'topics',
'data_dir': 'data',
},
'git': {
'enabled': False,
'vault_path': '~/Documents/Obsidian/KB',
'remote': 'origin',
'branch': 'main',
'auto_push': False,
'commit_message_template': 'Add note: {title}',
},
'obsidian': {
'vault_path': '~/Documents/Obsidian/KB',
'notes_folder': 'Notes',
},
'chunking': {
'enabled': True,
'chunk_duration_seconds': 900, # 15 minutes
'max_chunk_duration_seconds': 1800, # 30 minutes
'long_video_threshold': 1800, # 30 minutes
},
'output': {
'organize_by': 'author', # author | date | topic
'filename_format': '{date}_{title}.md',
'auto_tags': True,
'default_tags': ['video', 'bilibili'],
},
'sources': {
'bilibili': {
'enabled': True,
'cookie': None,
'extraction': {
'headless': True,
'timeout': 30,
},
},
'youtube': {
'enabled': False,
},
'zhihu': {
'enabled': False,
},
},
}
def __init__(self, config_path: Optional[str] = None, load_env: bool = True):
"""
Initialize configuration.
Args:
config_path: Path to config file. If None, uses default locations.
load_env: Whether to load environment variable overrides.
"""
self.config_path = config_path or self._find_config_file()
self._config = self._load_config(load_env)
def _find_config_file(self) -> str:
"""Find configuration file in standard locations."""
search_paths = [
Path.cwd() / 'config.yaml',
Path.cwd() / 'config.yml',
Path.home() / '.obsidian-kb' / 'config.yaml',
Path.home() / '.config' / 'obsidian-kb' / 'config.yaml',
]
for path in search_paths:
if path.exists():
return str(path)
# Return default path
return str(Path.cwd() / 'config.yaml')
def _load_config(self, load_env: bool = True) -> Dict[str, Any]:
"""Load configuration from file or use defaults."""
config = self.DEFAULT_CONFIG.copy()
if Path(self.config_path).exists():
with open(self.config_path, 'r', encoding='utf-8') as f:
user_config = yaml.safe_load(f)
if user_config:
self._deep_update(config, user_config)
# Override with environment variables
if load_env:
self._apply_env_overrides(config)
return config
def _deep_update(self, base: Dict, update: Dict) -> None:
"""Deep update dictionary."""
for key, value in update.items():
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
self._deep_update(base[key], value)
else:
base[key] = value
def _apply_env_overrides(self, config: Dict[str, Any]) -> None:
"""Apply environment variable overrides."""
# AI settings
if os.getenv('OPENAI_API_KEY'):
config['ai']['api_key'] = os.getenv('OPENAI_API_KEY')
if os.getenv('AI_MODEL'):
config['ai']['model'] = os.getenv('AI_MODEL')
# Git settings
if os.getenv('OBSIDIAN_VAULT_PATH'):
config['git']['vault_path'] = os.getenv('OBSIDIAN_VAULT_PATH')
# Bilibili cookie
if os.getenv('BILIBILI_COOKIE'):
config['sources']['bilibili']['cookie'] = os.getenv('BILIBILI_COOKIE')
def get(self, key: str, default: Any = None) -> Any:
"""
Get configuration value using dot notation.
Args:
key: Key in dot notation (e.g., 'ai.model')
default: Default value if key not found
Returns:
Configuration value
"""
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
def set(self, key: str, value: Any) -> None:
"""
Set configuration value using dot notation.
Args:
key: Key in dot notation
value: Value to set
"""
keys = key.split('.')
config = self._config
for k in keys[:-1]:
if k not in config:
config[k] = {}
config = config[k]
config[keys[-1]] = value
def save(self) -> None:
"""Save configuration to file."""
Path(self.config_path).parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(self._config, f, default_flow_style=False, allow_unicode=True)
@property
def obsidian_path(self) -> str:
"""Get obsidian path (alias for vault_path)."""
return str(self.vault_path)
@property
def vault_path(self) -> Path:
"""Get Obsidian vault path."""
path = self.get('obsidian.vault_path') or self.get('git.vault_path', '~/Documents/Obsidian/KB')
return Path(path).expanduser()
@property
def notes_folder(self) -> str:
"""Get notes folder."""
return self.get('obsidian.notes_folder', 'Notes')
@property
def ai_api_key(self) -> Optional[str]:
"""Get AI API key."""
return self.get('ai.api_key')
@property
def ai_model(self) -> str:
"""Get AI model."""
return self.get('ai.model', 'gpt-4')
@property
def chunking_enabled(self) -> bool:
"""Check if chunking is enabled."""
return self.get('chunking.enabled', True)
@property
def chunk_duration(self) -> int:
"""Get chunk duration in seconds."""
return self.get('chunking.chunk_duration_seconds', 900)
@property
def long_video_threshold(self) -> int:
"""Get long video threshold in seconds."""
return self.get('chunking.long_video_threshold', 1800)
@property
def quality_threshold(self) -> float:
"""Get quality threshold."""
return self.get('quality_threshold', 0.7)
@property
def obsidian(self) -> 'Config':
"""Get obsidian config section (returns self for compatibility)."""
return self
@property
def git(self) -> 'Config':
"""Get git config section (returns self for compatibility)."""
return self
@property
def enabled(self) -> bool:
"""Check if git is enabled."""
return self.get('git.enabled', False)
@property
def auto_push(self) -> bool:
"""Check if auto push is enabled."""
return self.get('git.auto_push', False)
@property
def commit_message_template(self) -> str:
"""Get commit message template."""
return self.get('git.commit_message_template', 'Add note: {title}')
@property
def kimi_api_key(self) -> str:
"""Get Kimi API key (alias for ai.api_key)."""
return self.get('ai.api_key') or ''
# Output configuration
@property
def organize_by(self) -> str:
"""Get output organize_by strategy."""
return self.get('output.organize_by', 'author')
@property
def filename_format(self) -> str:
"""Get output filename format."""
return self.get('output.filename_format', '{date}_{title}.md')
@property
def auto_tags(self) -> bool:
"""Get auto_tags setting."""
return self.get('output.auto_tags', True)
@property
def default_tags(self) -> list:
"""Get default tags list."""
return self.get('output.default_tags', ['video', 'bilibili'])
# Bilibili extraction configuration
@property
def bilibili_headless(self) -> bool:
"""Get bilibili extraction headless mode."""
return self.get('sources.bilibili.extraction.headless', True)
@property
def bilibili_timeout(self) -> int:
"""Get bilibili extraction timeout in seconds."""
return self.get('sources.bilibili.extraction.timeout', 30)
# Zhihu DrissionPage configuration
@property
def zhihu_enabled(self) -> bool:
"""知乎是否启用"""
return self.get('sources.zhihu.enabled', False)
@property
def zhihu_use_drissionpage(self) -> bool:
"""知乎是否使用 DrissionPage"""
return self.get('sources.zhihu.use_drissionpage', True)
@property
def zhihu_cookie_file(self) -> Optional[str]:
"""知乎 Cookie 文件路径"""
return self.get('sources.zhihu.cookie_file')
@property
def zhihu_cookie_str(self) -> Optional[str]:
"""知乎 Cookie 字符串"""
return self.get('sources.zhihu.cookie_str')
@property
def zhihu_timeout(self) -> int:
"""知乎提取超时时间(秒)"""
return self.get('sources.zhihu.timeout', 30)
@property
def zhihu_headless(self) -> bool:
"""知乎是否使用无头浏览器"""
return self.get('sources.zhihu.headless', True)
def ensure_dirs(self) -> None:
"""Ensure required directories exist."""
self.vault_path.mkdir(parents=True, exist_ok=True)
(self.vault_path / self.get('storage.inbox_dir', 'Inbox')).mkdir(exist_ok=True)
(self.vault_path / self.get('storage.topics_dir', 'Topics')).mkdir(exist_ok=True)
def validate(self) -> tuple[bool, list[str]]:
"""Validate configuration."""
errors = []
if not self.ai_api_key:
errors.append("AI API key not configured")
return len(errors) == 0, errors
# Subscriptions support
def get_subscriptions(self) -> list:
"""Get subscriptions list."""
return self.get('subscriptions', [])
def add_subscription(self, subscription: Dict[str, Any]) -> None:
"""Add a new subscription."""
subs = self.get('subscriptions', [])
subs.append(subscription)
self.set('subscriptions', subs)
self.save()