CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
200 lines
7.1 KiB
Rust
200 lines
7.1 KiB
Rust
//! 实例 / 版本 / Java 三个只读工具(M39 任务 3 的读侧)。
|
||
//!
|
||
//! 全部只读、全部走 [`crate::port::GamePort`]:agent 不自己去翻 `versions/` 目录,
|
||
//! 也不自己扫 JDK——那两件事的真相源在 M14(game)与 M33(java),重复一份必然漂。
|
||
//!
|
||
//! 输出形状刻意做成「摘要 + 上限」:实例可能几十个、Java 候选可能十几个,
|
||
//! 一次全塞给模型等于烧 token(02 §3.2 的「单次返回上限防 token 爆炸」同理)。
|
||
|
||
use std::sync::Arc;
|
||
|
||
use serde_json::{json, Value};
|
||
|
||
use crate::port::GamePort;
|
||
use crate::registry::schema::{integer, object};
|
||
use crate::registry::{
|
||
RegistryError, ToolCtx, ToolError, ToolFn, ToolOutput, ToolRegistry, ToolSpec,
|
||
};
|
||
|
||
/// 实例列表工具名。
|
||
pub const LIST_INSTANCES: &str = "list_instances";
|
||
/// 版本列表工具名。
|
||
pub const LIST_VERSIONS: &str = "list_versions";
|
||
/// Java 现状工具名。
|
||
pub const GET_JAVA_STATUS: &str = "get_java_status";
|
||
/// 单次最多返回多少个实例/版本。
|
||
pub const MAX_ROWS: usize = 50;
|
||
|
||
/// 注册三个只读工具。
|
||
///
|
||
/// # Errors
|
||
/// 工具名重复/schema 非法时返回 [`RegistryError`]。
|
||
pub fn register(registry: &mut ToolRegistry, port: Arc<dyn GamePort>) -> Result<(), RegistryError> {
|
||
registry.register(list_instances_spec(Arc::clone(&port)))?;
|
||
registry.register(list_versions_spec(Arc::clone(&port)))?;
|
||
registry.register(java_status_spec(port))
|
||
}
|
||
|
||
fn limit_arg(args: &Value) -> usize {
|
||
args.get("limit")
|
||
.and_then(Value::as_u64)
|
||
.map_or(MAX_ROWS, |n| usize::try_from(n).unwrap_or(MAX_ROWS))
|
||
.clamp(1, MAX_ROWS)
|
||
}
|
||
|
||
fn limit_schema() -> Value {
|
||
integer(
|
||
&format!("最多返回多少条(1~{MAX_ROWS},默认 {MAX_ROWS})"),
|
||
1,
|
||
MAX_ROWS as i64,
|
||
)
|
||
}
|
||
|
||
fn list_instances_spec(port: Arc<dyn GamePort>) -> ToolSpec {
|
||
let handler: ToolFn = Arc::new(move |_ctx: &ToolCtx, args: &Value| {
|
||
let limit = limit_arg(args);
|
||
let mut instances = port.list_instances().map_err(ToolError::Port)?;
|
||
let total = instances.len();
|
||
instances.truncate(limit);
|
||
Ok(ToolOutput::ok(
|
||
json!({
|
||
"total": total,
|
||
"shown": instances.len(),
|
||
"truncated": total > instances.len(),
|
||
"instances": instances,
|
||
}),
|
||
format!("列出 {}/{} 个实例", instances.len(), total),
|
||
))
|
||
});
|
||
|
||
ToolSpec::read_only(
|
||
LIST_INSTANCES,
|
||
"列出启动器里的游戏实例(id / 名称 / 版本 id / 载荷是否已安装 / 独立游戏目录 / 实例级 Java)。要看某个实例的日志先用 list_game_sessions。",
|
||
object(&[("limit", limit_schema())], &[]),
|
||
handler,
|
||
)
|
||
}
|
||
|
||
fn list_versions_spec(port: Arc<dyn GamePort>) -> ToolSpec {
|
||
let handler: ToolFn = Arc::new(move |_ctx: &ToolCtx, args: &Value| {
|
||
let limit = limit_arg(args);
|
||
let mut versions = port.list_versions().map_err(ToolError::Port)?;
|
||
let total = versions.len();
|
||
versions.truncate(limit);
|
||
Ok(ToolOutput::ok(
|
||
json!({
|
||
"total": total,
|
||
"shown": versions.len(),
|
||
"truncated": total > versions.len(),
|
||
"versions": versions,
|
||
}),
|
||
format!("列出 {}/{} 个已安装版本", versions.len(), total),
|
||
))
|
||
});
|
||
|
||
ToolSpec::read_only(
|
||
LIST_VERSIONS,
|
||
"列出已安装的游戏版本(`versions/<id>/` 下真实存在的载荷),带类型与被几个实例引用。判断「实例的载荷缺没缺」时配合 list_instances 的 version_present 一起看。",
|
||
object(&[("limit", limit_schema())], &[]),
|
||
handler,
|
||
)
|
||
}
|
||
|
||
fn java_status_spec(port: Arc<dyn GamePort>) -> ToolSpec {
|
||
let handler: ToolFn = Arc::new(move |_ctx: &ToolCtx, _args: &Value| {
|
||
let status = port.java_status().map_err(ToolError::Port)?;
|
||
let majors: Vec<String> = status
|
||
.candidates
|
||
.iter()
|
||
.filter_map(|c| c.major.map(|m| m.to_string()))
|
||
.collect();
|
||
Ok(ToolOutput::ok(
|
||
json!({
|
||
"default_memory_mb": status.default_memory_mb,
|
||
"managed_roots": status.managed_roots,
|
||
"candidate_count": status.candidates.len(),
|
||
"majors": majors,
|
||
"candidates": status.candidates,
|
||
}),
|
||
format!(
|
||
"Java 候选 {} 个(主版本 {})",
|
||
status.candidates.len(),
|
||
if majors.is_empty() {
|
||
"无".to_string()
|
||
} else {
|
||
majors.join("/")
|
||
}
|
||
),
|
||
))
|
||
});
|
||
|
||
ToolSpec::read_only(
|
||
GET_JAVA_STATUS,
|
||
"读取 Java 现状:托管扫描根、默认堆内存、探测到的全部运行时(路径 / 版本 / 主版本 / 发行方 / 来源)。启动报「不支持的 class 版本」「找不到 Java」时先看这里。",
|
||
object(&[], &[]),
|
||
handler,
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::testing::MockGame;
|
||
|
||
fn registry() -> ToolRegistry {
|
||
let mut registry = ToolRegistry::new();
|
||
register(&mut registry, Arc::new(MockGame::new())).expect("注册应成功");
|
||
registry
|
||
}
|
||
|
||
fn call(registry: &ToolRegistry, name: &str, args: Value) -> Value {
|
||
let spec = registry.get(name).expect("工具存在").clone();
|
||
let ctx = ToolCtx {
|
||
agent_session_id: "s1".to_string(),
|
||
confirmed: false,
|
||
};
|
||
(spec.handler)(&ctx, &args).expect("应成功").content
|
||
}
|
||
|
||
#[test]
|
||
fn lists_instances_with_limit() {
|
||
let content = call(®istry(), LIST_INSTANCES, json!({ "limit": 1 }));
|
||
assert_eq!(content["total"], json!(1));
|
||
assert_eq!(content["instances"][0]["id"], json!("inst-1"));
|
||
assert_eq!(content["truncated"], json!(false));
|
||
}
|
||
|
||
#[test]
|
||
fn lists_versions() {
|
||
let content = call(®istry(), LIST_VERSIONS, json!({}));
|
||
assert_eq!(content["total"], json!(2));
|
||
assert_eq!(content["versions"][0]["id"], json!("1.20.1-forge-47.2.20"));
|
||
}
|
||
|
||
#[test]
|
||
fn java_status_summarises_majors() {
|
||
let content = call(®istry(), GET_JAVA_STATUS, json!({}));
|
||
assert_eq!(content["candidate_count"], json!(2));
|
||
assert_eq!(content["majors"], json!(["17", "21"]));
|
||
assert_eq!(content["default_memory_mb"], json!(2048));
|
||
}
|
||
|
||
#[test]
|
||
fn port_errors_are_passed_through() {
|
||
let mut registry = ToolRegistry::new();
|
||
register(
|
||
&mut registry,
|
||
Arc::new(MockGame::failing("versions 目录不可读")),
|
||
)
|
||
.expect("注册应成功");
|
||
let spec = registry.get(LIST_VERSIONS).expect("工具存在").clone();
|
||
let ctx = ToolCtx {
|
||
agent_session_id: "s1".to_string(),
|
||
confirmed: false,
|
||
};
|
||
let err = (spec.handler)(&ctx, &json!({})).unwrap_err();
|
||
assert_eq!(err.code(), "port_error");
|
||
assert!(err.message().contains("versions 目录不可读"));
|
||
}
|
||
}
|