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
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:
@@ -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 字段 |
|
||||
|
||||
Reference in New Issue
Block a user