// `--review` 的执行流程:写请求体和 curl 配置 → 调 curl → 读响应 → 删临时文件。
//
// 只有这一层碰环境和文件系统;怎么造请求、怎么解响应都在库包里,可以离线测。
// 密钥从环境变量读,绝不从命令行参数走——命令行在整机进程列表里可见。
//
// 临时文件放在当前目录(点开头的隐藏文件),跑完就删。用相对路径是因为
// Windows 的反斜杠会被 curl 配置当成转义符,固定文件名又让要执行的命令
// 变成一个不含任何外部输入的常量字符串。
///|
/// 密钥变量的读取顺序:自己的在前,通用的在后。
let key_vars : Array[String] = ["MOONREVIEW_API_KEY", "OPENAI_API_KEY"]
///|
fn nonempty_env(name : String) -> String? {
match @env.get_env_var(name) {
Some(v) => if v == "" { None } else { Some(v) }
None => None
}
}
///|
/// 按顺序找第一个非空的变量;`--review` 之外的地方不读环境,避免误用。
fn first_env(names : Array[String]) -> String? {
for n in names {
match nonempty_env(n) {
Some(v) => return Some(v)
None => ()
}
}
None
}
///|
/// 读不到就当没有:curl 没跑起来时响应文件本来就不存在,不该让它变成崩溃。
fn try_read(path : String) -> String? {
Some(read_file(path)) catch {
_ => None
}
}
///|
/// (是否成功, 给人看的输出)。失败原因也走这个元组,好让调用方统一决定退出码。
pub fn ask_for_review(d : @diff.Diff) -> (Bool, String) raise IoError {
let api_key = match first_env(key_vars) {
Some(k) => k
None =>
return (
false, "没有 API Key:先 export MOONREVIEW_API_KEY=...(也认 OPENAI_API_KEY)",
)
}
let base_url = @diff.resolve_base_url(nonempty_env("MOONREVIEW_BASE_URL"))
let model = @diff.resolve_model(nonempty_env("MOONREVIEW_MODEL"))
write_text_file(@diff.body_file, d.chat_body(model))
write_text_file(@diff.config_file, @diff.curl_config(base_url, api_key))
let rc = run_command(@diff.curl_command)
let response = try_read(@diff.response_file)
remove_file(@diff.config_file)
remove_file(@diff.body_file)
remove_file(@diff.response_file)
guard rc == 0 else {
return (
false,
"curl 退出码 \{rc}:检查网络、代理,或者本机有没有 curl",
)
}
match response {
None => (false, "curl 没有写出响应文件 \{@diff.response_file}")
Some(text) => @diff.chat_reply(text)
}
}