feat(onboarding/upgrade-channel): 全托管文件升级通道 + 批量重分发(U1) #4
@ -372,7 +372,7 @@ modules:
|
|||||||
area: backend
|
area: backend
|
||||||
status: in_progress
|
status: in_progress
|
||||||
progress: 99
|
progress: 99
|
||||||
note: "体系分发主线:T1/T2/B1/B2/B3/V1/U3/B4 ✅(2026-07-15:U3 Rules-Applied ingest 层校验+回溯清洗;B4 沉默项目回传覆盖率诊断);剩 📋 U1(全托管升级通道+批量重分发)"
|
note: "体系分发主线全卡完成:…/V1/U3/B4/U1 ✅(2026-07-15 择升环三断点修复收官:U3 规则校验 / B4 沉默项目信号 / U1 全托管升级+批量重分发)。U1 批量模式 MCP 实调待重建安装后终验,通过后模块收官 done"
|
||||||
position: [3500, 0]
|
position: [3500, 0]
|
||||||
updated: 2026-07-15
|
updated: 2026-07-15
|
||||||
|
|
||||||
|
|||||||
@ -134,8 +134,9 @@
|
|||||||
- 版本化闭环要求(2026-07-03 用户澄清,体系分发的灵魂): 分发不是"发了就不管"——每类资产带版本分发 → 子项目按该版本实践产生经验(成功/失败)→ 经验自吸反哺炼境(trailer/failure_cases/复盘/返工率)→ 按版本分组对比效果 → 修订出新版 → 再分发。CONVENTIONS 是模范生(version+队列对比+梦核+同步,1.8→1.9 已跑通一整圈);B 系列每类资产按此标准建闭环。**前置依赖提升**:「接入包模板演进分发(MANAGED 块化)」concept 卡——模板无版本机制是当前最大缺口,没有它体系 v2 到不了存量项目
|
- 版本化闭环要求(2026-07-03 用户澄清,体系分发的灵魂): 分发不是"发了就不管"——每类资产带版本分发 → 子项目按该版本实践产生经验(成功/失败)→ 经验自吸反哺炼境(trailer/failure_cases/复盘/返工率)→ 按版本分组对比效果 → 修订出新版 → 再分发。CONVENTIONS 是模范生(version+队列对比+梦核+同步,1.8→1.9 已跑通一整圈);B 系列每类资产按此标准建闭环。**前置依赖提升**:「接入包模板演进分发(MANAGED 块化)」concept 卡——模板无版本机制是当前最大缺口,没有它体系 v2 到不了存量项目
|
||||||
- notes: 2026-07-03 概念探讨收束后的分发需求(体系详见 memory concept_dual_projection 与 mcp-server 模块 concept 卡)。用户全 agent 开发形态下,"给开发者立规范"="给 agent 注入规范",可完全机器化。四条现成管道分发四类资产:① B1(S) CONVENTIONS 1.10.0 加 R12 边界单点实现 + R13 验收须真实走通验证路径 + 载体选择原则;② B2(S) 接入包 AGENTS.md 模板加 agent 按钮章节(三要素/载体谱系/侦察驻军);③ B3(M) init/sync 提示词升级——建蓝图时声明项目按钮载体、acceptance 生成为可执行验证路径格式;④ B4(S,后置) 健康诊断加体系遵守度检查。移植体验=零新增动作,接入流程不变。
|
- notes: 2026-07-03 概念探讨收束后的分发需求(体系详见 memory concept_dual_projection 与 mcp-server 模块 concept 卡)。用户全 agent 开发形态下,"给开发者立规范"="给 agent 注入规范",可完全机器化。四条现成管道分发四类资产:① B1(S) CONVENTIONS 1.10.0 加 R12 边界单点实现 + R13 验收须真实走通验证路径 + 载体选择原则;② B2(S) 接入包 AGENTS.md 模板加 agent 按钮章节(三要素/载体谱系/侦察驻军);③ B3(M) init/sync 提示词升级——建蓝图时声明项目按钮载体、acceptance 生成为可执行验证路径格式;④ B4(S,后置) 健康诊断加体系遵守度检查。移植体验=零新增动作,接入流程不变。
|
||||||
|
|
||||||
### 🔵 U1. 全托管文件升级通道 + 批量重分发(择升环断点修复 ①)
|
### ✅ U1. 全托管文件升级通道 + 批量重分发(择升环断点修复 ①)
|
||||||
- status: planned
|
- status: done
|
||||||
|
- done_note: 2026-07-15 完成(分支 feat/onboarding-pack-u1 → PR)。dispatch_fully_managed 三态(written/skipped/updated,CRLF 归一化防覆盖抖动),envoy.md/blueprint-gate.cjs 切换全托管并加「由炼境托管」声明头;verify_onboarding 新增第 7 项「lefthook 检查段齐全」——期望键集合从内嵌 TMPL_LEFTHOOK 自省提取(lefthook_command_keys 纯函数,注释行不算键),零硬编码;MCP apply_onboarding_pack 支持批量模式(project_id 字段缺失→全部已登记项目,空串仍按无效单项目报错),schema required 放宽为空并同步测试断言(契约放宽属验收要求,已声明)。cargo test 102 全过。**遗留待终验**:批量模式 MCP 实调需重建安装炼境后执行(模板在二进制资源里,同 V1 先例);batch_apply.py/scan_onboarded.py 暂保留(用户保留意见),实调终验通过后再清理
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: onboarding-pack(卡 T1)
|
- depends: onboarding-pack(卡 T1)
|
||||||
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/resources/onboarding/.claude/commands/envoy.md, src-tauri/resources/onboarding/.claude/hooks/blueprint-gate.cjs
|
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/resources/onboarding/.claude/commands/envoy.md, src-tauri/resources/onboarding/.claude/hooks/blueprint-gate.cjs
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
<!-- 由炼境托管:重跑接入包会用最新模板覆盖本文件,请勿在此做项目定制 -->
|
||||||
|
|
||||||
你是**炼境使者**——在子项目会话里代表炼境执行现场感知与回传。
|
你是**炼境使者**——在子项目会话里代表炼境执行现场感知与回传。
|
||||||
|
|
||||||
直接执行以下步骤,无需用户提供任何输入。
|
直接执行以下步骤,无需用户提供任何输入。
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
// 由炼境托管:重跑接入包会用最新模板覆盖本文件,请勿在此做项目定制。
|
||||||
// 蓝图门控:pre-commit 检查未关闭的 🔵 任务卡,warn only(exit 0,不阻断提交)。
|
// 蓝图门控:pre-commit 检查未关闭的 🔵 任务卡,warn only(exit 0,不阻断提交)。
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|||||||
@ -62,6 +62,38 @@ fn dispatch_one(root: &Path, relative: &str, content: &str) -> TemplateResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 全托管文件分发(三档升级语义之②,U1):炼境独占管理、用户不定制(文件头有托管声明)。
|
||||||
|
/// 不存在→written;与内嵌模板一致→skipped;不一致→覆盖 updated。
|
||||||
|
/// 比较前归一化行尾(子项目 git autocrlf 可能把 LF 检出为 CRLF,避免永久 updated 抖动)。
|
||||||
|
fn dispatch_fully_managed(root: &Path, relative: &str, content: &str) -> TemplateResult {
|
||||||
|
let target = root.join(relative);
|
||||||
|
if !target.exists() {
|
||||||
|
return dispatch_one(root, relative, content);
|
||||||
|
}
|
||||||
|
let normalize = |s: &str| s.replace("\r\n", "\n");
|
||||||
|
if let Ok(existing) = std::fs::read_to_string(&target) {
|
||||||
|
if normalize(&existing) == normalize(content) {
|
||||||
|
return TemplateResult {
|
||||||
|
name: relative.to_string(),
|
||||||
|
action: "skipped".into(),
|
||||||
|
detail: "内容已是最新模板".into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match std::fs::write(&target, content) {
|
||||||
|
Ok(_) => TemplateResult {
|
||||||
|
name: relative.to_string(),
|
||||||
|
action: "updated".into(),
|
||||||
|
detail: "全托管文件已覆盖为最新模板".into(),
|
||||||
|
},
|
||||||
|
Err(e) => TemplateResult {
|
||||||
|
name: relative.to_string(),
|
||||||
|
action: "failed".into(),
|
||||||
|
detail: format!("覆盖失败: {e}"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 从模板中提取 ONBOARDING_MANAGED 块(含首尾标记行)。
|
/// 从模板中提取 ONBOARDING_MANAGED 块(含首尾标记行)。
|
||||||
fn extract_managed_block(template: &str) -> Option<&str> {
|
fn extract_managed_block(template: &str) -> Option<&str> {
|
||||||
let start = template.find(MANAGED_START)?;
|
let start = template.find(MANAGED_START)?;
|
||||||
@ -147,8 +179,8 @@ pub fn dispatch_templates(project_path: &str) -> Vec<TemplateResult> {
|
|||||||
dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS),
|
dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS),
|
||||||
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
|
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
|
||||||
dispatch_versioned(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW),
|
dispatch_versioned(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW),
|
||||||
dispatch_one(root, ".claude/commands/envoy.md", TMPL_ENVOY),
|
dispatch_fully_managed(root, ".claude/commands/envoy.md", TMPL_ENVOY),
|
||||||
dispatch_one(root, ".claude/hooks/blueprint-gate.cjs", TMPL_BLUEPRINT_GATE),
|
dispatch_fully_managed(root, ".claude/hooks/blueprint-gate.cjs", TMPL_BLUEPRINT_GATE),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,7 +349,59 @@ fn check_origin(origin_host: Option<&str>, instance_hosts: &[String]) -> (bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 接入生效验收:五项检查链。分发成功 ≠ 功能生效,本函数验证"真的用起来了"。
|
/// 从 lefthook.yml 内容提取 commands 键集合(` commands:` 下缩进 4 空格的键行)。
|
||||||
|
/// 纯函数;注释与更深缩进(run: | 多行体等)忽略。用于 U1 检查项自省——
|
||||||
|
/// 期望键集合取自内嵌模板,禁止硬编码具体检查项名(模板演进校验器零维护)。
|
||||||
|
pub fn lefthook_command_keys(yml: &str) -> Vec<String> {
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
let mut in_commands = false;
|
||||||
|
for line in yml.lines() {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let indent = line.len() - trimmed.len();
|
||||||
|
if indent == 2 && trimmed.trim_end() == "commands:" {
|
||||||
|
in_commands = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if indent < 4 {
|
||||||
|
in_commands = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if in_commands && indent == 4 {
|
||||||
|
if let Some(key) = trimmed.trim_end().strip_suffix(':') {
|
||||||
|
keys.push(key.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
|
||||||
|
/// U1 检查:项目 lefthook.yml 是否缺少模板要求的检查段(lefthook 不自动覆盖,只检测落后)。
|
||||||
|
fn check_lefthook_keys(project_yml: Option<&str>, template_yml: &str) -> (bool, String) {
|
||||||
|
let Some(yml) = project_yml else {
|
||||||
|
return (false, "lefthook.yml 缺失——重跑接入包写入".to_string());
|
||||||
|
};
|
||||||
|
let have: Vec<String> = lefthook_command_keys(yml);
|
||||||
|
let missing: Vec<String> = lefthook_command_keys(template_yml)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|k| !have.contains(k))
|
||||||
|
.collect();
|
||||||
|
if missing.is_empty() {
|
||||||
|
(true, "模板要求的检查段齐全".to_string())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
format!(
|
||||||
|
"缺少检查段: {}——lefthook.yml 不自动覆盖,请手动对照模板补齐",
|
||||||
|
missing.join(", ")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 接入生效验收:七项检查链。分发成功 ≠ 功能生效,本函数验证"真的用起来了"。
|
||||||
/// conn 由调用方注入(R06)。
|
/// conn 由调用方注入(R06)。
|
||||||
pub fn verify_onboarding(
|
pub fn verify_onboarding(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
@ -345,7 +429,11 @@ pub fn verify_onboarding(
|
|||||||
};
|
};
|
||||||
checks.push(VerifyCheck { name: "lefthook hook 挂载".into(), pass, detail });
|
checks.push(VerifyCheck { name: "lefthook hook 挂载".into(), pass, detail });
|
||||||
|
|
||||||
// 4 origin 指向已登记 Gitea 实例
|
// 4 lefthook 检查段齐全(期望键从内嵌模板自省,U1)
|
||||||
|
let (pass, detail) = check_lefthook_keys(read("lefthook.yml").as_deref(), TMPL_LEFTHOOK);
|
||||||
|
checks.push(VerifyCheck { name: "lefthook 检查段齐全".into(), pass, detail });
|
||||||
|
|
||||||
|
// 5 origin 指向已登记 Gitea 实例
|
||||||
let origin_host = read(".git/config")
|
let origin_host = read(".git/config")
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(parse_origin_url)
|
.and_then(parse_origin_url)
|
||||||
@ -364,7 +452,7 @@ pub fn verify_onboarding(
|
|||||||
let (pass, detail) = check_origin(origin_host.as_deref(), &instance_hosts);
|
let (pass, detail) = check_origin(origin_host.as_deref(), &instance_hosts);
|
||||||
checks.push(VerifyCheck { name: "origin 指向 Gitea".into(), pass, detail });
|
checks.push(VerifyCheck { name: "origin 指向 Gitea".into(), pass, detail });
|
||||||
|
|
||||||
// 5 CI workflow 存在
|
// 6 CI workflow 存在
|
||||||
let yml_count = std::fs::read_dir(root.join(".gitea/workflows"))
|
let yml_count = std::fs::read_dir(root.join(".gitea/workflows"))
|
||||||
.map(|rd| {
|
.map(|rd| {
|
||||||
rd.flatten()
|
rd.flatten()
|
||||||
@ -387,7 +475,7 @@ pub fn verify_onboarding(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6 飞轮数据在流入(近 30 天 commit_metrics)
|
// 7 飞轮数据在流入(近 30 天 commit_metrics)
|
||||||
let recent: i64 = conn
|
let recent: i64 = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND committed_at >= datetime('now','-30 days')",
|
"SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND committed_at >= datetime('now','-30 days')",
|
||||||
@ -587,12 +675,12 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&base).unwrap();
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
|
|
||||||
// 空目录:六项全不通过(真实反映"什么都没接")
|
// 空目录:七项全不通过(真实反映"什么都没接")
|
||||||
let checks = verify_onboarding(&conn, "p1", &base);
|
let checks = verify_onboarding(&conn, "p1", &base);
|
||||||
assert_eq!(checks.len(), 6, "检查链应为六项");
|
assert_eq!(checks.len(), 7, "检查链应为七项");
|
||||||
assert!(checks.iter().all(|c| !c.pass), "空项目应全部不通过");
|
assert!(checks.iter().all(|c| !c.pass), "空项目应全部不通过");
|
||||||
|
|
||||||
// 铺齐全部生效条件后:六项全通过
|
// 铺齐全部生效条件后:七项全通过
|
||||||
dispatch_templates(base.to_str().unwrap());
|
dispatch_templates(base.to_str().unwrap());
|
||||||
std::fs::create_dir_all(base.join(".git/hooks")).unwrap();
|
std::fs::create_dir_all(base.join(".git/hooks")).unwrap();
|
||||||
std::fs::write(base.join(".git/hooks/pre-commit"), "call lefthook run pre-commit").unwrap();
|
std::fs::write(base.join(".git/hooks/pre-commit"), "call lefthook run pre-commit").unwrap();
|
||||||
@ -640,6 +728,79 @@ mod tests {
|
|||||||
assert!(!check_managed_version(Some("手写内容,无标记"), expected).0, "无标记应 fail");
|
assert!(!check_managed_version(Some("手写内容,无标记"), expected).0, "无标记应 fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── U1:全托管文件三态 + lefthook 检查段自省 ─────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fully_managed_dispatch_three_states() {
|
||||||
|
let base = std::env::temp_dir()
|
||||||
|
.join(format!("lianjing_fm_test_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
|
|
||||||
|
// 不存在 → written
|
||||||
|
let r = dispatch_fully_managed(&base, "envoy.md", "v2 内容");
|
||||||
|
assert_eq!(r.action, "written");
|
||||||
|
|
||||||
|
// 一致 → skipped(CRLF 检出也视为一致)
|
||||||
|
let r = dispatch_fully_managed(&base, "envoy.md", "v2 内容");
|
||||||
|
assert_eq!(r.action, "skipped");
|
||||||
|
std::fs::write(base.join("envoy.md"), "v2 内容".replace('\n', "\r\n")).unwrap();
|
||||||
|
let r = dispatch_fully_managed(&base, "envoy.md", "v2 内容");
|
||||||
|
assert_eq!(r.action, "skipped", "仅行尾差异不应触发覆盖");
|
||||||
|
|
||||||
|
// 不一致(旧版模板/被改动)→ 覆盖 updated
|
||||||
|
std::fs::write(base.join("envoy.md"), "v1 旧内容").unwrap();
|
||||||
|
let r = dispatch_fully_managed(&base, "envoy.md", "v2 内容");
|
||||||
|
assert_eq!(r.action, "updated");
|
||||||
|
assert_eq!(std::fs::read_to_string(base.join("envoy.md")).unwrap(), "v2 内容");
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_templates_upgrades_fully_managed_files() {
|
||||||
|
let base = std::env::temp_dir()
|
||||||
|
.join(format!("lianjing_fm_e2e_test_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(&base).unwrap();
|
||||||
|
let pp = base.to_str().unwrap();
|
||||||
|
|
||||||
|
dispatch_templates(pp);
|
||||||
|
// 模拟旧版本 envoy:内容与当前模板不同 → 重跑接入包应覆盖为最新
|
||||||
|
std::fs::write(base.join(".claude/commands/envoy.md"), "旧版使者指令").unwrap();
|
||||||
|
let r2 = dispatch_templates(pp);
|
||||||
|
let envoy = r2.iter().find(|t| t.name == ".claude/commands/envoy.md").unwrap();
|
||||||
|
assert_eq!(envoy.action, "updated", "全托管文件应被升级,而非跳过");
|
||||||
|
let gate = r2.iter().find(|t| t.name == ".claude/hooks/blueprint-gate.cjs").unwrap();
|
||||||
|
assert_eq!(gate.action, "skipped", "未动过的全托管文件重跑应 skipped");
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lefthook_keys_extraction_and_check() {
|
||||||
|
// 从内嵌模板自省:至少含 typecheck 与 commit-msg 侧检查段
|
||||||
|
let tpl_keys = lefthook_command_keys(TMPL_LEFTHOOK);
|
||||||
|
assert!(tpl_keys.contains(&"typecheck".to_string()), "模板应含 typecheck: {tpl_keys:?}");
|
||||||
|
assert!(tpl_keys.len() >= 3, "模板检查段应不止一个: {tpl_keys:?}");
|
||||||
|
// 注释掉的示例段不提取
|
||||||
|
assert!(!tpl_keys.contains(&"api-typecheck".to_string()), "注释行不算键");
|
||||||
|
|
||||||
|
// 项目文件与模板一致 → pass
|
||||||
|
let (pass, _) = check_lefthook_keys(Some(TMPL_LEFTHOOK), TMPL_LEFTHOOK);
|
||||||
|
assert!(pass);
|
||||||
|
|
||||||
|
// 缺段 → fail 且列出缺失键
|
||||||
|
let partial = "pre-commit:\n commands:\n typecheck:\n run: pnpm typecheck\n";
|
||||||
|
let (pass, detail) = check_lefthook_keys(Some(partial), TMPL_LEFTHOOK);
|
||||||
|
assert!(!pass);
|
||||||
|
assert!(detail.contains("缺少检查段"), "应列出缺失键: {detail}");
|
||||||
|
assert!(!detail.contains("typecheck,") && detail.contains("blueprint-gate"), "已有键不在缺失列表: {detail}");
|
||||||
|
|
||||||
|
// 文件缺失 → fail
|
||||||
|
assert!(!check_lefthook_keys(None, TMPL_LEFTHOOK).0);
|
||||||
|
}
|
||||||
|
|
||||||
// ── B4:使者回传覆盖率(沉默项目信号)────────────────────────────────────
|
// ── B4:使者回传覆盖率(沉默项目信号)────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -235,18 +235,18 @@ pub fn tools_list_result() -> Value {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "apply_onboarding_pack",
|
"name": "apply_onboarding_pack",
|
||||||
"description": "向项目分发标准接入包:写入 AGENTS.md / lefthook / git-workflow 模板(已存在则原位升级 ONBOARDING_MANAGED 块,绝不覆盖块外用户内容),并挂载 buff 自动监听 git 提交。等价于炼境 UI「接入包」区域的「应用接入包」按钮。模板版本升级后对存量项目重新调用即可推送新版本。",
|
"description": "向项目分发标准接入包:写入 AGENTS.md / lefthook / git-workflow / envoy / blueprint-gate 模板(版本化块原位升级、全托管文件内容不一致即覆盖、lefthook 存在即跳过,块外用户内容绝不触碰),并挂载 buff 自动监听 git 提交。等价于炼境 UI「接入包」区域的「应用接入包」按钮。模板升级后对存量项目重新调用即可推送新版本;不传 project_id 则对所有已登记项目批量执行。",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"project_id": { "type": "string", "description": "项目 workspace ID(来自 list_group_projects 的返回结果)" }
|
"project_id": { "type": "string", "description": "项目 workspace ID(来自 list_group_projects)。省略则批量分发到所有已登记项目" }
|
||||||
},
|
},
|
||||||
"required": ["project_id"]
|
"required": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "verify_onboarding",
|
"name": "verify_onboarding",
|
||||||
"description": "接入生效验收:检查接入包在项目里是否真正生效(模板管理块版本到位 / lefthook hook 真挂在 .git/hooks / origin 指向已登记 Gitea / CI workflow 存在 / 近 30 天飞轮 commit 数据流入)。分发成功 ≠ 功能生效,apply_onboarding_pack 之后应调用本工具做消费者角色验收。",
|
"description": "接入生效验收:检查接入包在项目里是否真正生效(模板管理块版本到位 / lefthook hook 真挂在 .git/hooks / lefthook 检查段对齐模板 / origin 指向已登记 Gitea / CI workflow 存在 / 近 30 天飞轮 commit 数据流入)。分发成功 ≠ 功能生效,apply_onboarding_pack 之后应调用本工具做消费者角色验收。",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -607,6 +607,63 @@ pub async fn tools_call(params: Option<&Value>) -> Value {
|
|||||||
let private = args.get("private").and_then(|v| v.as_bool()).unwrap_or(false);
|
let private = args.get("private").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
crate::commands::gitea::gitea_api_setup_repo(pid, repo_name, private)
|
crate::commands::gitea::gitea_api_setup_repo(pid, repo_name, private)
|
||||||
}
|
}
|
||||||
|
// U1 批量模式:project_id 字段缺失(而非空串——空串按无效单项目报错)→ 全部已登记项目
|
||||||
|
"apply_onboarding_pack" if args.get("project_id").and_then(|v| v.as_str()).is_none() => {
|
||||||
|
// 复用 ingest_git_history 先例;替代曾经的 batch_apply.py 临时脚本
|
||||||
|
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||||
|
let targets: Vec<(String, String)> = {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT id, win_path FROM project_workspaces WHERE win_path IS NOT NULL")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
rows
|
||||||
|
};
|
||||||
|
drop(conn);
|
||||||
|
if targets.is_empty() {
|
||||||
|
Err("无已登记的 Windows 路径项目可分发".to_string())
|
||||||
|
} else {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let (mut ok, mut failed) = (0u32, 0u32);
|
||||||
|
for (id, path) in &targets {
|
||||||
|
match crate::commands::onboarding::apply_onboarding_pack(
|
||||||
|
path.clone(),
|
||||||
|
id.clone(),
|
||||||
|
) {
|
||||||
|
Ok(report) => {
|
||||||
|
ok += 1;
|
||||||
|
let summary: Vec<String> = report
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
|
let short = t.name.rsplit('/').next().unwrap_or(&t.name);
|
||||||
|
format!("{short}={}", t.action)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
lines.push(format!(
|
||||||
|
"- {}: {} | hook={}",
|
||||||
|
id,
|
||||||
|
summary.join(", "),
|
||||||
|
report.hook.status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
failed += 1;
|
||||||
|
lines.push(format!("- {}: ❌ {}", id, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(format!(
|
||||||
|
"批量接入包分发完成:{} 成功,{} 失败\n\n{}\n\n建议对关键项目调用 verify_onboarding 抽查生效。",
|
||||||
|
ok,
|
||||||
|
failed,
|
||||||
|
lines.join("\n")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
"apply_onboarding_pack" => {
|
"apply_onboarding_pack" => {
|
||||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
resolve_project_root(pid).and_then(|root| {
|
resolve_project_root(pid).and_then(|root| {
|
||||||
@ -1238,11 +1295,20 @@ mod tests {
|
|||||||
fn apply_onboarding_pack_schema_requires_project_id() {
|
fn apply_onboarding_pack_schema_requires_project_id() {
|
||||||
let result = tools_list_result();
|
let result = tools_list_result();
|
||||||
let tools = result["tools"].as_array().unwrap();
|
let tools = result["tools"].as_array().unwrap();
|
||||||
for name in ["apply_onboarding_pack", "verify_onboarding"] {
|
// verify 必填 project_id;apply 自 U1 起可省略(批量模式)
|
||||||
let tool = tools.iter().find(|t| t["name"] == name).unwrap();
|
let verify = tools.iter().find(|t| t["name"] == "verify_onboarding").unwrap();
|
||||||
let required = tool["inputSchema"]["required"].as_array().unwrap();
|
let fields: Vec<&str> = verify["inputSchema"]["required"]
|
||||||
let fields: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
|
.as_array()
|
||||||
assert!(fields.contains(&"project_id"), "{name} 应必填 project_id");
|
.unwrap()
|
||||||
}
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.collect();
|
||||||
|
assert!(fields.contains(&"project_id"), "verify_onboarding 应必填 project_id");
|
||||||
|
|
||||||
|
let apply = tools.iter().find(|t| t["name"] == "apply_onboarding_pack").unwrap();
|
||||||
|
assert!(
|
||||||
|
apply["inputSchema"]["required"].as_array().unwrap().is_empty(),
|
||||||
|
"apply_onboarding_pack 的 project_id 应可省略(U1 批量模式)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user