Files
kwcode/kaiwu/cli/onboarding.py
Val-sss 53c08a4e28 feat: three flywheels + anonymous telemetry + prompt quantification
Flywheel system (all local):
- strategy_stats.py: error strategy effectiveness tracking per error_type
- user_pattern_memory.py: cross-project user error pattern warnings
- skill_drafter.py: auto-generate SKILL.md drafts from stats

Anonymous telemetry (opt-in, default off):
- telemetry/client.py: HMAC-signed fire-and-forget upload
- Only sends: error_type, retry_count, success, model
- Never sends: code, paths, descriptions, user identity
- onboarding asks opt-in, kwcode telemetry enable/disable

CLI commands:
- kwcode telemetry status/enable/disable
- kwcode skill review/accept/discard
- kwcode stats: enhanced with flywheel + telemetry status

Prompt quantification (CC-style):
- GENERATOR_BASE_SYSTEM: "only do what's asked" → "≤2 functions, ≤30 lines"
- RETRY_STRATEGIES hints: quantified line limits per error type
- CHAT_SYSTEM: "short friendly" → "≤100 chars, ≤3 sentences"

Server deployed: https://llmbbs.com (nginx → uvicorn:9753, 3 SQLite tables)

Tests: 19 new, 0 regression

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 19:39:17 +08:00

