install-hermes-bridge.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """Install the bundled Hermes plugin; preserve disabled settings and keep a backup.
  2. Run with the Hermes virtualenv Python. This command does not restart Hermes.
  3. """
  4. import argparse
  5. import json
  6. import os
  7. import re
  8. import shutil
  9. import tempfile
  10. import uuid
  11. from datetime import datetime, timezone
  12. from pathlib import Path
  13. import yaml
  14. def install(root, profile, source):
  15. if not re.fullmatch(r'[a-z0-9][a-z0-9_-]{0,63}', profile):
  16. raise ValueError('Profile 名称无效')
  17. home = root if profile == 'default' else root / 'profiles' / profile
  18. config_path = home / 'config.yaml'
  19. if not config_path.is_file() or config_path.is_symlink():
  20. raise ValueError('Profile 配置不存在或是符号链接')
  21. original = config_path.read_bytes()
  22. config = yaml.safe_load(original) or {}
  23. if not isinstance(config, dict):
  24. raise ValueError('Profile 配置格式无效')
  25. plugins = config.setdefault('plugins', {})
  26. if not isinstance(plugins, dict):
  27. raise ValueError('Profile 插件配置格式无效')
  28. for key in ('enabled', 'disabled'):
  29. if key in plugins and not isinstance(plugins[key], list):
  30. raise ValueError('Profile 插件列表格式无效')
  31. name = 'yaoyao-bot-bridge'
  32. disabled = name in plugins.get('disabled', [])
  33. if not disabled and name not in plugins.setdefault('enabled', []):
  34. plugins['enabled'].append(name)
  35. directory = home / 'plugins'
  36. target = directory / name
  37. if directory.is_symlink() or target.is_symlink():
  38. raise ValueError('插件目录不能是符号链接')
  39. directory.mkdir(parents=True, exist_ok=True)
  40. stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ') + '-' + uuid.uuid4().hex[:8]
  41. backup = home / 'backups' / ('yaoyao-bridge-' + stamp)
  42. backup.mkdir(parents=True, mode=0o700)
  43. (backup / 'config.yaml').write_bytes(original)
  44. (backup / 'config.yaml').chmod(0o600)
  45. staged = directory / ('.yaoyao-bridge-' + uuid.uuid4().hex)
  46. previous = backup / name
  47. moved = False
  48. activated = False
  49. try:
  50. shutil.copytree(source, staged, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', 'tests'))
  51. if target.exists():
  52. target.rename(previous)
  53. moved = True
  54. staged.rename(target)
  55. activated = True
  56. fd, temporary = tempfile.mkstemp(prefix='.config-yaoyao-', dir=home)
  57. try:
  58. with os.fdopen(fd, 'w') as stream:
  59. yaml.safe_dump(config, stream, allow_unicode=True, sort_keys=False)
  60. os.replace(temporary, config_path)
  61. finally:
  62. Path(temporary).unlink(missing_ok=True)
  63. except BaseException:
  64. if activated:
  65. shutil.rmtree(target)
  66. if moved:
  67. previous.rename(target)
  68. config_path.write_bytes(original)
  69. raise
  70. finally:
  71. if staged.exists():
  72. shutil.rmtree(staged)
  73. return {'profile': profile, 'installed': str(target), 'backup': str(backup), 'disabled': disabled,
  74. 'message': '已更新,保留显式禁用设置' if disabled else '已更新并启用;空闲时重新加载 Hermes 后生效'}
  75. def main():
  76. parser = argparse.ArgumentParser(description=__doc__)
  77. parser.add_argument('--hermes-home', required=True, type=Path)
  78. parser.add_argument('--profile', action='append', default=[])
  79. args = parser.parse_args()
  80. source = Path(__file__).resolve().parent / 'hermes-bots-bridge'
  81. if not source.is_dir():
  82. source = Path(__file__).resolve().parents[1] / 'integrations' / 'hermes-bots-bridge'
  83. if not (source / 'plugin.yaml').is_file():
  84. raise ValueError('缺少随附的工具桥插件')
  85. for profile in dict.fromkeys(args.profile or ['default']):
  86. print(json.dumps(install(args.hermes_home.expanduser().resolve(), profile, source), ensure_ascii=False))
  87. if __name__ == '__main__':
  88. main()