feat: 新增技能编排引擎和工作流构建器组件
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor: 统一Hands系统常量到单个源文件
refactor: 更新Hands中文名称和描述

fix: 修复技能市场在连接状态变化时重新加载
fix: 修复身份变更提案的错误处理逻辑

docs: 更新多个功能文档的验证状态和实现位置
docs: 更新Hands系统文档

test: 添加测试文件验证工作区路径
This commit is contained in:
iven
2026-03-25 08:27:25 +08:00
parent 9c781f5f2a
commit aa6a9cbd84
110 changed files with 12384 additions and 1337 deletions

View File

@@ -1205,6 +1205,70 @@ zclaw_types::Message::ToolUse { id, tool, input } => {
[AgentLoop] ToolUseEnd: id=call_xxx, input={"skill_id":"finance-tracker","input":{...}}
```
### 9.6 日志截断导致 UTF-8 字符边界 Panic
**症状**:
- 会话一直卡在"思考中..."状态
- 终端显示 panic`byte index 100 is not a char boundary; it is inside '务' (bytes 99..102)`
**错误信息**:
```
thread 'tokio-rt-worker' panicked at crates\zclaw-runtime\src\driver\openai.rs:502:82:
byte index 100 is not a char boundary; it is inside '务' (bytes 99..102) of `你好!我是 **Agent Soul**...`
```
**根本原因**: 使用 `&c[..100]` 按字节截断 UTF-8 字符串用于日志输出
**问题代码** (`crates/zclaw-runtime/src/driver/openai.rs:502`):
```rust
// ❌ 错误 - 按字节截断,可能切断多字节字符
choice.message.content.as_ref().map(|c| if c.len() > 100 { &c[..100] } else { c.as_str() })
```
**问题分析**:
Rust 字符串是 UTF-8 编码的:
- ASCII 字符1 字节
- 中文字符3 字节(如 '务' = bytes 99..102
- 当截断位置正好落在多字节字符内部时,程序 panic
**修复方案**:
使用 `floor_char_boundary()` 找到最近的合法字符边界:
```rust
// ✅ 正确 - 使用 floor_char_boundary 确保不截断多字节字符
choice.message.content.as_ref().map(|c| {
if c.len() > 100 {
let end = c.floor_char_boundary(100); // 找到 <= 100 的最近字符边界
&c[..end]
} else {
c.as_str()
}
})
```
**相关文件**:
- `crates/zclaw-runtime/src/driver/openai.rs:502` - 日志截断逻辑
**验证修复**:
1. 启动应用
2. 发送包含中文的消息
3. 查看终端日志,应正常显示截断的内容
4. 会话不应卡住
**最佳实践**:
Rust 中截断 UTF-8 字符串的正确方式:
| 方法 | 用途 |
|------|------|
| `s.floor_char_boundary(n)` | 找到 <= n 的最近字符边界 |
| `s.ceil_char_boundary(n)` | 找到 >= n 的最近字符边界 |
| `s.chars().take(n).collect()` | 取前 n 个字符(创建新 String |
**注意**: `floor_char_boundary()` 需要 Rust 1.65+
---
## 10. 技能系统问题
@@ -1394,6 +1458,115 @@ fn default_skills_dir() -> Option<PathBuf> {
}
```
### 10.3 技能页面显示"暂无技能"但技能目录存在
**症状**:
- 技能市场显示 "暂无技能" 和 "0 技能"
- 控制台日志显示 `[skill_list] Found 0 skills`
- 技能目录 `G:\ZClaw_openfang\skills` 存在且包含 70+ 个 SKILL.md 文件
**根本原因**: 多层问题叠加
1. **技能目录路径解析失败**: Tauri dev 模式下 `current_exe()` 和 `current_dir()` 返回意外路径
- `current_dir()` 可能返回 `desktop/src-tauri` 而非项目根目录
- `current_exe()` 可能返回 Tauri CLI 或 node.exe 而非编译后的 exe
2. **SkillRegistry.async 上下文使用 blocking_write()**: 在 tokio 异步运行时中调用 `blocking_write()` 导致 panic
```
thread 'tokio-rt-worker' panicked at registry.rs:86:38:
Cannot block the current thread from within a runtime.
```
**问题代码** (`crates/zclaw-skills/src/registry.rs`):
```rust
// ❌ 错误 - 在 async 函数调用的 sync 函数中使用 blocking_write
pub async fn add_skill_dir(&self, dir: PathBuf) -> Result<()> {
// ...
for skill_path in skill_paths {
self.load_skill_from_dir(&skill_path)?; // 调用 sync 函数
}
}
fn load_skill_from_dir(&self, dir: &PathBuf) -> Result<()> {
// ...
let mut skills = self.skills.blocking_write(); // 在 async 上下文中 panic!
}
```
**修复方案**:
1. **使用编译时路径作为技能目录备选** (`config.rs:default_skills_dir`):
```rust
fn default_skills_dir() -> Option<std::path::PathBuf> {
// 1. 环境变量
if let Ok(dir) = std::env::var("ZCLAW_SKILLS_DIR") {
return Some(PathBuf::from(dir));
}
// 2. 编译时路径 - CARGO_MANIFEST_DIR 是 crates/zclaw-kernel
// 向上两级找到 workspace root
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
if let Some(workspace_root) = manifest_dir.parent().and_then(|p| p.parent()) {
let workspace_skills = workspace_root.join("skills");
if workspace_skills.exists() {
return Some(workspace_skills);
}
}
// 3. 当前工作目录及向上搜索
// ... 其他备选方案
}
```
2. **将 load_skill_from_dir 改为 async** (`registry.rs`):
```rust
// ✅ 正确 - 使用 async write
async fn load_skill_from_dir(&self, dir: &PathBuf) -> Result<()> {
// ... 解析 SKILL.md
// 使用 async write 而非 blocking_write
let mut skills = self.skills.write().await;
let mut manifests = self.manifests.write().await;
skills.insert(manifest.id.clone(), skill);
manifests.insert(manifest.id.clone(), manifest);
Ok(())
}
```
**调试日志示例** (修复后):
```
[default_skills_dir] CARGO_MANIFEST_DIR: G:\ZClaw_openfang\crates\zclaw-kernel
[default_skills_dir] Workspace skills: G:\ZClaw_openfang\skills (exists: true)
[kernel_init] Skills directory: G:\ZClaw_openfang\skills (exists: true)
[skill_list] Found 77 skills
```
**影响范围**:
- `crates/zclaw-kernel/src/config.rs` - default_skills_dir() 函数
- `crates/zclaw-skills/src/registry.rs` - load_skill_from_dir() 函数
- `desktop/src-tauri/src/kernel_commands.rs` - SkillInfoResponse 结构体(添加 triggers 和 category 字段)
**前端配套修改**:
- `desktop/src-tauri/src/kernel_commands.rs`: 添加 `triggers: Vec<String>` 和 `category: Option<String>` 字段
- `desktop/src/lib/kernel-client.ts`: 更新 `listSkills()` 返回类型
- `desktop/src/store/configStore.ts`: 更新 `createConfigClientFromKernel` 中的字段映射
- `desktop/src/lib/skill-adapter.ts`: 更新 `extractTriggers` 和 `extractCapabilities`
**验证修复**:
1. 启动应用,查看终端日志
2. 应看到 `[kernel_init] Skills directory: ... (exists: true)`
3. 技能市场应显示 77 个技能
4. 点击技能可展开查看详情
**技能目录发现优先级**:
1. `ZCLAW_SKILLS_DIR` 环境变量
2. `CARGO_MANIFEST_DIR`/../skills (编译时路径)
3. `current_dir()`/skills 及向上搜索
4. `current_exe()`/skills 及向上搜索
5. 回退到 `current_dir()`/skills
---
## 11. 相关文档
@@ -1408,6 +1581,7 @@ fn default_skills_dir() -> Option<PathBuf> {
| 日期 | 变更 |
|------|------|
| 2026-03-24 | 添加 9.6 节:日志截断导致 UTF-8 字符边界 Panic - floor_char_boundary 修复方案 |
| 2026-03-24 | 添加 9.5 节:阿里云百炼 Coding Plan 工具调用 400 错误 - 流式+工具不兼容、响应解析优先级、JSON 序列化问题 |
| 2026-03-24 | 添加 10.2 节:`skills_dir: None` 导致技能系统完全失效 - from_provider() 硬编码问题 |
| 2026-03-24 | 添加 10.1 节Agent 无法调用合适的技能 - 系统提示词注入技能列表 + triggers 字段 |