profile_runtime_repair.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. """Prepare narrowly scoped repairs for Hermes' multi-Profile session routing.
  2. The installer owns backup/activation/rollback. This module never writes files.
  3. Unsupported source layouts fail before anything is changed.
  4. """
  5. import ast
  6. from pathlib import Path
  7. def _function(source, name):
  8. matches = [node for node in ast.parse(source).body
  9. if isinstance(node, ast.FunctionDef) and node.name == name]
  10. if len(matches) != 1:
  11. raise ValueError(f'Hermes 接口 {name} 不兼容,未修改核心文件')
  12. node = matches[0]
  13. lines = source.splitlines(keepends=True)
  14. return node, ''.join(lines[node.lineno - 1:node.end_lineno])
  15. def _replace_function(source, name, transform):
  16. node, body = _function(source, name)
  17. lines = source.splitlines(keepends=True)
  18. return ''.join(lines[:node.lineno - 1]) + transform(body) + ''.join(lines[node.end_lineno:])
  19. def _config_first(body):
  20. marker = '# Yaoyao: each Profile owns its configured model.'
  21. if marker in body:
  22. return body
  23. old = ' if env := _env_model_seed():\n return env\n m = _load_cfg().get("model", "")\n'
  24. empty_model = ' if isinstance(m, dict):\n return str(m.get("default", "") or "").strip()\n'
  25. anchor = ' # No env seed / config preference:'
  26. if body.count(old) != 1 or body.count(anchor) != 1 or body.count(empty_model) != 1:
  27. raise ValueError('Hermes 模型解析接口已改变,未修改核心文件')
  28. body = body.replace(old, f' {marker}\n m = _load_cfg().get("model", "")\n')
  29. body = body.replace(empty_model, ' if isinstance(m, dict):\n m = str(m.get("default", "") or "").strip()\n')
  30. return body.replace(anchor, ' if env := _env_model_seed():\n return env\n' + anchor)
  31. def _provider_from_profile(body):
  32. marker = '# Yaoyao: launch-model/provider seeds cannot replace a configured Profile.'
  33. if marker in body:
  34. return body
  35. anchor = ' model = _resolve_model()\n'
  36. if body.count(anchor) != 1:
  37. raise ValueError('Hermes Provider 解析接口已改变,未修改核心文件')
  38. return body.replace(anchor, f' {marker}\n'
  39. ' configured_model, configured_provider = _config_model_target()\n'
  40. ' if configured_model:\n'
  41. ' return configured_model, configured_provider or None\n' + anchor)
  42. def _scope_session_metadata(source):
  43. for method in ('session.create', 'session.resume'):
  44. needle = f'@method("{method}")\n'
  45. if source.count(needle) != 1:
  46. raise ValueError(f'Hermes {method} 接口已改变,未修改核心文件')
  47. if needle + '@_profile_scoped\n' not in source:
  48. source = source.replace(needle, needle + '@_profile_scoped\n')
  49. return source
  50. def prepare(source_root: Path):
  51. """Return (path, original, replacement) tuples, including no-op entries."""
  52. files = [(source_root / 'tui_gateway' / 'server.py', lambda text:
  53. _replace_function(_replace_function(text, '_resolve_model', _config_first),
  54. '_resolve_startup_runtime', _provider_from_profile)),
  55. (source_root / 'tui_gateway' / 'methods_session.py', _scope_session_metadata)]
  56. changes = []
  57. for path, transform in files:
  58. if not path.is_file() or path.is_symlink():
  59. raise ValueError('未找到可修复的 Hermes 核心文件')
  60. original = path.read_bytes()
  61. repaired = transform(original.decode('utf-8'))
  62. compile(repaired, str(path), 'exec')
  63. changes.append((path, original, repaired.encode('utf-8')))
  64. return changes