feat: add interactive setup wizard with model selection and agent config (v2.6.0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 10:34:39 +10:00
co-authored by Claude Sonnet 4.6
parent 9f43452f03
commit 719723225d
3 changed files with 605 additions and 1 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "memory-continuity",
"name": "Memory Continuity",
"description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.",
"version": "2.5.0",
"version": "2.6.0",
"source": "https://github.com/dtzp555-max/memory-continuity",
"configSchema": {
"type": "object",
+4
View File
@@ -276,6 +276,10 @@ else
esac
fi
echo ""
echo "🧙 启动配置向导..."
python3 "$(dirname "$0")/setup_wizard.py" || true
echo ""
echo "=== Installation complete ==="
echo ""
+600
View File
@@ -0,0 +1,600 @@
#!/usr/bin/env python3
# setup_wizard.py — Interactive post-install wizard for memory-continuity skill
#
# Guides the user through:
# Step 1/4: Select default model
# Step 2/4: Configure per-agent models
# Step 3/4: Review changes + OCP check
# Step 4/4: Restart gateway
#
# Usage:
# python3 scripts/setup_wizard.py
#
# Requires Python 3.6+
import sys
import os
import json
import subprocess
import termios
import tty
import signal
# ---------------------------------------------------------------------------
# Terminal colors (ANSI)
# ---------------------------------------------------------------------------
GREEN = "\033[32m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
RED = "\033[31m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
def green(s): return f"{GREEN}{s}{RESET}"
def yellow(s): return f"{YELLOW}{s}{RESET}"
def cyan(s): return f"{CYAN}{s}{RESET}"
def red(s): return f"{RED}{s}{RESET}"
def bold(s): return f"{BOLD}{s}{RESET}"
def dim(s): return f"{DIM}{s}{RESET}"
# ---------------------------------------------------------------------------
# Key reading (raw terminal, no curses dependency)
# ---------------------------------------------------------------------------
UP_ARROW = "UP"
DOWN_ARROW = "DOWN"
ENTER_KEY = "ENTER"
CTRL_C = "CTRL_C"
def _read_key():
"""Read a single keypress from stdin. Returns a key name string."""
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
if ch == "\x03":
return CTRL_C
if ch in ("\r", "\n"):
return ENTER_KEY
if ch == "\x1b":
# Escape sequence — read more
ch2 = sys.stdin.read(1)
if ch2 == "[":
ch3 = sys.stdin.read(1)
if ch3 == "A":
return UP_ARROW
if ch3 == "B":
return DOWN_ARROW
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
# ---------------------------------------------------------------------------
# Clean exit on Ctrl+C anywhere
# ---------------------------------------------------------------------------
def _handle_sigint(sig, frame):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
signal.signal(signal.SIGINT, _handle_sigint)
# ---------------------------------------------------------------------------
# Arrow-key menu selector
# ---------------------------------------------------------------------------
def arrow_select(prompt, options, default_index=0):
"""
Display a list of options with arrow-key navigation.
Returns the selected index, or raises SystemExit on Ctrl+C.
"""
selected = default_index
n = len(options)
# Hide cursor
sys.stdout.write("\033[?25l")
sys.stdout.flush()
def _render():
# Move cursor up to re-draw
sys.stdout.write(f"\033[{n}A\r")
for i, opt in enumerate(options):
if i == selected:
prefix = f" {CYAN}{BOLD}"
suffix = RESET
else:
prefix = " "
suffix = ""
sys.stdout.write(f"\r{prefix}{opt}{suffix}\n")
sys.stdout.flush()
# Initial render
if prompt:
print(prompt)
for i, opt in enumerate(options):
print(f" {opt}")
try:
while True:
_render()
key = _read_key()
if key == CTRL_C:
sys.stdout.write("\033[?25h")
sys.stdout.flush()
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
elif key == UP_ARROW:
selected = (selected - 1) % n
elif key == DOWN_ARROW:
selected = (selected + 1) % n
elif key == ENTER_KEY:
break
finally:
sys.stdout.write("\033[?25h")
sys.stdout.flush()
return selected
# ---------------------------------------------------------------------------
# Yes/No prompt
# ---------------------------------------------------------------------------
def yes_no(prompt, default_yes=True):
"""Ask a yes/no question. Returns True for yes."""
hint = "[Y/n]" if default_yes else "[y/N]"
while True:
try:
ans = input(f"{prompt} {hint}: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
if ans == "":
return default_yes
if ans in ("y", "yes"):
return True
if ans in ("n", "no"):
return False
print(yellow(" 请输入 Y 或 N"))
# ---------------------------------------------------------------------------
# Progress header
# ---------------------------------------------------------------------------
def print_step(n, total, title):
print()
print(bold(cyan(f"步骤 {n}/{total}: {title}")))
print(cyan("" * 56))
# ---------------------------------------------------------------------------
# Find openclaw.json
# ---------------------------------------------------------------------------
OPENCLAW_JSON_PATHS = [
os.path.expanduser("~/.openclaw/openclaw.json"),
"/etc/openclaw/openclaw.json",
]
def find_openclaw_json():
for p in OPENCLAW_JSON_PATHS:
if os.path.isfile(p):
return p
return None
def load_config(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
def save_config(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
# ---------------------------------------------------------------------------
# Model list
# ---------------------------------------------------------------------------
MODELS = [
("claude-sonnet-4-6", "推荐:综合能力最强,速度均衡"),
("claude-opus-4-6", "能力最强,适合复杂任务,较慢"),
("claude-haiku-4-5", "速度最快,适合简单任务"),
]
KEEP_CURRENT = "(保持当前设置)"
KEEP_UNCHANGED = "(保持不变)"
def model_display_options(include_keep_current=False, include_keep_unchanged=False):
lines = []
for m, desc in MODELS:
lines.append(f"{m:<25} {dim(desc)}")
if include_keep_current:
lines.append(KEEP_CURRENT)
if include_keep_unchanged:
lines.append(KEEP_UNCHANGED)
return lines
def model_values(include_keep_current=False, include_keep_unchanged=False):
vals = [m for m, _ in MODELS]
if include_keep_current:
vals.append(None) # None = keep current
if include_keep_unchanged:
vals.append(None)
return vals
# ---------------------------------------------------------------------------
# Step 1: Default model selection
# ---------------------------------------------------------------------------
def step1_default_model(config):
print_step(1, 4, "选择默认 Model")
print()
print("┌─────────────────────────────────────────────────────┐")
print("│ 选择 OpenClaw 默认 Model │")
print("│ │")
print("│ claude-sonnet-4-6 ← 推荐:综合能力最强,速度均衡 │")
print("│ claude-opus-4-6 能力最强,适合复杂任务,较慢 │")
print("│ claude-haiku-4-5 速度最快,适合简单任务 │")
print("│ (保持当前设置) │")
print("└─────────────────────────────────────────────────────┘")
print()
current = (config
.get("agents", {})
.get("defaults", {})
.get("model", {})
.get("primary", None))
if current:
print(f" {dim('当前默认 Model:')} {cyan(current)}")
else:
print(f" {dim('当前默认 Model:')} {yellow('(未设置)')}")
print()
# Find default selection index
default_idx = len(MODELS) # default: keep current
if current:
for i, (m, _) in enumerate(MODELS):
if m == current:
default_idx = i
break
options = model_display_options(include_keep_current=True)
print(f" {bold('使用 ↑↓ 方向键选择,Enter 确认:')}")
print()
idx = arrow_select(None, options, default_index=default_idx)
if idx >= len(MODELS):
# Keep current
chosen_model = current # may be None
print(f"\n {green('')} 保持当前设置: {cyan(current) if current else yellow('(未设置)')}")
return chosen_model, False # (value, changed)
else:
chosen_model = MODELS[idx][0]
changed = (chosen_model != current)
print(f"\n {green('')} 选择: {cyan(chosen_model)}")
return chosen_model, changed
# ---------------------------------------------------------------------------
# Step 2: Agent model configuration
# ---------------------------------------------------------------------------
def step2_agent_config(config, default_model):
print_step(2, 4, "Agent Model 配置")
print()
agent_options = [
"不修改任何 Agent(默认)",
"全部按默认 Model 修改",
"逐一配置每个 Agent",
]
print(f" {bold('选择配置方式:')}")
print()
mode_idx = arrow_select(None, agent_options, default_index=0)
print()
agents = config.get("agents", {}).get("list", [])
agent_changes = {} # agent_id -> {"primary": ..., "fallback": ...}
if mode_idx == 0:
print(f" {green('')} 不修改任何 Agent")
return agent_changes
if mode_idx == 1:
# Apply default model to all agents
if not default_model:
print(f" {yellow('⚠️ 未设置默认 Model,跳过批量修改')}")
return agent_changes
for agent in agents:
aid = agent.get("id", "")
if not aid:
continue
current_primary = (agent.get("model", {}) or {}).get("primary", None)
if current_primary != default_model:
agent_changes[aid] = {"primary": default_model, "fallback": None, "keep_fallback": True}
print(f" {green('')} 已将 {len(agent_changes)} 个 Agent 的 primary 设为 {cyan(default_model)}")
return agent_changes
# mode_idx == 2: configure one by one
if not agents:
print(f" {yellow('openclaw.json 中没有找到 Agent 列表')}")
return agent_changes
for agent in agents:
aid = agent.get("id", "")
aname = agent.get("name", aid)
if not aid:
continue
cur_model_block = agent.get("model", {}) or {}
cur_primary = cur_model_block.get("primary", None)
cur_fallback = cur_model_block.get("fallback", None)
print()
print(f" {bold(cyan('Agent:'))} {aname} ({dim(aid)})")
print(f" current primary: {cyan(cur_primary) if cur_primary else yellow('(未设置)')}")
print(f" current fallback: {cyan(cur_fallback) if cur_fallback else yellow('(未设置)')}")
print()
# Pick primary
print(f" {bold('选择新 primary Model (↑↓ Enter):')}")
print()
primary_options = model_display_options(include_keep_unchanged=True)
primary_vals = model_values(include_keep_unchanged=True)
# Default: keep unchanged
p_default = len(MODELS)
if cur_primary:
for i, (m, _) in enumerate(MODELS):
if m == cur_primary:
p_default = i
break
p_idx = arrow_select(None, primary_options, default_index=p_default)
new_primary = primary_vals[p_idx] if p_idx < len(MODELS) else cur_primary
print(f"\n {green('')} primary: {cyan(new_primary) if new_primary else yellow('(保持不变)')}")
# Pick fallback
print()
print(f" {bold('选择新 fallback Model (↑↓ Enter):')}")
print()
fallback_options = model_display_options() + ["(跳过 / 不设置 fallback"]
f_default = len(MODELS) # skip
if cur_fallback:
for i, (m, _) in enumerate(MODELS):
if m == cur_fallback:
f_default = i
break
f_idx = arrow_select(None, fallback_options, default_index=f_default)
if f_idx < len(MODELS):
new_fallback = MODELS[f_idx][0]
else:
new_fallback = cur_fallback # keep existing
print(f"\n {green('')} fallback: {cyan(new_fallback) if new_fallback else yellow('(保持不变)')}")
# Record changes only if something actually differs
primary_changed = (new_primary is not None and new_primary != cur_primary)
fallback_changed = (new_fallback is not None and new_fallback != cur_fallback)
if primary_changed or fallback_changed:
agent_changes[aid] = {
"primary": new_primary if primary_changed else cur_primary,
"fallback": new_fallback if fallback_changed else cur_fallback,
}
# Ask whether to continue to next agent
print()
try:
cont = input(f" 还需要修改其他 Agent 吗?[Y/n]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
if cont in ("n", "no"):
break
return agent_changes
# ---------------------------------------------------------------------------
# Step 3: Summary + OCP check + confirm
# ---------------------------------------------------------------------------
OCP_MODELS_NEEDING_PROXY = ("claude-local", "claude-sonnet", "claude-opus", "claude-haiku")
def _needs_ocp(model_name):
if not model_name:
return False
return any(kw in model_name for kw in OCP_MODELS_NEEDING_PROXY)
def _ocp_running():
try:
result = subprocess.run(
["curl", "-s", "--max-time", "3", "http://localhost:3456/health"],
capture_output=True, text=True
)
return result.returncode == 0 and result.stdout.strip() != ""
except Exception:
return False
def step3_summary_confirm(config, new_default_model, default_changed, agent_changes):
print_step(3, 4, "确认修改")
print()
# Build summary lines
current_default = (config
.get("agents", {})
.get("defaults", {})
.get("model", {})
.get("primary", None))
has_any_change = default_changed or bool(agent_changes)
if not has_any_change:
return False # caller will handle "no changes" message
print(f" {bold('即将应用以下修改:')}")
print()
if default_changed:
old_str = cyan(current_default) if current_default else yellow("(未设置)")
new_str = cyan(new_default_model)
print(f" {bold('[defaults]')} {old_str}{new_str}")
agents = config.get("agents", {}).get("list", [])
agent_map = {a.get("id", ""): a for a in agents if a.get("id")}
for aid, change in agent_changes.items():
agent = agent_map.get(aid, {})
cur_primary = (agent.get("model", {}) or {}).get("primary", None)
new_primary = change.get("primary", cur_primary)
old_str = cyan(cur_primary) if cur_primary else yellow("(未设置)")
new_str = cyan(new_primary) if new_primary else yellow("(未设置)")
aname = agent.get("name", aid)
if cur_primary == new_primary:
print(f" {bold(aname + ':')}{'':4}不变")
else:
print(f" {bold(aname + ':')}{'':4}{old_str}{new_str}")
print()
# OCP check
all_new_models = []
if default_changed and new_default_model:
all_new_models.append(new_default_model)
for aid, change in agent_changes.items():
if change.get("primary"):
all_new_models.append(change["primary"])
if change.get("fallback"):
all_new_models.append(change["fallback"])
needs_ocp = any(_needs_ocp(m) for m in all_new_models)
if needs_ocp and not _ocp_running():
print(f" {yellow('⚠️ 检测到你选择的 Model 需要 openclaw-claude-proxy (ocp)')}")
print(f" {yellow(' 但 ocp 目前未运行。建议先安装并启动 ocp。')}")
print()
try:
ans = input(f" 继续 [c] / 退出查看 ocp 安装文档 [q]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
if ans in ("q", "quit", "exit"):
print()
print(f" {cyan('请参考 ocp 安装文档: https://github.com/openclaw/openclaw-claude-proxy')}")
print(f" {yellow('已退出,未做任何修改')}")
sys.exit(0)
confirmed = yes_no(" 确认应用?", default_yes=True)
return confirmed
# ---------------------------------------------------------------------------
# Apply changes to openclaw.json
# ---------------------------------------------------------------------------
def apply_changes(config, new_default_model, default_changed, agent_changes):
if default_changed and new_default_model is not None:
agents_block = config.setdefault("agents", {})
defaults_block = agents_block.setdefault("defaults", {})
model_block = defaults_block.setdefault("model", {})
model_block["primary"] = new_default_model
if agent_changes:
agents_list = config.get("agents", {}).get("list", [])
for agent in agents_list:
aid = agent.get("id", "")
if aid in agent_changes:
change = agent_changes[aid]
if "model" not in agent or agent["model"] is None:
agent["model"] = {}
if change.get("primary") is not None:
agent["model"]["primary"] = change["primary"]
if change.get("fallback") is not None:
agent["model"]["fallback"] = change["fallback"]
# ---------------------------------------------------------------------------
# Step 4: Restart gateway
# ---------------------------------------------------------------------------
def step4_restart_gateway():
print_step(4, 4, "重启 Gateway")
print()
restart = yes_no(" 配置已更新。是否立即重启 Gateway 以使配置生效?", default_yes=True)
if not restart:
print(f" {dim('跳过重启。请手动运行: openclaw gateway restart')}")
return
print(f" {cyan('正在执行: openclaw gateway restart ...')}")
try:
result = subprocess.run(
["openclaw", "gateway", "restart"],
capture_output=True, text=True
)
if result.returncode == 0:
print(f" {green('✓ Gateway 重启成功')}")
if result.stdout.strip():
print(f" {dim(result.stdout.strip())}")
else:
print(f" {yellow('⚠️ Gateway 重启返回非零退出码')}")
if result.stderr.strip():
print(f" {red(result.stderr.strip())}")
except FileNotFoundError:
print(f" {yellow('⚠️ 未找到 openclaw 命令。请手动重启 Gateway。')}")
except Exception as e:
print(f" {red(f'重启失败: {e}')}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
print()
print(bold(cyan("╔══════════════════════════════════════════════════════╗")))
print(bold(cyan("║ OpenClaw Memory-Continuity 配置向导 ║")))
print(bold(cyan("╚══════════════════════════════════════════════════════╝")))
print()
# Check if stdin is a tty — if not, skip interactive wizard
if not sys.stdin.isatty():
print(f" {yellow('非交互式终端,跳过配置向导')}")
sys.exit(0)
# Find config file
config_path = find_openclaw_json()
if not config_path:
print(f" {red('错误: 未找到 openclaw.json')}")
print(f" {dim('已搜索以下路径:')}")
for p in OPENCLAW_JSON_PATHS:
print(f" {dim(p)}")
print(f" {yellow('请先初始化 OpenClaw 配置,再运行此向导')}")
sys.exit(1)
print(f" {dim('配置文件:')} {cyan(config_path)}")
config = load_config(config_path)
# ── Step 1 ──────────────────────────────────────────────────────────────
new_default_model, default_changed = step1_default_model(config)
# ── Step 2 ──────────────────────────────────────────────────────────────
agent_changes = step2_agent_config(config, new_default_model)
# ── Check for any changes ───────────────────────────────────────────────
has_any_change = default_changed or bool(agent_changes)
if not has_any_change:
print()
print(f" {green('无需修改,配置保持不变 ✅')}")
print()
sys.exit(0)
# ── Step 3 ──────────────────────────────────────────────────────────────
confirmed = step3_summary_confirm(
config, new_default_model, default_changed, agent_changes
)
if not confirmed:
print()
print(f" {yellow('已取消,未做任何修改')}")
print()
sys.exit(0)
# Write changes
apply_changes(config, new_default_model, default_changed, agent_changes)
save_config(config_path, config)
print(f"\n {green('✓ 配置已写入')} {cyan(config_path)}")
# ── Step 4 ──────────────────────────────────────────────────────────────
step4_restart_gateway()
print()
print(bold(green("╔══════════════════════════════════════════════════════╗")))
print(bold(green("║ 配置向导完成 ✅ ║")))
print(bold(green("╚══════════════════════════════════════════════════════╝")))
print()
if __name__ == "__main__":
main()