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:
toot
2026-09-16 15:30:50 +08:00
parent 8a6c55d94a
commit aa57ea69de
19 changed files with 1977 additions and 154 deletions
+52 -9
View File
@@ -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 {