feat(controller): 工具仓库、Skills 录入、多轮追问与终端运行日志
- 插件/MCP 改为通用工具仓库:内置、HTTP MCP、本机命令(stdio)三类工具,可增删改、 按 Agent 分配;定义变更自动提升下发版本并重发,删除即回收分配并通知 Agent 清空。 - 旧 desktop-filesystem 降级为仓库中的一个内置条目;本机预设工具改为总控只展示、 Agent 默认启用,任务执行来源新增 builtin(审计 task.builtin_tool_execution)。 - Skills 补齐录入闭环:新建或按名称覆盖更新,并可从旧版技能目录(SKILL.md + skill.json) 批量导入;同名更新保持标识不变并自动重新应用,旧包入口脚本只提示不执行。 - 单独指派支持基于某条已完成任务的多轮追问:任务内容框仍下发无上下文的一次性任务, 追问时由总控组装历史(仅成功轮次、默认最近 50 轮 / 约 6 万字符),会话按 Agent 隔离; 执行动态把同一会话合并为一条记录,任务详情弹窗底部提供继续对话输入框。 - 任务详情支持运行中观测:打开期间每 2 秒刷新,终端风格输出全部事件并自动滚动; progress 事件不改变任务状态且顺延无进展超时,避免正在执行工具的任务被误判超时。 - 修复 build.ps1 缺 UTF-8 BOM 导致 build.bat 在 PowerShell 5.1 下语法报错。
This commit is contained in:
@@ -11,6 +11,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// maxSkillContentBytes bounds one Skill Markdown document. The Agent applies the
|
||||
// same limit so content is never silently truncated after deployment.
|
||||
const maxSkillContentBytes = 50000
|
||||
|
||||
// Skill is a safe Markdown-only capability guide owned by the controller.
|
||||
type Skill struct {
|
||||
ID string `json:"id"`
|
||||
@@ -85,25 +89,64 @@ func (c *ControlPlane) SkillDocument(skillID string) (SkillDocument, error) {
|
||||
return document, nil
|
||||
}
|
||||
|
||||
// SaveSkill stores one Markdown-only Skill package. A skill is identified by its
|
||||
// display name, so importing an updated version keeps the identity — and therefore
|
||||
// every existing Agent assignment — intact instead of creating a duplicate.
|
||||
func (c *ControlPlane) SaveSkill(input SkillInput) (Skill, error) {
|
||||
name, version, content := strings.TrimSpace(input.Name), strings.TrimSpace(input.Version), strings.TrimSpace(input.Content)
|
||||
if name == "" || version == "" || content == "" {
|
||||
return Skill{}, errors.New("请完整填写 Skill 名称、版本和内容")
|
||||
}
|
||||
if len([]rune(name)) > 80 || len([]rune(version)) > 40 || len(content) > 50000 {
|
||||
if len([]rune(name)) > 80 || len([]rune(version)) > 40 || len(content) > maxSkillContentBytes {
|
||||
return Skill{}, errors.New("Skill 内容或名称过长")
|
||||
}
|
||||
id, err := randomHex(16)
|
||||
if err != nil {
|
||||
return Skill{}, err
|
||||
}
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
checksum := hex.EncodeToString(sum[:])
|
||||
now := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
if _, err = c.db.Exec("INSERT INTO skill_packages(skill_id, name, version, content, checksum, updated_at) VALUES (?, ?, ?, ?, ?, ?)", id, name, version, content, hex.EncodeToString(sum[:]), now); err != nil {
|
||||
return Skill{}, fmt.Errorf("无法保存 Skill: %w", err)
|
||||
skillID := ""
|
||||
err := c.db.QueryRow("SELECT skill_id FROM skill_packages WHERE name = ?", name).Scan(&skillID)
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
skillID, err = randomHex(16)
|
||||
if err != nil {
|
||||
return Skill{}, err
|
||||
}
|
||||
if _, err = c.db.Exec("INSERT INTO skill_packages(skill_id, name, version, content, checksum, updated_at) VALUES (?, ?, ?, ?, ?, ?)", skillID, name, version, content, checksum, now); err != nil {
|
||||
return Skill{}, fmt.Errorf("无法保存 Skill: %w", err)
|
||||
}
|
||||
c.recordAudit("skill.package_saved", "controller", "skill:"+skillID, "completed", "已保存经哈希校验的 Skill 包。")
|
||||
case err != nil:
|
||||
return Skill{}, fmt.Errorf("无法读取 Skill: %w", err)
|
||||
default:
|
||||
if _, err = c.db.Exec("UPDATE skill_packages SET version = ?, content = ?, checksum = ?, updated_at = ? WHERE skill_id = ?", version, content, checksum, now, skillID); err != nil {
|
||||
return Skill{}, fmt.Errorf("无法保存 Skill: %w", err)
|
||||
}
|
||||
c.recordAudit("skill.package_updated", "controller", "skill:"+skillID, "completed", "已按名称覆盖更新 Skill 包内容与版本。")
|
||||
}
|
||||
c.redeploySkill(skillID)
|
||||
return Skill{ID: skillID, Name: name, Version: version, Checksum: checksum, UpdatedAt: now}, nil
|
||||
}
|
||||
|
||||
// redeploySkill re-sends an updated Skill to the Agents that already selected it.
|
||||
func (c *ControlPlane) redeploySkill(skillID string) {
|
||||
rows, err := c.db.Query("SELECT agent_id FROM agent_skill_assignments WHERE skill_id = ?", skillID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
agentIDs := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var agentID string
|
||||
if rows.Scan(&agentID) == nil {
|
||||
agentIDs = append(agentIDs, agentID)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
for _, agentID := range agentIDs {
|
||||
if _, err := c.db.Exec("UPDATE agent_skill_assignments SET apply_status = 'pending', last_error = '', updated_at = ? WHERE agent_id = ? AND skill_id = ?", time.Now().UTC().Format(time.RFC3339Nano), agentID, skillID); err != nil {
|
||||
continue
|
||||
}
|
||||
go c.pushSkills(agentID)
|
||||
}
|
||||
c.recordAudit("skill.package_saved", "controller", "skill:"+id, "completed", "已保存经哈希校验的 Skill 包。")
|
||||
return Skill{ID: id, Name: name, Version: version, Checksum: hex.EncodeToString(sum[:]), UpdatedAt: now}, nil
|
||||
}
|
||||
|
||||
func (c *ControlPlane) DeploySkill(skillID string, agentIDs []string) error {
|
||||
|
||||
Reference in New Issue
Block a user