256 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# kwcode/cli/onboarding.py
"""
首次启动引导流程。
BOOT-RED-1未完成配置不得进入REPL。
BOOT-RED-2API连通性验证必须在保存前完成。
FLEX-1验证失败时允许用户跳过但明确告知风险。
"""
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt, Confirm
from rich.progress import Progress, SpinnerColumn, TextColumn
from pathlib import Path
import httpx
import yaml
console = Console()
CONFIG_PATH = Path.home() / ".kwcode" / "config.yaml"
def is_first_run() -> bool:
"""检查是否首次运行config.yaml 不存在)"""
return not CONFIG_PATH.exists()
def load_config() -> dict:
"""读取已有的 config.yaml返回 config dict"""
if not CONFIG_PATH.exists():
return {}
try:
return yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8")) or {}
except Exception:
return {}
def run_onboarding() -> dict:
"""
首次启动引导。返回config dict。
BOOT-RED-1未完成配置不得进入REPL。
"""
_print_welcome()
net = _detect_network_with_progress()
config = _configure_api(net)
config = _ask_telemetry(config)
_save_config(config)
_print_ready(config, net)
return config
def _print_welcome():
console.print()
console.print(Panel(
"[bold cyan]KWCode[/bold cyan] [dim]天工开物[/dim]\n"
"[dim]中国开发者的本地 Coding Agent[/dim]",
border_style="cyan",
padding=(1, 4),
))
console.print()
console.print(" 欢迎使用 KWCode首次使用需要完成以下配置。\n")
def _detect_network_with_progress() -> dict:
"""探测网络,显示进度"""
from kaiwu.core.network import detect_network
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
console=console,
) as progress:
task = progress.add_task(" 检测网络环境...", total=None)
net = detect_network()
progress.update(task, description=" 网络检测完成")
# 显示检测结果
if net["china"]:
console.print(" [yellow]检测到国内网络[/yellow]")
console.print(" [dim]· 搜索增强:自动使用 Bing 中文版[/dim]")
if not net["hf_ok"]:
console.print(" [dim]· 模型下载HuggingFace不可达建议用ModelScope[/dim]")
if net["proxy"]:
console.print(f" [dim]· 代理:{net['proxy']}[/dim]")
else:
console.print(" [green]网络正常[/green],使用 DuckDuckGo 搜索")
console.print()
return net
def _configure_api(net: dict) -> dict:
"""API配置引导"""
console.print(" [bold]配置模型接入[/bold]\n")
# 提示
console.print(" [dim]支持任何 OpenAI 兼容接口:[/dim]")
console.print(" [dim] 本地 Ollama → http://localhost:11434[/dim]")
console.print(" [dim] 本地 llama.cpp → http://localhost:8080[/dim]")
console.print(" [dim] DeepSeek API → https://api.deepseek.com[/dim]")
console.print(" [dim] 其他兼容服务 → 填入对应地址即可[/dim]")
console.print()
while True:
base_url = Prompt.ask(
" API Base URL",
default="http://localhost:11434",
).strip().rstrip("/")
api_key = Prompt.ask(
" API Key [dim](本地模型留空,直接回车)[/dim]",
default="",
password=True,
).strip()
model = Prompt.ask(
" 模型名称",
default="qwen3:8b",
).strip()
console.print()
# 连通性验证BOOT-RED-2
ok, err = _verify_api(base_url, api_key, model)
if ok:
console.print(" [green]✓ 连接成功[/green]")
console.print()
break
else:
console.print(f" [red]✗ 连接失败:{err}[/red]")
console.print(" [dim]请检查地址和Key是否正确[/dim]")
console.print()
# 允许跳过验证FLEX-1
skip = Confirm.ask(
" 网络可能临时抖动,是否跳过验证直接保存?",
default=False,
)
if skip:
console.print(" [yellow]⚠ 已跳过验证,请确认配置正确[/yellow]")
console.print()
break
# 否则重新输入
console.print(" 重新输入配置:\n")
return {
"default": {
"base_url": base_url,
"api_key": api_key,
"model": model,
}
}
def _verify_api(base_url: str, api_key: str, model: str) -> tuple[bool, str]:
"""
验证API连通性。
BOOT-RED-2保存前必须验证。
尝试 /v1/models 或 /api/tagsOllama或 /v1/chat/completions。
"""
headers = {}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
console=console,
) as progress:
progress.add_task(" 验证连接...", total=None)
# 尝试OpenAI兼容接口
for path in ["/v1/models", "/api/tags", "/v1/chat/completions"]:
try:
url = base_url + path
if path == "/v1/chat/completions":
# 发一个最小请求验证模型可用
resp = httpx.post(
url,
headers=headers,
json={
"model": model,
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 1,
},
timeout=10,
)
else:
resp = httpx.get(url, headers=headers, timeout=5)
if resp.status_code == 200:
return True, ""
elif resp.status_code in (401, 403):
return False, f"认证失败({resp.status_code}),请检查 API Key"
elif resp.status_code == 404:
continue # Try next endpoint
elif resp.status_code < 500:
return True, "" # Other 2xx/3xx considered OK
except httpx.ConnectError:
return False, f"无法连接到 {base_url},请确认服务已启动"
except httpx.TimeoutException:
return False, f"连接超时({base_url}),请检查地址"
except Exception:
continue
return False, "API验证失败请检查地址和Key"
def _ask_telemetry(config: dict) -> dict:
"""询问用户是否开启匿名遥测。"""
console.print(" [bold]匿名统计[/bold]\n")
console.print(" [dim]是否帮助改善kwcode[/dim]")
console.print(" [dim]开启后将匿名上传任务成功率统计(不包含任何代码内容)[/dim]")
console.print(" [dim]可随时在设置中关闭。[/dim]")
console.print()
enabled = Confirm.ask(
" 开启匿名统计",
default=False,
)
config["telemetry_enabled"] = enabled
if enabled:
console.print(" [green]✓ 已开启匿名统计[/green]")
else:
console.print(" [dim]已关闭,可随时用 kwcode telemetry enable 开启[/dim]")
console.print()
return config
def _save_config(config: dict):
"""保存配置到 ~/.kwcode/config.yaml"""
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(
yaml.dump(config, allow_unicode=True, default_flow_style=False),
encoding="utf-8",
)
console.print(f" [dim]配置已保存至 {CONFIG_PATH}[/dim]\n")
def _print_ready(config: dict, net: dict):
"""引导完成提示"""
model = config.get("default", {}).get("model", "未配置")
search_src = "DuckDuckGo" if not net["china"] else "Bing 中文版"
console.print(Panel(
f" [green]✓ KWCode 已就绪[/green]\n\n"
f" 模型 {model}\n"
f" 搜索 {search_src}(自动)\n"
f" 数据 完全本地,不出网\n\n"
f" 输入 [cyan]/help[/cyan] 查看所有命令",
border_style="green",
padding=(0, 2),
))
console.print()