@team-agent/installer 0.5.110 → 0.5.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +231 -812
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +493 -451
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -169
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +10 -5
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +53 -109
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +26 -121
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +0 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -298,7 +298,8 @@ mod tests {
|
|
|
298
298
|
#[cfg(unix)]
|
|
299
299
|
#[test]
|
|
300
300
|
fn search_survives_unrelated_spaced_model_ids() {
|
|
301
|
-
let catalog =
|
|
301
|
+
let catalog =
|
|
302
|
+
include_str!("../lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt");
|
|
302
303
|
let path = fixture(&format!("printf '%s' '{catalog}'"));
|
|
303
304
|
let result = cmd_models_with(
|
|
304
305
|
&ModelsArgs {
|
|
@@ -370,7 +371,8 @@ mod tests {
|
|
|
370
371
|
&["models", "--provider", "cursor_agent", "--json"],
|
|
371
372
|
);
|
|
372
373
|
let text = public_cli_text(&output);
|
|
373
|
-
let calls =
|
|
374
|
+
let calls =
|
|
375
|
+
std::fs::read_to_string(agent.program.with_extension("count")).unwrap_or_default();
|
|
374
376
|
drop(agent);
|
|
375
377
|
assert!(output.status.success(), "{text}");
|
|
376
378
|
assert_eq!(calls, "1:--list-models\n");
|
|
@@ -405,12 +407,19 @@ mod tests {
|
|
|
405
407
|
let no_match = run_public_models_cli(
|
|
406
408
|
&grok.program,
|
|
407
409
|
grok.response.as_deref(),
|
|
408
|
-
&[
|
|
410
|
+
&[
|
|
411
|
+
"models",
|
|
412
|
+
"--provider",
|
|
413
|
+
"grok",
|
|
414
|
+
"--search",
|
|
415
|
+
"definitely-absent",
|
|
416
|
+
],
|
|
409
417
|
);
|
|
410
418
|
let json_text = public_cli_text(&json);
|
|
411
419
|
let human_text = public_cli_text(&human);
|
|
412
420
|
let no_match_text = public_cli_text(&no_match);
|
|
413
|
-
let calls =
|
|
421
|
+
let calls =
|
|
422
|
+
std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
414
423
|
drop(grok);
|
|
415
424
|
assert!(json.status.success(), "{json_text}");
|
|
416
425
|
assert!(human.status.success(), "{human_text}");
|
|
@@ -422,10 +431,15 @@ mod tests {
|
|
|
422
431
|
assert_eq!(value["auth_basis"], "catalog_visibility");
|
|
423
432
|
let models = value["models"].as_array().unwrap();
|
|
424
433
|
assert_eq!(
|
|
425
|
-
models
|
|
434
|
+
models
|
|
435
|
+
.iter()
|
|
436
|
+
.map(|model| model["model_id"].as_str().unwrap())
|
|
437
|
+
.collect::<Vec<_>>(),
|
|
426
438
|
vec!["grok-4.7", "grok-4.7-build-fast", "grok-4.5", "grok-4.6"]
|
|
427
439
|
);
|
|
428
|
-
assert!(models.iter().all(|model| model["provider"] == "grok"
|
|
440
|
+
assert!(models.iter().all(|model| model["provider"] == "grok"
|
|
441
|
+
&& model["vendor"] == "xai"
|
|
442
|
+
&& model["aliases"] == json!([])));
|
|
429
443
|
assert_eq!(models[0]["default"], false);
|
|
430
444
|
assert_eq!(models[3]["default"], true);
|
|
431
445
|
assert!(human_text.contains("grok-4.7-build-fast"));
|
|
@@ -471,7 +485,8 @@ mod tests {
|
|
|
471
485
|
&["models", "--provider", "grok", "--json"],
|
|
472
486
|
);
|
|
473
487
|
let text = public_cli_text(&output);
|
|
474
|
-
let calls =
|
|
488
|
+
let calls =
|
|
489
|
+
std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
475
490
|
outcomes.push((name, output.status.success(), text, calls));
|
|
476
491
|
}
|
|
477
492
|
for (name, success, text, calls) in outcomes {
|
|
@@ -481,21 +496,32 @@ mod tests {
|
|
|
481
496
|
assert_eq!(value["schema_version"], "models.v1", "{name}");
|
|
482
497
|
assert_eq!(value["ok"], false, "{name}");
|
|
483
498
|
assert_eq!(value["models"], json!([]), "{name}");
|
|
484
|
-
assert!(
|
|
499
|
+
assert!(
|
|
500
|
+
!value["error"]
|
|
501
|
+
.as_str()
|
|
502
|
+
.unwrap()
|
|
503
|
+
.to_ascii_lowercase()
|
|
504
|
+
.contains("unsupported model provider"),
|
|
505
|
+
"{name}: {text}"
|
|
506
|
+
);
|
|
485
507
|
}
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
#[cfg(unix)]
|
|
489
511
|
#[test]
|
|
490
512
|
fn grok_nonzero_native_command_is_not_a_successful_empty_catalog() {
|
|
491
|
-
let grok = CliFixture::new(fixture_program(
|
|
513
|
+
let grok = CliFixture::new(fixture_program(
|
|
514
|
+
"grok",
|
|
515
|
+
"printf 'private diagnostic' >&2\nexit 7",
|
|
516
|
+
));
|
|
492
517
|
let output = run_public_models_cli(
|
|
493
518
|
&grok.program,
|
|
494
519
|
None,
|
|
495
520
|
&["models", "--provider", "grok", "--json"],
|
|
496
521
|
);
|
|
497
522
|
let text = public_cli_text(&output);
|
|
498
|
-
let calls =
|
|
523
|
+
let calls =
|
|
524
|
+
std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
499
525
|
drop(grok);
|
|
500
526
|
assert!(!output.status.success(), "{text}");
|
|
501
527
|
assert_eq!(calls, "1:models\n");
|
|
@@ -503,7 +529,14 @@ mod tests {
|
|
|
503
529
|
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
504
530
|
assert_eq!(value["ok"], false);
|
|
505
531
|
assert_eq!(value["models"], json!([]));
|
|
506
|
-
assert!(
|
|
532
|
+
assert!(
|
|
533
|
+
value["error"]
|
|
534
|
+
.as_str()
|
|
535
|
+
.unwrap()
|
|
536
|
+
.to_ascii_lowercase()
|
|
537
|
+
.contains("grok"),
|
|
538
|
+
"{text}"
|
|
539
|
+
);
|
|
507
540
|
}
|
|
508
541
|
|
|
509
542
|
#[cfg(unix)]
|
|
@@ -518,10 +551,20 @@ mod tests {
|
|
|
518
551
|
let text = public_cli_text(&output);
|
|
519
552
|
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
520
553
|
assert_eq!(value["ok"], false);
|
|
521
|
-
assert!(
|
|
554
|
+
assert!(
|
|
555
|
+
value["error"]
|
|
556
|
+
.as_str()
|
|
557
|
+
.unwrap()
|
|
558
|
+
.to_ascii_lowercase()
|
|
559
|
+
.contains("grok"),
|
|
560
|
+
"{text}"
|
|
561
|
+
);
|
|
522
562
|
assert!(value["action"].as_str().unwrap().contains("grok"), "{text}");
|
|
523
563
|
assert!(crate::cli::emit::default_help().contains("grok"));
|
|
524
|
-
assert!(crate::cli::spec::command_spec("models")
|
|
564
|
+
assert!(crate::cli::spec::command_spec("models")
|
|
565
|
+
.unwrap()
|
|
566
|
+
.usage
|
|
567
|
+
.contains("grok"));
|
|
525
568
|
|
|
526
569
|
let unsupported = run_public_models_cli(
|
|
527
570
|
&missing.program,
|
|
@@ -533,7 +576,13 @@ mod tests {
|
|
|
533
576
|
drop(missing);
|
|
534
577
|
assert!(!output.status.success());
|
|
535
578
|
assert!(!unsupported.status.success());
|
|
536
|
-
assert!(
|
|
579
|
+
assert!(
|
|
580
|
+
unsupported_value["action"]
|
|
581
|
+
.as_str()
|
|
582
|
+
.unwrap()
|
|
583
|
+
.contains("grok"),
|
|
584
|
+
"{unsupported_text}"
|
|
585
|
+
);
|
|
537
586
|
}
|
|
538
587
|
|
|
539
588
|
#[cfg(unix)]
|
|
@@ -579,7 +628,10 @@ mod tests {
|
|
|
579
628
|
#[cfg(unix)]
|
|
580
629
|
impl CliFixture {
|
|
581
630
|
fn new(program: std::path::PathBuf) -> Self {
|
|
582
|
-
Self {
|
|
631
|
+
Self {
|
|
632
|
+
program,
|
|
633
|
+
response: None,
|
|
634
|
+
}
|
|
583
635
|
}
|
|
584
636
|
}
|
|
585
637
|
|
|
@@ -614,7 +666,10 @@ mod tests {
|
|
|
614
666
|
"--nocapture",
|
|
615
667
|
])
|
|
616
668
|
.env("TEAM_AGENT_MODELS_CHILD", "1")
|
|
617
|
-
.env(
|
|
669
|
+
.env(
|
|
670
|
+
"TEAM_AGENT_MODELS_ARGS",
|
|
671
|
+
serde_json::to_string(args).unwrap(),
|
|
672
|
+
)
|
|
618
673
|
.env("PATH", path.parent().unwrap())
|
|
619
674
|
.env_remove("PI_PROVIDER")
|
|
620
675
|
.env_remove("PI_MODEL")
|
|
@@ -719,7 +774,10 @@ mod tests {
|
|
|
719
774
|
fn with_native_mode<T>(
|
|
720
775
|
mode: &str,
|
|
721
776
|
action: impl FnOnce() -> T,
|
|
722
|
-
) -> (
|
|
777
|
+
) -> (
|
|
778
|
+
T,
|
|
779
|
+
crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
780
|
+
) {
|
|
723
781
|
let receipt = native_timeout_receipt();
|
|
724
782
|
assert!(!receipt.exists());
|
|
725
783
|
let result = crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
@@ -1122,9 +1180,8 @@ mod tests {
|
|
|
1122
1180
|
fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
|
|
1123
1181
|
let fail = native_timeout_fixture();
|
|
1124
1182
|
let started = Instant::now();
|
|
1125
|
-
let (failed_result, fail_observation) =
|
|
1126
|
-
run_catalog(&fail, Duration::from_secs(1), 1024)
|
|
1127
|
-
});
|
|
1183
|
+
let (failed_result, fail_observation) =
|
|
1184
|
+
with_native_mode("exit7", || run_catalog(&fail, Duration::from_secs(1), 1024));
|
|
1128
1185
|
let elapsed = started.elapsed();
|
|
1129
1186
|
assert_eq!(
|
|
1130
1187
|
failed_result.unwrap_err(),
|
|
@@ -1161,9 +1218,8 @@ mod tests {
|
|
|
1161
1218
|
assert_eq!(failed.exit, ExitCode::Error);
|
|
1162
1219
|
assert!(!text.contains("sensitive-token"));
|
|
1163
1220
|
let big = native_timeout_fixture();
|
|
1164
|
-
let (big_result, big_observation) =
|
|
1165
|
-
run_catalog(&big, Duration::from_secs(1), 8)
|
|
1166
|
-
});
|
|
1221
|
+
let (big_result, big_observation) =
|
|
1222
|
+
with_native_mode("oversize", || run_catalog(&big, Duration::from_secs(1), 8));
|
|
1167
1223
|
assert!(big_result.unwrap_err().contains("bounded output"));
|
|
1168
1224
|
assert_eq!(big_observation.spawn_count, 1);
|
|
1169
1225
|
assert_eq!(big_observation.argv, vec!["--list-models"]);
|
|
@@ -1217,7 +1273,7 @@ mod tests {
|
|
|
1217
1273
|
);
|
|
1218
1274
|
assert_eq!(
|
|
1219
1275
|
crate::cli::spec::command_spec("models").unwrap().usage,
|
|
1220
|
-
"
|
|
1276
|
+
"team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]"
|
|
1221
1277
|
);
|
|
1222
1278
|
}
|
|
1223
1279
|
|
|
@@ -71,6 +71,25 @@ pub(crate) struct NamedAddressError {
|
|
|
71
71
|
/// `requested_name`/`suggested_name` without re-parsing. Absent
|
|
72
72
|
/// for exact refusals like empty-workspace.
|
|
73
73
|
pub requested_name: Option<String>,
|
|
74
|
+
// Presentation only: never serialized or used as routing authority.
|
|
75
|
+
human_scope: Option<PathBuf>,
|
|
76
|
+
human_team: Option<String>,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub(crate) fn human_address_reason(reason: &str) -> Option<&'static str> {
|
|
80
|
+
Some(match reason {
|
|
81
|
+
"name_invalid" => "收信队友名不正确;请使用队友列表中的名称。",
|
|
82
|
+
"workspace_not_found" => "所选项目目录不存在;请核对项目路径。",
|
|
83
|
+
"state_not_found" | "workspace_no_state" => {
|
|
84
|
+
"所选项目还没有可用队伍;首次起队用 quick-start,已有队伍恢复用 restart。"
|
|
85
|
+
}
|
|
86
|
+
"team_key_not_found" => "所选项目没有这支队伍;请核对 --workspace 和 --team。",
|
|
87
|
+
"leader_not_attached" => "主控还没有连接到这支队伍;请先体检,按提示恢复连接。",
|
|
88
|
+
"name_not_resolvable" => "当前队伍没有这个队友;请先查看队友列表。",
|
|
89
|
+
"name_not_live" => "该队友当前未运行或连接已失效;请查看状态再恢复。",
|
|
90
|
+
"name_ambiguous" => "有多个同名对象;请明确选择项目和队伍,不猜对象。",
|
|
91
|
+
_ => return None,
|
|
92
|
+
})
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
impl NamedAddressError {
|
|
@@ -86,7 +105,77 @@ impl NamedAddressError {
|
|
|
86
105
|
available_team_keys: Vec::new(),
|
|
87
106
|
suggested_name: None,
|
|
88
107
|
requested_name: None,
|
|
108
|
+
human_scope: None,
|
|
109
|
+
human_team: None,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn with_human_scope(mut self, workspace: Option<&Path>, team: Option<&str>) -> Self {
|
|
114
|
+
self.human_scope = workspace.map(Path::to_path_buf);
|
|
115
|
+
if self.human_team.is_none() {
|
|
116
|
+
self.human_team = team.map(str::to_string);
|
|
89
117
|
}
|
|
118
|
+
self
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub(crate) fn human_guidance(&self, requested: &str) -> (String, String) {
|
|
122
|
+
let workspace = self.human_scope.as_deref();
|
|
123
|
+
let mut message = format!(
|
|
124
|
+
"{}\n你刚才填写的是 {requested:?}。",
|
|
125
|
+
human_address_reason(self.kind.as_str()).unwrap_or("未能找到可用的收信队友。")
|
|
126
|
+
);
|
|
127
|
+
if let Some(workspace) = workspace {
|
|
128
|
+
message.push_str(&format!("\n所选项目:{:?}。", workspace.to_string_lossy()));
|
|
129
|
+
}
|
|
130
|
+
if let Some(team) = self.human_team.as_deref() {
|
|
131
|
+
message.push_str(&format!("\n所选队伍:{team:?}。"));
|
|
132
|
+
}
|
|
133
|
+
let mut action = "没有可靠的拼写建议;请先核对项目和队伍,不猜对象。".to_string();
|
|
134
|
+
for candidate in &self.candidates {
|
|
135
|
+
if let (Some(agent), Some(team)) = (
|
|
136
|
+
candidate.get("agent_id").and_then(Value::as_str),
|
|
137
|
+
candidate.get("team_key").and_then(Value::as_str),
|
|
138
|
+
) {
|
|
139
|
+
message.push_str(&format!(
|
|
140
|
+
"\n候选队伍 {team:?},队友 {agent:?};请确认是否是你要找的对象。"
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if let Some(candidate) = self.suggested_name.as_deref().and_then(|suggested| {
|
|
145
|
+
self.candidates
|
|
146
|
+
.iter()
|
|
147
|
+
.find(|candidate| candidate.get("name").and_then(Value::as_str) == Some(suggested))
|
|
148
|
+
}) {
|
|
149
|
+
if let (Some(workspace), Some(agent), Some(team)) = (
|
|
150
|
+
workspace,
|
|
151
|
+
candidate.get("agent_id").and_then(Value::as_str),
|
|
152
|
+
candidate.get("team_key").and_then(Value::as_str),
|
|
153
|
+
) {
|
|
154
|
+
if !agent.is_empty()
|
|
155
|
+
&& !team.is_empty()
|
|
156
|
+
&& !agent
|
|
157
|
+
.chars()
|
|
158
|
+
.chain(team.chars())
|
|
159
|
+
.chain(workspace.to_string_lossy().chars())
|
|
160
|
+
.any(char::is_control)
|
|
161
|
+
{
|
|
162
|
+
if let Some(command) =
|
|
163
|
+
super::adapters::send_command(agent, workspace, Some(team))
|
|
164
|
+
{
|
|
165
|
+
action = format!("你是否想发给 {agent:?}、队伍 {team:?}?确认后将任务内容替换到这个例子;本次没有发送:\n{command}");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if let Some(workspace) =
|
|
171
|
+
workspace.filter(|path| !path.to_string_lossy().chars().any(char::is_control))
|
|
172
|
+
{
|
|
173
|
+
action.push_str(&format!(
|
|
174
|
+
"\n也可查看可用队友列表:\nteam-agent status --workspace {}",
|
|
175
|
+
super::adapters::shell_quote(&workspace.to_string_lossy())
|
|
176
|
+
));
|
|
177
|
+
}
|
|
178
|
+
(message, action)
|
|
90
179
|
}
|
|
91
180
|
|
|
92
181
|
pub(crate) fn n38_message(&self) -> String {
|
|
@@ -188,9 +277,7 @@ impl NamedAddressError {
|
|
|
188
277
|
})
|
|
189
278
|
})
|
|
190
279
|
.collect();
|
|
191
|
-
let best = ranked
|
|
192
|
-
.first()
|
|
193
|
-
.map(|agent_id| format!("{team}/{agent_id}"));
|
|
280
|
+
let best = ranked.first().map(|agent_id| format!("{team}/{agent_id}"));
|
|
194
281
|
self.with_scoped_suggestions(requested_display, candidate_values, best)
|
|
195
282
|
}
|
|
196
283
|
|
|
@@ -216,9 +303,7 @@ impl NamedAddressError {
|
|
|
216
303
|
// returned candidate (RED-4). Falls back to a status-JSON
|
|
217
304
|
// hint when no candidate cleared the threshold.
|
|
218
305
|
if let Some(best) = best_copyable {
|
|
219
|
-
self.action = format!(
|
|
220
|
-
"Did you mean `{best}`? Rerun with `--to-name {best}`."
|
|
221
|
-
);
|
|
306
|
+
self.action = format!("Did you mean `{best}`? Rerun with `--to-name {best}`.");
|
|
222
307
|
} else {
|
|
223
308
|
self.action = format!(
|
|
224
309
|
"No close matches for `{requested}`. \
|
|
@@ -430,8 +515,14 @@ pub(crate) fn resolve_name_for_cli(
|
|
|
430
515
|
// registry-to-E6 internal helper deliberately pass `None` so
|
|
431
516
|
// caller-supplied `--team` never overrides a full address.
|
|
432
517
|
let parsed = normalize_bare_with_team_scope(parsed, bare_team_scope);
|
|
433
|
-
let
|
|
434
|
-
|
|
518
|
+
let requested_team = match &parsed.target {
|
|
519
|
+
ParsedTarget::TeamEntity { team, .. } => Some(team.as_str()),
|
|
520
|
+
_ => None,
|
|
521
|
+
};
|
|
522
|
+
let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())
|
|
523
|
+
.map_err(|error| error.with_human_scope(None, requested_team))?;
|
|
524
|
+
let state = load_state_checked(&target_workspace)
|
|
525
|
+
.map_err(|error| error.with_human_scope(Some(&target_workspace), requested_team))?;
|
|
435
526
|
let transport = transport_for_cli_target(&target_workspace, &state, &parsed);
|
|
436
527
|
let resolved = resolve_in_workspace(
|
|
437
528
|
sender_workspace,
|
|
@@ -439,7 +530,8 @@ pub(crate) fn resolve_name_for_cli(
|
|
|
439
530
|
&state,
|
|
440
531
|
&parsed,
|
|
441
532
|
transport.as_ref(),
|
|
442
|
-
)
|
|
533
|
+
)
|
|
534
|
+
.map_err(|error| error.with_human_scope(Some(&target_workspace), requested_team))?;
|
|
443
535
|
Ok((resolved, transport))
|
|
444
536
|
}
|
|
445
537
|
|
|
@@ -487,8 +579,8 @@ pub(crate) fn parse_leader_target_workspace_and_team(
|
|
|
487
579
|
match parsed.target {
|
|
488
580
|
ParsedTarget::TeamEntity { team, entity } if entity == "leader" => {
|
|
489
581
|
let state = load_state_checked(&target_workspace)?;
|
|
490
|
-
let canonical =
|
|
491
|
-
.unwrap_or(team);
|
|
582
|
+
let canonical =
|
|
583
|
+
canonical_named_team_key(&target_workspace, &state, &team).unwrap_or(team);
|
|
492
584
|
Ok(Some((target_workspace, canonical)))
|
|
493
585
|
}
|
|
494
586
|
_ => Ok(None),
|
|
@@ -594,7 +686,7 @@ fn resolve_in_workspace(
|
|
|
594
686
|
ParsedTarget::TeamEntity { team, entity } => {
|
|
595
687
|
let canonical = canonical_named_team_key(target_workspace, state, team)
|
|
596
688
|
.unwrap_or_else(|| team.clone());
|
|
597
|
-
if entity == "leader" {
|
|
689
|
+
let result = if entity == "leader" {
|
|
598
690
|
resolve_leader(
|
|
599
691
|
sender_workspace,
|
|
600
692
|
target_workspace,
|
|
@@ -613,7 +705,11 @@ fn resolve_in_workspace(
|
|
|
613
705
|
parsed,
|
|
614
706
|
transport,
|
|
615
707
|
)
|
|
616
|
-
}
|
|
708
|
+
};
|
|
709
|
+
result.map_err(|mut error| {
|
|
710
|
+
error.human_team = Some(canonical);
|
|
711
|
+
error
|
|
712
|
+
})
|
|
617
713
|
}
|
|
618
714
|
ParsedTarget::SessionWindow { session, window } => resolve_session_window(
|
|
619
715
|
sender_workspace,
|
|
@@ -976,11 +1072,7 @@ fn resolve_leader(
|
|
|
976
1072
|
let socket = string_field(receiver, "tmux_socket")
|
|
977
1073
|
.map(str::to_string)
|
|
978
1074
|
.or_else(|| transport.tmux_endpoint());
|
|
979
|
-
match crate::messaging::resolve_live_leader_channel(
|
|
980
|
-
target_workspace,
|
|
981
|
-
receiver,
|
|
982
|
-
transport,
|
|
983
|
-
) {
|
|
1075
|
+
match crate::messaging::resolve_live_leader_channel(target_workspace, receiver, transport) {
|
|
984
1076
|
crate::messaging::LeaderChannelResolution::Live(
|
|
985
1077
|
crate::messaging::LiveLeaderChannel::DirectTmux(channel),
|
|
986
1078
|
) => Ok(ResolvedNamedAddress {
|
|
@@ -1194,8 +1286,8 @@ fn transport_for_cli_target(
|
|
|
1194
1286
|
parsed: &ParsedNamedAddress,
|
|
1195
1287
|
) -> Box<dyn Transport> {
|
|
1196
1288
|
if let ParsedTarget::TeamEntity { team, entity } = &parsed.target {
|
|
1197
|
-
let canonical =
|
|
1198
|
-
.unwrap_or_else(|| team.clone());
|
|
1289
|
+
let canonical =
|
|
1290
|
+
canonical_named_team_key(target_workspace, state, team).unwrap_or_else(|| team.clone());
|
|
1199
1291
|
if entity == "leader" {
|
|
1200
1292
|
let team_scoped_socket = team_entry(state, &canonical)
|
|
1201
1293
|
.and_then(|entry| entry.get("leader_receiver"))
|
|
@@ -1302,11 +1394,8 @@ fn canonical_named_team_key(
|
|
|
1302
1394
|
if team_entry(state, requested).is_some() || is_legacy_single_team_active(state, requested) {
|
|
1303
1395
|
return Some(requested.to_string());
|
|
1304
1396
|
}
|
|
1305
|
-
let selected =
|
|
1306
|
-
target_workspace,
|
|
1307
|
-
Some(requested),
|
|
1308
|
-
)
|
|
1309
|
-
.ok()?;
|
|
1397
|
+
let selected =
|
|
1398
|
+
crate::state::projection::select_runtime_state(target_workspace, Some(requested)).ok()?;
|
|
1310
1399
|
let canonical = selected
|
|
1311
1400
|
.get("active_team_key")
|
|
1312
1401
|
.and_then(Value::as_str)
|
|
@@ -5,7 +5,7 @@ use crate::provider::argv_route::{self, Config, Mutation, Override, RouteError};
|
|
|
5
5
|
use crate::provider::Provider;
|
|
6
6
|
use serde_json::{json, Value};
|
|
7
7
|
|
|
8
|
-
pub(crate) const HELP: &str = "
|
|
8
|
+
pub(crate) const HELP: &str = "做什么:\n设置工具启动时额外使用的命令行参数,默认关闭。\n用法:team-agent route [status|enable|disable|show [TOOL]|set TOOL -- ARG...|add TOOL -- ARG...|clear TOOL] [--json]\n\n怎么用:\nstatus 看开关;enable 开启;disable 关闭;show 看全部或指定工具的参数。\nset 替换参数;add 追加参数;clear 清除指定工具的参数;--json 给程序读取。\nTOOL 是已支持的工具名称,如 pi/codex/claude/copilot/grok/cursor_agent。\n第一个 -- 后全是工具的字面参数,包含 --help/--json 也不会被这里解析。\n配置保存在 ~/.team-agent/argv-routing.json;影响全机,不按项目或队伍隔离。\nTEAM_AGENT_CLI_ARGV_ROUTING 环境变量优先于保存的开关。\nExamples(可复制):\nteam-agent route status\nteam-agent route set pi -- --mode text\nteam-agent route enable\n\n下一步(Next Action):\n用 route show pi 核对参数;仅影响下一次启动,不改变正在运行的对话。\n没有开启或没有配置时,工具仍按原参数启动;不了解参数用途时保持关闭。";
|
|
9
9
|
|
|
10
10
|
struct Request {
|
|
11
11
|
operation: String,
|
|
@@ -16,10 +16,9 @@ struct Request {
|
|
|
16
16
|
|
|
17
17
|
fn usage() -> RouteError {
|
|
18
18
|
RouteError {
|
|
19
|
-
error: "
|
|
19
|
+
error: "参数不完整或不正确;set/add 需要工具名、-- 和至少一个工具参数".to_string(),
|
|
20
20
|
reason: "argv_route_usage",
|
|
21
|
-
action: "
|
|
22
|
-
.to_string(),
|
|
21
|
+
action: "请按 team-agent route --help 的 Examples 填写;例如 team-agent route set pi -- --mode text".to_string(),
|
|
23
22
|
config_path: None,
|
|
24
23
|
}
|
|
25
24
|
}
|
|
@@ -119,7 +118,7 @@ fn execute(request: &Request) -> Result<Value, RouteError> {
|
|
|
119
118
|
}
|
|
120
119
|
if override_state != Override::Unset {
|
|
121
120
|
value["notice"] = json!(format!(
|
|
122
|
-
"
|
|
121
|
+
"当前开关由环境变量 {} 决定({})",
|
|
123
122
|
argv_route::ENV_NAME,
|
|
124
123
|
override_state.status()
|
|
125
124
|
));
|
|
@@ -168,6 +167,9 @@ fn emit_error(error: RouteError, args: &[String], delimiter: usize, as_json: boo
|
|
|
168
167
|
.map(String::as_str)
|
|
169
168
|
.unwrap_or("status"));
|
|
170
169
|
}
|
|
170
|
+
if is_usage {
|
|
171
|
+
value["next_actions"] = json!(["team-agent route --help"]);
|
|
172
|
+
}
|
|
171
173
|
if let Some(text) = super::emit(&CmdOutput::Json(value), as_json) {
|
|
172
174
|
if as_json {
|
|
173
175
|
println!("{text}");
|
|
@@ -176,6 +178,9 @@ fn emit_error(error: RouteError, args: &[String], delimiter: usize, as_json: boo
|
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
if is_usage {
|
|
181
|
+
if !as_json {
|
|
182
|
+
eprintln!("\n{HELP}");
|
|
183
|
+
}
|
|
179
184
|
ExitCode::Usage
|
|
180
185
|
} else {
|
|
181
186
|
ExitCode::Error
|
|
@@ -20,7 +20,7 @@ pub(super) fn watch_notice_json(target: &MessageTarget, opts: &SendOptions) -> V
|
|
|
20
20
|
"watcher_id": format!("watch-{agent_id}"),
|
|
21
21
|
"task_id": opts.task_id.as_ref().map(|t| t.as_str().to_string()),
|
|
22
22
|
"agent_id": agent_id,
|
|
23
|
-
"notice": "
|
|
23
|
+
"notice": "队友回复后会通知主控;用 team-agent inbox leader -n 3 查看。"
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -183,39 +183,56 @@ pub(super) fn cmd_send_result(value: Value, as_json: bool) -> CmdResult {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
pub(super) fn send_human_output(value: &Value) -> String {
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
186
|
+
let agent = send_human_target(value);
|
|
187
|
+
if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
188
|
+
let reason = value.get("reason").and_then(Value::as_str).unwrap_or("");
|
|
189
|
+
let explanation =
|
|
190
|
+
crate::cli::named_address::human_address_reason(reason).unwrap_or(match reason {
|
|
191
|
+
"target_not_in_team" | "unknown_recipient" => "当前队伍没有这个队友。",
|
|
192
|
+
"missing_permissions" | "human_confirmation_required" => {
|
|
193
|
+
"发送被拒绝:需要先确认交流权限。"
|
|
194
|
+
}
|
|
195
|
+
"recipient_busy" | "recipient_pane_in_non_input_mode" => {
|
|
196
|
+
"队友当前不能接收任务;请先查看其状态。"
|
|
197
|
+
}
|
|
198
|
+
"routing_ambiguous" | "empty_target_list" | "ambiguous" => {
|
|
199
|
+
"没有明确选定收信队友;请填写队友名和任务内容。"
|
|
200
|
+
}
|
|
201
|
+
"team_owner_mismatch" | "PaneWorkspaceMismatch" | "session_drift" => {
|
|
202
|
+
"队伍或终端归属不匹配;已停止发送。"
|
|
203
|
+
}
|
|
204
|
+
"coordinator_unavailable" | "tmux_target_missing" => {
|
|
205
|
+
"队伍连接暂不可用;本次没有确认送达。"
|
|
206
|
+
}
|
|
207
|
+
"message_already_claimed" | "duplicate" => {
|
|
208
|
+
"这条消息已由其他发送处理接收;不要重复发送。"
|
|
209
|
+
}
|
|
210
|
+
"no_caller_pane" => "当前终端没有可用的队伍连接;请在主控的命令行/工具上下文发送。",
|
|
211
|
+
_ => value
|
|
212
|
+
.get("error")
|
|
213
|
+
.and_then(Value::as_str)
|
|
214
|
+
.unwrap_or("发送未成功;没有确认任务已送达。"),
|
|
215
|
+
});
|
|
216
|
+
let mut out = format!("{explanation}\n下一步:先运行 team-agent status,使用列表中的队友名;连接问题用 team-agent doctor --workspace .。不要反复重发。");
|
|
217
|
+
if let Some(suggested) = value
|
|
218
|
+
.get("suggested_name")
|
|
219
|
+
.and_then(Value::as_str)
|
|
220
|
+
.filter(|name| !name.is_empty() && !name.contains(['/', ':', ',', '\n', '\r']))
|
|
221
|
+
{
|
|
222
|
+
out.push_str(&format!(
|
|
223
|
+
"\n你是否想发给 {suggested}?确认后使用这个队友名。"
|
|
224
|
+
));
|
|
195
225
|
}
|
|
226
|
+
return out;
|
|
196
227
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// surface it verbatim in human output so users can copy the
|
|
200
|
-
// right short id. `requested_name` echoes the typo, `suggested_
|
|
201
|
-
// name` is the copyable canonical.
|
|
202
|
-
if let Some(requested) = value
|
|
203
|
-
.get("requested_name")
|
|
204
|
-
.and_then(Value::as_str)
|
|
205
|
-
.filter(|s| !s.is_empty())
|
|
206
|
-
{
|
|
207
|
-
parts.push(format!("requested_name: {requested}"));
|
|
228
|
+
if value.get("status").and_then(Value::as_str) == Some("stored_only") {
|
|
229
|
+
return format!("已给{agent}留下消息;本次没有发送到当前对话。\n下一步:用 team-agent inbox leader -n 3 查看回复;留言不保证自动执行。");
|
|
208
230
|
}
|
|
209
|
-
if
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
parts.push(format!(
|
|
215
|
-
"Did you mean `{suggested}`? suggested_name: {suggested}"
|
|
216
|
-
));
|
|
231
|
+
if value.get("delivered").and_then(Value::as_bool) == Some(true) {
|
|
232
|
+
format!("已发给{agent}。{}", crate::cli::SEND_REMINDER)
|
|
233
|
+
} else {
|
|
234
|
+
"任务已收下,但还未送到队友的对话。\n下一步:查看 team-agent status 或 team-agent doctor --workspace .,等真实回复,不反复重发。".to_string()
|
|
217
235
|
}
|
|
218
|
-
parts.join(" ")
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
pub(super) fn send_human_field(value: &Value, key: &str) -> String {
|
|
@@ -227,11 +244,14 @@ pub(super) fn send_human_field(value: &Value, key: &str) -> String {
|
|
|
227
244
|
}
|
|
228
245
|
|
|
229
246
|
pub(super) fn send_human_target(value: &Value) -> String {
|
|
230
|
-
|
|
231
|
-
.
|
|
232
|
-
.
|
|
233
|
-
.
|
|
234
|
-
|
|
247
|
+
value
|
|
248
|
+
.get("agent_id")
|
|
249
|
+
.and_then(Value::as_str)
|
|
250
|
+
.filter(|name| {
|
|
251
|
+
!name.is_empty() && *name != "*" && !name.contains(['/', ':', ',', '\n', '\r'])
|
|
252
|
+
})
|
|
253
|
+
.map(str::to_string)
|
|
254
|
+
.unwrap_or_else(|| "该队友".to_string())
|
|
235
255
|
}
|
|
236
256
|
|
|
237
257
|
pub(super) fn send_human_status(value: &Value) -> String {
|
|
@@ -265,13 +285,12 @@ pub(super) fn send_reminder_for_value(value: &Value) -> &'static str {
|
|
|
265
285
|
let delivered = value.get("delivered").and_then(Value::as_bool);
|
|
266
286
|
let status = value.get("status").and_then(Value::as_str);
|
|
267
287
|
let delivery_status = value.get("delivery_status").and_then(Value::as_str);
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
{
|
|
272
|
-
"Message queued; coordinator will notify when the worker receives it. Do not poll the worker terminal with capture-pane."
|
|
273
|
-
} else {
|
|
288
|
+
if status == Some("stored_only") {
|
|
289
|
+
"本次只留言,没有发送到当前对话;留言不保证自动执行。"
|
|
290
|
+
} else if delivered == Some(true) && delivery_status != Some("pending") {
|
|
274
291
|
crate::cli::SEND_REMINDER
|
|
292
|
+
} else {
|
|
293
|
+
"任务已收下,但还未确认送到队友对话。查看 team-agent status/doctor,等真实回复,不反复重发。"
|
|
275
294
|
}
|
|
276
295
|
}
|
|
277
296
|
|