mirror of
https://github.com/okxlin/release-factory.git
synced 2026-10-07 08:01:59 +00:00
241 lines
7.9 KiB
Python
241 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
import copy
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
CONFIG_DIRS = [
|
|
Path(os.environ.get('OPENCODE_CONFIG_DIR', '')).expanduser() if os.environ.get('OPENCODE_CONFIG_DIR') else None,
|
|
Path('/config/opencode'),
|
|
Path.home() / '.config' / 'opencode',
|
|
]
|
|
|
|
CONFIG_FILES = ['opencode.json', 'opencode.jsonc']
|
|
|
|
|
|
def resolve_config_dir() -> Path | None:
|
|
env_dir = os.environ.get('OPENCODE_CONFIG_DIR', '').strip()
|
|
if env_dir:
|
|
return Path(env_dir).expanduser()
|
|
for directory in CONFIG_DIRS[1:]:
|
|
if directory and directory.exists():
|
|
return directory
|
|
for directory in CONFIG_DIRS[1:]:
|
|
if directory:
|
|
return directory
|
|
return None
|
|
|
|
|
|
def strip_jsonc(text: str) -> str:
|
|
lines = []
|
|
for line in text.splitlines():
|
|
out = []
|
|
in_string = False
|
|
escaped = False
|
|
i = 0
|
|
while i < len(line):
|
|
ch = line[i]
|
|
if escaped:
|
|
out.append(ch)
|
|
escaped = False
|
|
elif ch == '\\':
|
|
out.append(ch)
|
|
escaped = True
|
|
elif ch == '"':
|
|
out.append(ch)
|
|
in_string = not in_string
|
|
elif not in_string and ch == '/' and i + 1 < len(line) and line[i + 1] == '/':
|
|
break
|
|
else:
|
|
out.append(ch)
|
|
i += 1
|
|
lines.append(''.join(out))
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def load_config(path: Path):
|
|
if not path.exists():
|
|
return {}
|
|
raw = path.read_text(encoding='utf-8')
|
|
cleaned = strip_jsonc(raw).strip()
|
|
return json.loads(cleaned) if cleaned else {}
|
|
|
|
|
|
def save_config(path: Path, data):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
|
|
|
|
|
def locate_config() -> Path:
|
|
preferred_dir = resolve_config_dir()
|
|
if preferred_dir:
|
|
for filename in CONFIG_FILES:
|
|
path = preferred_dir / filename
|
|
if path.exists():
|
|
return path
|
|
return preferred_dir / 'opencode.json'
|
|
for directory in CONFIG_DIRS:
|
|
if not directory:
|
|
continue
|
|
for filename in CONFIG_FILES:
|
|
path = directory / filename
|
|
if path.exists():
|
|
return path
|
|
for directory in CONFIG_DIRS:
|
|
if directory:
|
|
return directory / 'opencode.json'
|
|
return Path.home() / '.config' / 'opencode' / 'opencode.json'
|
|
|
|
|
|
def ensure_plugin(data, plugin_name):
|
|
plugins = data.get('plugin')
|
|
if not isinstance(plugins, list):
|
|
plugins = []
|
|
normalized = {str(item).split('@')[0] for item in plugins if isinstance(item, str)}
|
|
plugin_base = plugin_name.split('@')[0]
|
|
if plugin_base not in normalized and plugin_name not in plugins:
|
|
plugins.append(plugin_name)
|
|
data['plugin'] = plugins
|
|
|
|
|
|
def ensure_oh_my_opencode_registration(data):
|
|
mcp = data.get('mcp')
|
|
if not isinstance(mcp, dict):
|
|
mcp = {}
|
|
if 'oh-my-opencode' not in mcp:
|
|
mcp['oh-my-opencode'] = {
|
|
'type': 'local',
|
|
'command': ['bunx', '--bun', 'oh-my-opencode', 'mcp', 'start'],
|
|
'enabled': True,
|
|
}
|
|
data['mcp'] = mcp
|
|
|
|
|
|
def ensure_refusal_settings(data):
|
|
experimental = data.get('experimental')
|
|
if not isinstance(experimental, dict):
|
|
experimental = {}
|
|
refusal = experimental.get('refusal_patcher')
|
|
if not isinstance(refusal, dict):
|
|
refusal = {}
|
|
refusal.setdefault('enabled', True)
|
|
base_url = os.environ.get('GPT_UNLOCKED_BASE_URL', '').strip()
|
|
api_key = os.environ.get('GPT_UNLOCKED_API_KEY', '').strip()
|
|
model = os.environ.get('GPT_UNLOCKED_MODEL', '').strip()
|
|
placeholder = os.environ.get('GPT_UNLOCKED_PLACEHOLDER', '').strip()
|
|
if base_url:
|
|
refusal['base_url'] = base_url
|
|
if api_key:
|
|
refusal['api_key'] = api_key
|
|
if model:
|
|
refusal['model'] = model
|
|
if placeholder:
|
|
refusal['placeholder'] = placeholder
|
|
experimental['refusal_patcher'] = refusal
|
|
data['experimental'] = experimental
|
|
|
|
|
|
def ensure_model_settings(data):
|
|
model = os.environ.get('OPENCODE_MODEL', '').strip()
|
|
small_model = os.environ.get('OPENCODE_SMALL_MODEL', '').strip()
|
|
provider_id = os.environ.get('OPENCODE_PROVIDER_ID', '').strip()
|
|
if model:
|
|
data['model'] = model
|
|
if small_model:
|
|
data['small_model'] = small_model
|
|
provider_map = {
|
|
'openai': ('OPENAI_BASE_URL', 'OPENAI_API_KEY'),
|
|
'anthropic': ('ANTHROPIC_BASE_URL', 'ANTHROPIC_API_KEY'),
|
|
'openrouter': ('OPENROUTER_BASE_URL', 'OPENROUTER_API_KEY'),
|
|
'gemini': ('GEMINI_BASE_URL', 'GEMINI_API_KEY'),
|
|
'gpt-unlocked': ('GPT_UNLOCKED_BASE_URL', 'GPT_UNLOCKED_API_KEY'),
|
|
'mimo': ('OPENAI_BASE_URL', 'OPENAI_API_KEY'),
|
|
'xiaomi': ('OPENAI_BASE_URL', 'OPENAI_API_KEY'),
|
|
}
|
|
if provider_id:
|
|
provider = data.get('provider')
|
|
if not isinstance(provider, dict):
|
|
provider = {}
|
|
provider_entry = provider.get(provider_id)
|
|
if not isinstance(provider_entry, dict):
|
|
provider_entry = {}
|
|
options = provider_entry.get('options')
|
|
if not isinstance(options, dict):
|
|
options = {}
|
|
base_env, key_env = provider_map.get(provider_id, ('', ''))
|
|
base_url = os.environ.get(base_env, '').strip() if base_env else ''
|
|
api_key = os.environ.get(key_env, '').strip() if key_env else ''
|
|
if base_url:
|
|
options['baseURL'] = base_url
|
|
if api_key:
|
|
options['apiKey'] = api_key
|
|
if options:
|
|
provider_entry['options'] = options
|
|
if provider_entry:
|
|
provider[provider_id] = provider_entry
|
|
if provider:
|
|
data['provider'] = provider
|
|
|
|
|
|
def merge_values(base, override):
|
|
if isinstance(base, dict) and isinstance(override, dict):
|
|
result = {k: copy.deepcopy(v) for k, v in base.items()}
|
|
for key, value in override.items():
|
|
if key in result:
|
|
result[key] = merge_values(result[key], value)
|
|
else:
|
|
result[key] = copy.deepcopy(value)
|
|
return result
|
|
if isinstance(base, list) and isinstance(override, list):
|
|
if all(isinstance(v, str) for v in base + override):
|
|
merged = []
|
|
seen = set()
|
|
for item in base + override:
|
|
if item in seen:
|
|
continue
|
|
seen.add(item)
|
|
merged.append(item)
|
|
return merged
|
|
return copy.deepcopy(base) + copy.deepcopy(override)
|
|
return copy.deepcopy(override)
|
|
|
|
|
|
def apply_user_override(data, config_path: Path):
|
|
user_override = config_path.with_name('opencode.user.json')
|
|
if user_override.exists():
|
|
override_data = load_config(user_override)
|
|
if isinstance(override_data, dict):
|
|
return merge_values(data, override_data)
|
|
return data
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit('usage: update_opencode_config.py plugin <plugin-name> | oh-my-opencode register | sync-model')
|
|
action = sys.argv[1]
|
|
value = sys.argv[2] if len(sys.argv) >= 3 else None
|
|
path = locate_config()
|
|
data = load_config(path)
|
|
if action == 'plugin':
|
|
if not value:
|
|
raise SystemExit('usage: update_opencode_config.py plugin <plugin-name>')
|
|
ensure_plugin(data, value)
|
|
if value.startswith('opencode-gpt-unlocked'):
|
|
ensure_refusal_settings(data)
|
|
elif action == 'oh-my-opencode' and value == 'register':
|
|
ensure_oh_my_opencode_registration(data)
|
|
elif action == 'sync-model':
|
|
pass
|
|
else:
|
|
suffix = f' {value}' if value is not None else ''
|
|
raise SystemExit(f'unknown action: {action}{suffix}')
|
|
ensure_model_settings(data)
|
|
data = apply_user_override(data, path)
|
|
save_config(path, data)
|
|
print(path)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|