Compare commits

..

12 Commits

Author SHA1 Message Date
公明 715240dc5e Add files via upload 2026-04-15 00:54:15 +08:00
公明 5f8b19e179 Add files via upload 2026-04-15 00:53:14 +08:00
公明 ea48f3d71b Add files via upload 2026-04-15 00:43:35 +08:00
公明 e3013aa230 Add files via upload 2026-04-15 00:39:23 +08:00
公明 1cf34797b8 Add files via upload 2026-04-15 00:38:07 +08:00
公明 62241e0e66 Add files via upload 2026-04-15 00:13:09 +08:00
公明 dda4edb952 Add files via upload 2026-04-15 00:08:35 +08:00
公明 5bf6317dcb Add files via upload 2026-04-14 19:30:39 +08:00
公明 9331fbfea1 Add files via upload 2026-04-14 19:28:17 +08:00
公明 b1ac985c28 Add files via upload 2026-04-14 19:06:52 +08:00
公明 4f4a725034 Add files via upload 2026-04-14 19:02:28 +08:00
公明 3e689a5dcb Add files via upload 2026-04-14 12:53:49 +08:00
14 changed files with 674 additions and 71 deletions
+4 -4
View File
@@ -9,8 +9,8 @@ toolchain go1.24.4
require (
github.com/bytedance/sonic v1.15.0
github.com/cloudwego/eino v0.8.4
github.com/cloudwego/eino-ext/components/model/openai v0.1.10
github.com/cloudwego/eino v0.8.8
github.com/cloudwego/eino-ext/components/model/openai v0.1.12
github.com/creack/pty v1.1.24
github.com/eino-contrib/jsonschema v1.0.3
github.com/gin-gonic/gin v1.9.1
@@ -34,7 +34,7 @@ require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.14 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.16 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
@@ -52,7 +52,7 @@ require (
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/meguminnnnnnnnn/go-openai v0.1.1 // indirect
github.com/meguminnnnnnnnn/go-openai v0.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
+8
View File
@@ -22,10 +22,16 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.8.4 h1:aFKJK82MmPR6dm5y5J7IXivYSvh4HkcXwf18j6vyhmk=
github.com/cloudwego/eino v0.8.4/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU=
github.com/cloudwego/eino v0.8.8 h1:64NuheQBmxOXe/28Tm85rkBkxXMB5ZhjSu/j0RDFyZU=
github.com/cloudwego/eino v0.8.8/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU=
github.com/cloudwego/eino-ext/components/model/openai v0.1.10 h1:zVkU4rZUUUUAPEXOGs98n8nsT/NZvQ9zWY0B9h2US7k=
github.com/cloudwego/eino-ext/components/model/openai v0.1.10/go.mod h1:smEeTKXe8uz+HDUBQn0yZhpx7mmOUKFQyguLfjAQ57I=
github.com/cloudwego/eino-ext/components/model/openai v0.1.12 h1:vcwNXeT7bpaXMNwUhtcHZwMYY8II2jAihuooyivmEZ0=
github.com/cloudwego/eino-ext/components/model/openai v0.1.12/go.mod h1:ve/+/hLZMvxD5AieQ355xHIFhAZVlsG4rdwTnE16aQU=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.14 h1:yOZII6VYaL00CVZYba+HUixFygsW0Xz/1QjQ5htj1Ls=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.14/go.mod h1:1xMQZ8eE11pkEoTAEy8UlaAY817qGVMvjpDPGSIO3Ns=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.16 h1:q242n5P5Tx3a2QLaBmkfEpfRs/o17Ac6u3EAgItEEOc=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.16/go.mod h1:p+l0zBB0GjjX8HTlbTs3g3KfUFwZC11bsCGZOXW/3L0=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -113,6 +119,8 @@ github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/meguminnnnnnnnn/go-openai v0.1.1 h1:u/IMMgrj/d617Dh/8BKAwlcstD74ynOJzCtVl+y8xAs=
github.com/meguminnnnnnnnn/go-openai v0.1.1/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA=
github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
+18
View File
@@ -1589,6 +1589,7 @@ type BatchTaskRequest struct {
AgentMode string `json:"agentMode,omitempty"` // single | multi
ScheduleMode string `json:"scheduleMode,omitempty"` // manual | cron
CronExpr string `json:"cronExpr,omitempty"` // scheduleMode=cron 时必填
ExecuteNow bool `json:"executeNow,omitempty"` // 创建后是否立即执行(默认 false)
}
func normalizeBatchQueueAgentMode(mode string) string {
@@ -1650,9 +1651,26 @@ func (h *AgentHandler) CreateBatchQueue(c *gin.Context) {
}
queue := h.batchTaskManager.CreateBatchQueue(req.Title, req.Role, agentMode, scheduleMode, cronExpr, nextRunAt, validTasks)
started := false
if req.ExecuteNow {
ok, err := h.startBatchQueueExecution(queue.ID, false)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "队列不存在"})
return
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "queueId": queue.ID})
return
}
started = true
if refreshed, exists := h.batchTaskManager.GetBatchQueue(queue.ID); exists {
queue = refreshed
}
}
c.JSON(http.StatusOK, gin.H{
"queueId": queue.ID,
"queue": queue,
"started": started,
})
}
+44 -18
View File
@@ -678,7 +678,7 @@ func (m *BatchTaskManager) ResetQueueForRerun(queueID string) bool {
return true
}
// UpdateTaskMessage 更新任务消息(仅限待执行状态
// UpdateTaskMessage 更新任务消息(队列空闲时可改;任务需非 running
func (m *BatchTaskManager) UpdateTaskMessage(queueID, taskID, message string) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -688,17 +688,15 @@ func (m *BatchTaskManager) UpdateTaskMessage(queueID, taskID, message string) er
return fmt.Errorf("队列不存在")
}
// 检查队列状态,只有待执行状态的队列才能编辑任务
if queue.Status != "pending" {
return fmt.Errorf("只有待执行状态的队列才能编辑任务")
if !queueAllowsTaskListMutationLocked(queue) {
return fmt.Errorf("队列正在执行或未就绪,无法编辑任务")
}
// 查找并更新任务
for _, task := range queue.Tasks {
if task.ID == taskID {
// 只有待执行状态的任务才能编辑
if task.Status != "pending" {
return fmt.Errorf("只有待执行状态的任务才能编辑")
if task.Status == "running" {
return fmt.Errorf("执行中的任务不能编辑")
}
task.Message = message
@@ -715,7 +713,7 @@ func (m *BatchTaskManager) UpdateTaskMessage(queueID, taskID, message string) er
return fmt.Errorf("任务不存在")
}
// AddTaskToQueue 添加任务到队列(仅限待执行状态
// AddTaskToQueue 添加任务到队列(队列空闲时可添加:含 cron 本轮 completed、手动暂停后等
func (m *BatchTaskManager) AddTaskToQueue(queueID, message string) (*BatchTask, error) {
m.mu.Lock()
defer m.mu.Unlock()
@@ -725,9 +723,8 @@ func (m *BatchTaskManager) AddTaskToQueue(queueID, message string) (*BatchTask,
return nil, fmt.Errorf("队列不存在")
}
// 检查队列状态,只有待执行状态的队列才能添加任务
if queue.Status != "pending" {
return nil, fmt.Errorf("只有待执行状态的队列才能添加任务")
if !queueAllowsTaskListMutationLocked(queue) {
return nil, fmt.Errorf("队列正在执行或未就绪,无法添加任务")
}
if message == "" {
@@ -757,7 +754,7 @@ func (m *BatchTaskManager) AddTaskToQueue(queueID, message string) (*BatchTask,
return task, nil
}
// DeleteTask 删除任务(仅限待执行状态
// DeleteTask 删除任务(队列空闲时可删;执行中任务不可删
func (m *BatchTaskManager) DeleteTask(queueID, taskID string) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -767,18 +764,16 @@ func (m *BatchTaskManager) DeleteTask(queueID, taskID string) error {
return fmt.Errorf("队列不存在")
}
// 检查队列状态,只有待执行状态的队列才能删除任务
if queue.Status != "pending" {
return fmt.Errorf("只有待执行状态的队列才能删除任务")
if !queueAllowsTaskListMutationLocked(queue) {
return fmt.Errorf("队列正在执行或未就绪,无法删除任务")
}
// 查找并删除任务
taskIndex := -1
for i, task := range queue.Tasks {
if task.ID == taskID {
// 只有待执行状态的任务才能删除
if task.Status != "pending" {
return fmt.Errorf("只有待执行状态的任务才能删除")
if task.Status == "running" {
return fmt.Errorf("执行中的任务不能删除")
}
taskIndex = i
break
@@ -804,6 +799,37 @@ func (m *BatchTaskManager) DeleteTask(queueID, taskID string) error {
return nil
}
func queueHasRunningTaskLocked(queue *BatchTaskQueue) bool {
if queue == nil {
return false
}
for _, t := range queue.Tasks {
if t != nil && t.Status == "running" {
return true
}
}
return false
}
// queueAllowsTaskListMutationLocked 是否允许增删改子任务文案/列表(必须在持有 BatchTaskManager.mu 下调用)
func queueAllowsTaskListMutationLocked(queue *BatchTaskQueue) bool {
if queue == nil {
return false
}
if queue.Status == "running" {
return false
}
if queueHasRunningTaskLocked(queue) {
return false
}
switch queue.Status {
case "pending", "paused", "completed", "cancelled":
return true
default:
return false
}
}
// GetNextTask 获取下一个待执行的任务
func (m *BatchTaskManager) GetNextTask(queueID string) (*BatchTask, bool) {
m.mu.RLock()
+134 -7
View File
@@ -27,7 +27,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- list ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskList,
Description: "列出批量任务队列,支持按状态筛选与关键字搜索。用于查看队列 id、状态、进度及 Cron 配置等。",
Description: "列出批量任务队列(精简摘要,省上下文)。含队列元数据、子任务 id/status/截断后的 message、各状态计数。完整子任务(含 result/error/conversationId/时间等)请用 batch_task_get(queue_id)。",
ShortDescription: "列出批量任务队列",
InputSchema: map[string]interface{}{
"type": "object",
@@ -77,8 +77,15 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
if totalPages == 0 {
totalPages = 1
}
slim := make([]batchTaskQueueMCPListItem, 0, len(queues))
for _, q := range queues {
if q == nil {
continue
}
slim = append(slim, toBatchTaskQueueMCPListItem(q))
}
payload := map[string]interface{}{
"queues": queues,
"queues": slim,
"total": total,
"page": page,
"page_size": pageSize,
@@ -120,8 +127,8 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
Name: builtin.ToolBatchTaskCreate,
Description: `创建新的批量任务队列。任务列表使用 tasks(字符串数组)或 tasks_text(多行,每行一条)。
agent_mode: single(默认)或 multi(需系统启用多代理)。schedule_mode: manual(默认)或 cron;为 cron 时必须提供 cron_expr(如 "0 */6 * * *")。
重要:创建成功后队列处于 pending,不会自动开始跑子任务。若要立即执行或手工开跑,必须再调用工具 batch_task_start(传入返回的 queue_id。Cron 队列若需按表达式自动触发下一轮,还需保持调度开关开启(可用 batch_task_schedule_enabled)。`,
ShortDescription: "创建批量任务队列(创建后需 batch_task_start 才会执行)",
默认创建后不会立即执行。可通过 execute_now=true 在创建后立即启动;也可后续调用 batch_task_start 手工启动。Cron 队列若需按表达式自动触发下一轮,还需保持调度开关开启(可用 batch_task_schedule_enabled)。`,
ShortDescription: "创建批量任务队列(可选立即执行)",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
@@ -156,6 +163,10 @@ agent_mode: single(默认)或 multi(需系统启用多代理)。schedule
"type": "string",
"description": "schedule_mode 为 cron 时必填",
},
"execute_now": map[string]interface{}{
"type": "boolean",
"description": "是否创建后立即执行,默认 false",
},
},
},
}, func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
@@ -180,12 +191,37 @@ agent_mode: single(默认)或 multi(需系统启用多代理)。schedule
n := sch.Next(time.Now())
nextRunAt = &n
}
executeNow, ok := mcpArgBool(args, "execute_now")
if !ok {
executeNow = false
}
queue := h.batchTaskManager.CreateBatchQueue(title, role, agentMode, scheduleMode, cronExpr, nextRunAt, tasks)
started := false
if executeNow {
ok, err := h.startBatchQueueExecution(queue.ID, false)
if !ok {
return batchMCPTextResult("队列不存在: "+queue.ID, true), nil
}
if err != nil {
return batchMCPTextResult("创建成功但启动失败: "+err.Error(), true), nil
}
started = true
if refreshed, exists := h.batchTaskManager.GetBatchQueue(queue.ID); exists {
queue = refreshed
}
}
logger.Info("MCP batch_task_create", zap.String("queueId", queue.ID), zap.Int("taskCount", len(tasks)))
return batchMCPJSONResult(map[string]interface{}{
"queue_id": queue.ID,
"queue": queue,
"reminder": "队列已创建,当前为 pending。需要开始执行时请调用 MCP工具 batch_task_startqueue_id 同上)。Cron 自动调度需 schedule_enabled 为 true,可用 batch_task_schedule_enabled。",
"queue_id": queue.ID,
"queue": queue,
"started": started,
"execute_now": executeNow,
"reminder": func() string {
if started {
return "队列已创建并立即启动。"
}
return "队列已创建,当前为 pending。需要开始执行时请调用 MCP 工具 batch_task_startqueue_id 同上)。Cron 自动调度需 schedule_enabled 为 true,可用 batch_task_schedule_enabled。"
}(),
})
})
@@ -423,6 +459,97 @@ agent_mode: single(默认)或 multi(需系统启用多代理)。schedule
logger.Info("批量任务 MCP 工具已注册", zap.Int("count", 10))
}
// --- batch_task_list 精简结构(避免把每条子任务的 result 等大段文本塞进列表上下文) ---
const mcpBatchListTaskMessageMaxRunes = 160
// batchTaskMCPListSummary 列表中的子任务摘要(完整字段用 batch_task_get
type batchTaskMCPListSummary struct {
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
}
// batchTaskQueueMCPListItem 列表中的队列摘要
type batchTaskQueueMCPListItem struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Role string `json:"role,omitempty"`
AgentMode string `json:"agentMode"`
ScheduleMode string `json:"scheduleMode"`
CronExpr string `json:"cronExpr,omitempty"`
NextRunAt *time.Time `json:"nextRunAt,omitempty"`
ScheduleEnabled bool `json:"scheduleEnabled"`
LastScheduleTriggerAt *time.Time `json:"lastScheduleTriggerAt,omitempty"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
StartedAt *time.Time `json:"startedAt,omitempty"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
CurrentIndex int `json:"currentIndex"`
TaskTotal int `json:"task_total"`
TaskCounts map[string]int `json:"task_counts"`
Tasks []batchTaskMCPListSummary `json:"tasks"`
}
func truncateStringRunes(s string, maxRunes int) string {
if maxRunes <= 0 {
return ""
}
n := 0
for i := range s {
if n == maxRunes {
out := strings.TrimSpace(s[:i])
if out == "" {
return "…"
}
return out + "…"
}
n++
}
return s
}
func toBatchTaskQueueMCPListItem(q *BatchTaskQueue) batchTaskQueueMCPListItem {
counts := map[string]int{
"pending": 0,
"running": 0,
"completed": 0,
"failed": 0,
"cancelled": 0,
}
tasks := make([]batchTaskMCPListSummary, 0, len(q.Tasks))
for _, t := range q.Tasks {
if t == nil {
continue
}
counts[t.Status]++
tasks = append(tasks, batchTaskMCPListSummary{
ID: t.ID,
Status: t.Status,
Message: truncateStringRunes(t.Message, mcpBatchListTaskMessageMaxRunes),
})
}
return batchTaskQueueMCPListItem{
ID: q.ID,
Title: q.Title,
Role: q.Role,
AgentMode: q.AgentMode,
ScheduleMode: q.ScheduleMode,
CronExpr: q.CronExpr,
NextRunAt: q.NextRunAt,
ScheduleEnabled: q.ScheduleEnabled,
LastScheduleTriggerAt: q.LastScheduleTriggerAt,
Status: q.Status,
CreatedAt: q.CreatedAt,
StartedAt: q.StartedAt,
CompletedAt: q.CompletedAt,
CurrentIndex: q.CurrentIndex,
TaskTotal: len(tasks),
TaskCounts: counts,
Tasks: tasks,
}
}
func batchMCPTextResult(text string, isErr bool) *mcp.ToolResult {
return &mcp.ToolResult{
Content: []mcp.Content{{Type: "text", Text: text}},
+26
View File
@@ -325,6 +325,17 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
searchTermLower = strings.ToLower(searchTerm)
}
// 解析状态筛选参数: "true" = 仅已启用, "false" = 仅已停用, "" = 全部
enabledFilter := c.Query("enabled")
var filterEnabled *bool
if enabledFilter == "true" {
v := true
filterEnabled = &v
} else if enabledFilter == "false" {
v := false
filterEnabled = &v
}
// 解析角色参数,用于过滤工具并标注启用状态
roleName := c.Query("role")
var roleToolsSet map[string]bool // 角色配置的工具集合
@@ -388,6 +399,11 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
}
}
// 状态筛选
if filterEnabled != nil && toolInfo.Enabled != *filterEnabled {
continue
}
allTools = append(allTools, toolInfo)
}
@@ -444,6 +460,11 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
}
}
// 状态筛选
if filterEnabled != nil && toolInfo.Enabled != *filterEnabled {
continue
}
allTools = append(allTools, toolInfo)
}
}
@@ -486,6 +507,11 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
}
}
// 状态筛选
if filterEnabled != nil && toolInfo.Enabled != *filterEnabled {
continue
}
allTools = append(allTools, toolInfo)
}
}
+25 -3
View File
@@ -403,6 +403,24 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"type": "string",
"description": "角色名称(可选)",
},
"agentMode": map[string]interface{}{
"type": "string",
"description": "代理模式(single | multi",
"enum": []string{"single", "multi"},
},
"scheduleMode": map[string]interface{}{
"type": "string",
"description": "调度方式(manual | cron",
"enum": []string{"manual", "cron"},
},
"cronExpr": map[string]interface{}{
"type": "string",
"description": "Cron 表达式(scheduleMode=cron 时必填)",
},
"executeNow": map[string]interface{}{
"type": "boolean",
"description": "是否创建后立即执行(默认 false)",
},
},
},
"BatchQueue": map[string]interface{}{
@@ -1540,9 +1558,9 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"message": map[string]interface{}{"type": "string"},
"conversationId": map[string]interface{}{"type": "string"},
"role": map[string]interface{}{"type": "string"},
"message": map[string]interface{}{"type": "string"},
"conversationId": map[string]interface{}{"type": "string"},
"role": map[string]interface{}{"type": "string"},
"webshellConnectionId": map[string]interface{}{"type": "string"},
},
"required": []string{"message"},
@@ -1711,6 +1729,10 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"queue": map[string]interface{}{
"$ref": "#/components/schemas/BatchQueue",
},
"started": map[string]interface{}{
"type": "boolean",
"description": "是否已立即启动执行",
},
},
},
},
+294 -9
View File
@@ -3621,7 +3621,7 @@ header {
color: var(--text-primary);
}
.form-group input,
.form-group input:not([type="checkbox"]):not([type="radio"]),
.form-group select {
padding: 10px 12px;
border: 1px solid var(--border-color);
@@ -3644,30 +3644,43 @@ header {
padding-right: 36px;
}
.form-group input:focus,
.form-group input:not([type="checkbox"]):not([type="radio"]):focus,
.form-group select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.form-group input:hover,
.form-group input:not([type="checkbox"]):not([type="radio"]):hover,
.form-group select:hover {
border-color: var(--accent-color);
}
.form-group input.error,
.form-group input:not([type="checkbox"]):not([type="radio"]).error,
.form-group select.error {
border-color: var(--error-color);
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
}
.form-group input.error:focus,
.form-group input:not([type="checkbox"]):not([type="radio"]).error:focus,
.form-group select.error:focus {
border-color: var(--error-color);
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.2);
}
.batch-execute-now-label {
display: inline-flex;
align-items: center;
gap: 8px;
width: fit-content;
cursor: pointer;
}
.batch-execute-now-label input[type="checkbox"] {
width: auto;
margin: 0;
}
/* 现代化复选框样式 */
.checkbox-label {
display: flex !important;
@@ -3826,6 +3839,41 @@ header {
align-items: center;
}
.tools-status-filter {
display: flex;
gap: 0;
flex-shrink: 0;
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
}
.tools-status-filter .btn-filter {
padding: 6px 12px;
border: none;
border-right: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.tools-status-filter .btn-filter:last-child {
border-right: none;
}
.tools-status-filter .btn-filter:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.tools-status-filter .btn-filter.active {
background: var(--accent-color);
color: #fff;
}
.page-size-selector {
display: flex;
align-items: center;
@@ -8160,6 +8208,21 @@ header {
.tasks-filters select {
min-width: 100%;
}
.batch-queues-filters.tasks-filters.batch-queues-filters--compact {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.batch-queues-filters.tasks-filters.batch-queues-filters--compact select {
min-width: 120px;
width: auto;
}
.batch-queues-filters__search {
flex: 1 1 100%;
}
.tasks-batch-actions {
flex-direction: column;
@@ -8210,8 +8273,10 @@ header {
.batch-queues-board .batch-queues-list {
margin-bottom: 0;
padding: 12px 16px;
padding: 14px 16px;
box-sizing: border-box;
/* 与卡片底色区分,flex gap 才能被看见,避免「白贴白」连成一片 */
background: var(--bg-secondary);
}
.batch-queues-board #batch-queues-pagination {
@@ -8233,6 +8298,45 @@ header {
margin-bottom: 12px;
}
/* 任务管理 · 筛选条:标签与控件同一行,降低占用高度 */
.batch-queues-filters.tasks-filters.batch-queues-filters--compact {
padding: 8px 12px;
margin-bottom: 10px;
gap: 10px 16px;
align-items: center;
}
.batch-queues-filters.tasks-filters.batch-queues-filters--compact label {
flex-direction: row;
align-items: center;
gap: 8px;
}
.batch-queues-filters.tasks-filters.batch-queues-filters--compact label > span {
flex-shrink: 0;
font-size: 0.8125rem;
max-width: 9em;
line-height: 1.25;
}
.batch-queues-filters.tasks-filters.batch-queues-filters--compact select {
min-width: 120px;
padding: 6px 10px;
font-size: 0.8125rem;
}
.batch-queues-filters.tasks-filters.batch-queues-filters--compact input[type="text"] {
padding: 6px 10px;
font-size: 0.8125rem;
min-width: 0;
}
.batch-queues-filters__search {
flex: 1 1 220px;
min-width: 0;
max-width: none;
}
.batch-queues-header {
display: flex;
justify-content: space-between;
@@ -8251,15 +8355,15 @@ header {
.batch-queues-list {
display: flex;
flex-direction: column;
gap: 12px;
gap: 14px;
margin-bottom: 28px;
}
.batch-queue-item {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 14px 16px;
border-radius: 12px;
padding: 11px 16px 12px;
box-shadow: var(--shadow-sm);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
cursor: pointer;
@@ -8286,6 +8390,148 @@ header {
min-width: 0;
}
/* 任务管理 · 队列卡片:单行主网格 + 进度列内统计,降低高度 */
.batch-queue-item__inner--grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(128px, auto) minmax(88px, 14%) 44px;
grid-template-rows: auto;
grid-template-areas: "lead cluster progress actions";
column-gap: 22px;
align-items: center;
gap: 0;
}
.batch-queue-item__lead {
grid-area: lead;
min-width: 0;
}
.batch-queue-item__title-row {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.batch-queue-item__role-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
line-height: 1;
border-radius: 9px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.batch-queue-item__titles {
flex: 1;
min-width: 0;
}
.batch-queue-item__inner--grid .batch-queue-card-title {
font-size: 0.9375rem;
line-height: 1.3;
}
.batch-queue-item__cluster {
grid-area: cluster;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
text-align: right;
max-width: min(100%, 360px);
justify-self: end;
}
/* 状态徽章与百分比同一行,用 gap 明确分隔(避免视觉上「贴住」) */
.batch-queue-item__status-inline {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 10px 20px;
max-width: 100%;
min-width: 0;
}
.batch-queue-item__progress-col {
grid-area: progress;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
align-self: center;
}
.batch-queue-item__actions-col {
grid-area: actions;
justify-self: end;
align-self: center;
padding-left: 6px;
}
.batch-queue-item__idline--lead {
display: block;
margin: 4px 0 0;
font-size: 0.6875rem;
color: var(--text-muted, var(--text-secondary));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.batch-queue-item__idline--lead code {
font-size: 0.65rem;
}
.batch-queue-item__inner--grid .batch-queue-item__config {
margin-top: 3px;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.batch-queue-item__inner--grid .batch-queue-item__sublabel {
font-size: 0.75rem;
color: var(--text-secondary);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.batch-queue-item__inner--grid .batch-queue-item__pct {
flex: 0 0 auto;
text-align: right;
font-size: 0.75rem;
letter-spacing: 0.02em;
font-weight: 600;
color: var(--text-primary);
line-height: 1.25;
white-space: nowrap;
}
.batch-queue-progress-bar--card-row {
height: 6px;
}
.batch-queue-item--no-actions .batch-queue-item__inner--grid {
grid-template-columns: minmax(0, 1fr) minmax(128px, auto) minmax(88px, 16%);
grid-template-areas: "lead cluster progress";
}
.batch-queue-item--no-actions .batch-queue-item__actions-col {
display: none;
}
.batch-queue-item__top {
display: flex;
align-items: flex-start;
@@ -9323,6 +9569,45 @@ header {
align-items: flex-start;
}
.batch-queue-item__inner--grid {
grid-template-columns: 1fr auto;
grid-template-areas:
"lead actions"
"cluster cluster"
"progress progress";
row-gap: 8px;
}
.batch-queue-item--no-actions .batch-queue-item__inner--grid {
grid-template-columns: 1fr;
grid-template-areas:
"lead"
"cluster"
"progress";
}
.batch-queue-item__cluster {
align-items: flex-start;
text-align: left;
max-width: none;
justify-self: stretch;
}
.batch-queue-item__status-inline {
justify-content: flex-start;
width: 100%;
}
.batch-queue-item__inner--grid .batch-queue-item__pct {
text-align: left;
}
.batch-queue-item__idline--lead {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
.batch-queue-item__top {
flex-wrap: wrap;
gap: 8px 10px;
+6
View File
@@ -507,6 +507,8 @@
"toolSearchPlaceholder": "Enter tool name...",
"statusFilter": "Status filter",
"filterAll": "All",
"filterEnabled": "Enabled",
"filterDisabled": "Disabled",
"selectedCount": "{{count}} selected",
"selectAll": "Select all",
"deselectAll": "Deselect all",
@@ -1515,6 +1517,8 @@
"cronExprPlaceholder": "e.g. 0 */2 * * * (run every 2 hours)",
"cronExprHint": "Use standard 5-field Cron: minute hour day month weekday. Example: `0 2 * * *` runs at 02:00 daily.",
"cronExprRequired": "Please fill in a Cron expression when Cron schedule is selected",
"executeNow": "Run immediately after creation",
"executeNowHint": "Default is off. When disabled, the queue stays pending and can be started manually later.",
"tasksList": "Task list (one task per line)",
"tasksListPlaceholder": "Enter task list, one per line",
"tasksListPlaceholderExample": "Enter task list, one per line, for example:\nScan open ports of 192.168.1.1\nCheck if https://example.com has SQL injection\nEnumerate subdomains of example.com",
@@ -1526,6 +1530,8 @@
"title": "Batch queue details",
"addTask": "Add task",
"startExecute": "Start",
"startExecuteNow": "Run now (one round)",
"startExecuteNowConfirm": "This is a Cron queue. Clicking Start will run the current round immediately instead of waiting for the next Cron time. Continue?",
"pauseQueue": "Pause queue",
"deleteQueue": "Delete queue",
"queueTitle": "Task title",
+6
View File
@@ -507,6 +507,8 @@
"toolSearchPlaceholder": "输入工具名称...",
"statusFilter": "状态筛选",
"filterAll": "全部",
"filterEnabled": "已启用",
"filterDisabled": "已停用",
"selectedCount": "已选择 {{count}} 项",
"selectAll": "全选",
"deselectAll": "全不选",
@@ -1515,6 +1517,8 @@
"cronExprPlaceholder": "例如:0 */2 * * *(每2小时执行一次)",
"cronExprHint": "采用标准 5 段 Cron:分 时 日 月 周,例如 `0 2 * * *` 表示每天 02:00 执行。",
"cronExprRequired": "请选择 Cron 调度后填写 Cron 表达式",
"executeNow": "创建后立即执行",
"executeNowHint": "默认不立即执行;关闭后队列保持待执行,可在需要时手动开始。",
"tasksList": "任务列表(每行一个任务)",
"tasksListPlaceholder": "请输入任务列表,每行一个任务",
"tasksListPlaceholderExample": "请输入任务列表,每行一个任务,例如:\n扫描 192.168.1.1 的开放端口\n检查 https://example.com 是否存在SQL注入\n枚举 example.com 的子域名",
@@ -1526,6 +1530,8 @@
"title": "批量任务队列详情",
"addTask": "添加任务",
"startExecute": "开始执行",
"startExecuteNow": "立即执行一轮",
"startExecuteNowConfirm": "这是 Cron 队列,点击后会立即执行当前这一轮,不会等待下次 Cron 时间。确定立即执行吗?",
"pauseQueue": "暂停队列",
"deleteQueue": "删除队列",
"queueTitle": "任务标题",
+3 -1
View File
@@ -232,7 +232,9 @@ function showSubmenuPopup(navItem, menuId) {
}
// 初始化页面
function initPage(pageId) {
async function initPage(pageId) {
// 等待 i18n 就绪,避免快速刷新时翻译函数未初始化导致页面显示原始占位符 key
if (window.i18nReady) await window.i18nReady;
switch(pageId) {
case 'dashboard':
if (typeof refreshDashboard === 'function') {
+22 -1
View File
@@ -273,10 +273,15 @@ async function loadConfig(loadTools = true) {
// 工具搜索关键词
let toolsSearchKeyword = '';
// 工具状态筛选: '' = 全部, 'true' = 已启用, 'false' = 已停用
let toolsStatusFilter = '';
// 加载工具列表(分页)
async function loadToolsList(page = 1, searchKeyword = '') {
// 等待 i18n 就绪,避免快速刷新时翻译函数未初始化导致显示占位符
if (window.i18nReady) await window.i18nReady;
const toolsList = document.getElementById('tools-list');
// 显示加载状态
if (toolsList) {
// 清空整个容器,包括可能存在的分页控件
@@ -292,6 +297,9 @@ async function loadToolsList(page = 1, searchKeyword = '') {
if (searchKeyword) {
url += `&search=${encodeURIComponent(searchKeyword)}`;
}
if (toolsStatusFilter !== '') {
url += `&enabled=${toolsStatusFilter}`;
}
// 使用较短的超时时间(10秒),避免长时间等待
const controller = new AbortController();
@@ -387,6 +395,17 @@ function handleSearchKeyPress(event) {
}
}
// 按状态筛选工具
function filterToolsByStatus(status) {
toolsStatusFilter = status;
// 更新按钮激活状态
document.querySelectorAll('.tools-status-filter .btn-filter').forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === status);
});
// 重置到第一页并重新加载
loadToolsList(1, toolsSearchKeyword);
}
// 渲染工具列表
function renderToolsList() {
const toolsList = document.getElementById('tools-list');
@@ -1224,6 +1243,8 @@ async function fetchExternalMCPs() {
// 加载外部MCP列表并渲染
async function loadExternalMCPs() {
try {
// 等待 i18n 就绪,避免快速刷新时翻译函数未初始化导致显示占位符
if (window.i18nReady) await window.i18nReady;
const data = await fetchExternalMCPs();
renderExternalMCPList(data.servers || {});
renderExternalMCPStats(data.stats || {});
+70 -26
View File
@@ -57,6 +57,15 @@ function getBatchQueueStatusPresentation(queue) {
return { ...base, ...empty };
}
/** 队列是否处于「可改子任务列表/文案」的空闲态(与后端 batch_task_manager.queueAllowsTaskListMutationLocked 对齐) */
function batchQueueAllowsSubtaskMutation(queue) {
if (!queue) return false;
if (queue.status === 'running') return false;
const hasRunningSubtask = Array.isArray(queue.tasks) && queue.tasks.some(t => t && t.status === 'running');
if (hasRunningSubtask) return false;
return queue.status === 'pending' || queue.status === 'paused' || queue.status === 'completed' || queue.status === 'cancelled';
}
// HTML转义函数(如果未定义)
if (typeof escapeHtml === 'undefined') {
function escapeHtml(text) {
@@ -782,6 +791,7 @@ async function showBatchImportModal() {
const agentModeSelect = document.getElementById('batch-queue-agent-mode');
const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode');
const cronExprInput = document.getElementById('batch-queue-cron-expr');
const executeNowCheckbox = document.getElementById('batch-queue-execute-now');
if (modal && input) {
input.value = '';
if (titleInput) {
@@ -800,6 +810,9 @@ async function showBatchImportModal() {
if (cronExprInput) {
cronExprInput.value = '';
}
if (executeNowCheckbox) {
executeNowCheckbox.checked = false;
}
handleBatchScheduleModeChange();
updateBatchImportStats('');
@@ -895,6 +908,7 @@ async function createBatchQueue() {
const agentModeSelect = document.getElementById('batch-queue-agent-mode');
const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode');
const cronExprInput = document.getElementById('batch-queue-cron-expr');
const executeNowCheckbox = document.getElementById('batch-queue-execute-now');
if (!input) return;
const text = input.value.trim();
@@ -918,6 +932,7 @@ async function createBatchQueue() {
const agentMode = agentModeSelect ? (agentModeSelect.value === 'multi' ? 'multi' : 'single') : 'single';
const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual';
const cronExpr = cronExprInput ? cronExprInput.value.trim() : '';
const executeNow = executeNowCheckbox ? !!executeNowCheckbox.checked : false;
if (scheduleMode === 'cron' && !cronExpr) {
alert(_t('batchImportModal.cronExprRequired'));
return;
@@ -929,7 +944,7 @@ async function createBatchQueue() {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, tasks, role, agentMode, scheduleMode, cronExpr }),
body: JSON.stringify({ title, tasks, role, agentMode, scheduleMode, cronExpr, executeNow }),
});
if (!response.ok) {
@@ -1118,34 +1133,34 @@ function renderBatchQueues() {
? `<h4 class="batch-queue-card-title">${escapeHtml(queue.title)}</h4>`
: `<h4 class="batch-queue-card-title batch-queue-card-title--muted">${escapeHtml(_t('tasks.batchQueueUntitled'))}</h4>`;
const doneCount = stats.completed + stats.failed + stats.cancelled;
const statsCompact = `<span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.totalLabel'))}\u00a0${stats.total}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.pendingLabel'))}\u00a0${stats.pending}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.runningLabel'))}\u00a0${stats.running}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item batch-queue-statsline__item--ok">${escapeHtml(_t('tasks.completedLabel'))}\u00a0${stats.completed}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item batch-queue-statsline__item--err">${escapeHtml(_t('tasks.failedLabel'))}\u00a0${stats.failed}</span>${stats.cancelled > 0 ? `<span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.cancelledLabel'))}\u00a0${stats.cancelled}</span>` : ''}`;
const noActionsClass = canDelete ? '' : ' batch-queue-item--no-actions';
return `
<div class="batch-queue-item batch-queue-item--compact${cardMod}" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
<div class="batch-queue-item__inner">
<div class="batch-queue-item__top">
<div class="batch-queue-item__title-col">
${titleBlock}
<p class="batch-queue-item__config">${configLine}${cronPausedNote}</p>
<p class="batch-queue-item__idline"><code title="${escapeHtml(queue.id)}">${shortId}</code><span class="batch-queue-item__idsep">\u00b7</span><span>${escapeHtml(_t('tasks.createdTimeLabel'))}\u00a0${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></p>
</div>
<div class="batch-queue-item__top-actions" onclick="event.stopPropagation();">
${canDelete ? `<button type="button" class="batch-queue-icon-btn" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
<div class="batch-queue-item batch-queue-item--compact${cardMod}${noActionsClass}" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
<div class="batch-queue-item__inner batch-queue-item__inner--grid">
<div class="batch-queue-item__lead">
<div class="batch-queue-item__title-row">
<span class="batch-queue-item__role-icon" aria-hidden="true">${escapeHtml(roleIcon)}</span>
<div class="batch-queue-item__titles">${titleBlock}</div>
</div>
<p class="batch-queue-item__config">${configLine}${cronPausedNote}</p>
<p class="batch-queue-item__idline batch-queue-item__idline--lead"><code title="${escapeHtml(queue.id)}">${shortId}</code><span class="batch-queue-item__idsep">\u00b7</span><span>${escapeHtml(_t('tasks.createdTimeLabel'))}\u00a0${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></p>
</div>
<div class="batch-queue-item__mid">
<div class="batch-queue-item__mid-left">
<div class="batch-queue-item__cluster">
<div class="batch-queue-item__status-inline">
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
${pres.sublabel ? `<span class="batch-queue-item__sublabel">${escapeHtml(pres.sublabel)}</span>` : ''}
</div>
<div class="batch-queue-item__mid-right">
<div class="batch-queue-progress-bar batch-queue-progress-bar--card batch-queue-progress-bar--list">
<div class="batch-queue-progress-fill${progressFillMod}" style="width: ${progress}%"></div>
</div>
<span class="batch-queue-item__pct">${progress}%\u00a0<span class="batch-queue-item__pct-frac">(${doneCount}/${stats.total})</span></span>
</div>
${pres.sublabel ? `<span class="batch-queue-item__sublabel">${escapeHtml(pres.sublabel)}</span>` : ''}
</div>
<div class="batch-queue-item__progress-col">
<div class="batch-queue-progress-bar batch-queue-progress-bar--card batch-queue-progress-bar--list batch-queue-progress-bar--card-row">
<div class="batch-queue-progress-fill${progressFillMod}" style="width: ${progress}%"></div>
</div>
</div>
<div class="batch-queue-item__actions-col" onclick="event.stopPropagation();">
${canDelete ? `<button type="button" class="batch-queue-icon-btn" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
</div>
<div class="batch-queue-statsline" aria-label="${escapeHtml(_t('tasks.batchQueueTitle'))}">${statsCompact}</div>
</div>
</div>
`;
@@ -1266,6 +1281,7 @@ async function showBatchQueueDetail(queueId) {
const queue = result.queue;
batchQueuesState.currentQueueId = queueId;
const pres = getBatchQueueStatusPresentation(queue);
const allowSubtaskMutation = batchQueueAllowsSubtaskMutation(queue);
if (title) {
// textContent 本身会做转义;这里不要再 escapeHtml,否则会把 && 显示成 &amp;...(看起来像“变形/乱码”)
@@ -1275,7 +1291,7 @@ async function showBatchQueueDetail(queueId) {
// 更新按钮显示
const pauseBtn = document.getElementById('batch-queue-pause-btn');
if (addTaskBtn) {
addTaskBtn.style.display = queue.status === 'pending' ? 'inline-block' : 'none';
addTaskBtn.style.display = allowSubtaskMutation ? 'inline-block' : 'none';
}
if (startBtn) {
// pending状态显示"开始执行"paused状态显示"继续执行"
@@ -1283,7 +1299,10 @@ async function showBatchQueueDetail(queueId) {
if (startBtn && queue.status === 'paused') {
startBtn.textContent = _t('tasks.resumeExecute');
} else if (startBtn && queue.status === 'pending') {
startBtn.textContent = _t('batchQueueDetailModal.startExecute');
const isCronPending = queue.scheduleMode === 'cron' && queue.scheduleEnabled !== false;
startBtn.textContent = isCronPending
? _t('batchQueueDetailModal.startExecuteNow')
: _t('batchQueueDetailModal.startExecute');
}
}
if (pauseBtn) {
@@ -1339,9 +1358,14 @@ async function showBatchQueueDetail(queueId) {
const tasksList = content.querySelector('.batch-queue-tasks-list');
const savedModalBodyScrollTop = modalBody ? modalBody.scrollTop : 0;
const savedTasksListScrollTop = tasksList ? tasksList.scrollTop : 0;
const prevTechDetails = content.querySelector('details.batch-queue-detail-tech');
const prevLayout = content.querySelector('.batch-queue-detail-layout[data-bq-detail-for]');
const prevDetailFor = prevLayout ? prevLayout.getAttribute('data-bq-detail-for') : null;
const sameQueueAsBefore = prevDetailFor === queue.id;
const savedTechDetailsOpen = sameQueueAsBefore && !!(prevTechDetails && prevTechDetails.open);
content.innerHTML = `
<div class="batch-queue-detail-layout">
<div class="batch-queue-detail-layout" data-bq-detail-for="${escapeHtml(queue.id)}">
<section class="batch-queue-detail-hero">
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
${pres.sublabel ? `<p class="batch-queue-detail-hero__sub">${escapeHtml(pres.sublabel)}</p>` : ''}
@@ -1374,7 +1398,7 @@ async function showBatchQueueDetail(queueId) {
<h4>` + _t('batchQueueDetailModal.taskList') + `</h4>
${queue.tasks.map((task, index) => {
const taskStatus = taskStatusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
const canEdit = queue.status === 'pending' && task.status === 'pending';
const canEdit = allowSubtaskMutation && task.status !== 'running';
const taskMessageEscaped = escapeHtml(task.message).replace(/'/g, "&#39;").replace(/"/g, "&quot;").replace(/\n/g, "\\n");
return `
<div class="batch-task-item ${task.status === 'running' ? 'batch-task-item-active' : ''}" data-queue-id="${queue.id}" data-task-id="${task.id}" data-task-message="${taskMessageEscaped}">
@@ -1405,11 +1429,18 @@ async function showBatchQueueDetail(queueId) {
newTasksList.scrollTop = savedTasksListScrollTop;
}
const newTechDetails = content.querySelector('details.batch-queue-detail-tech');
if (newTechDetails && savedTechDetailsOpen) {
newTechDetails.open = true;
}
modal.style.display = 'block';
// 如果队列正在运行,自动刷新
// 仅运行中定时拉取详情;其它状态应停止,避免 innerHTML 重绘把 <details> 等 UI 打回默认态
if (queue.status === 'running') {
startBatchQueueRefresh(queueId);
} else {
stopBatchQueueRefresh();
}
} catch (error) {
console.error('获取队列详情失败:', error);
@@ -1423,6 +1454,19 @@ async function startBatchQueue() {
if (!queueId) return;
try {
// Cron 队列点击“开始执行”会立即运行一轮,这里二次确认避免误触
const queueResponse = await apiFetch(`/api/batch-tasks/${queueId}`);
if (!queueResponse.ok) {
throw new Error(_t('tasks.getQueueDetailFailed'));
}
const queueResult = await queueResponse.json();
const queue = queueResult && queueResult.queue ? queueResult.queue : null;
const isCronPending = queue && queue.status === 'pending' && queue.scheduleMode === 'cron' && queue.scheduleEnabled !== false;
if (isCronPending) {
const okNow = confirm(_t('batchQueueDetailModal.startExecuteNowConfirm'));
if (!okNow) return;
}
const response = await apiFetch(`/api/batch-tasks/${queueId}/start`, {
method: 'POST',
});
+14 -2
View File
@@ -725,6 +725,11 @@
<div class="tools-actions">
<button class="btn-secondary" onclick="selectAllTools()" data-i18n="mcp.selectAll">全选</button>
<button class="btn-secondary" onclick="deselectAllTools()" data-i18n="mcp.deselectAll">全不选</button>
<div class="tools-status-filter">
<button class="btn-filter active" data-filter="" onclick="filterToolsByStatus('')" data-i18n="mcp.filterAll">全部</button>
<button class="btn-filter" data-filter="true" onclick="filterToolsByStatus('true')" data-i18n="mcp.filterEnabled">已启用</button>
<button class="btn-filter" data-filter="false" onclick="filterToolsByStatus('false')" data-i18n="mcp.filterDisabled">已停用</button>
</div>
<div class="search-box">
<input type="text" id="tools-search" data-i18n="mcp.toolSearchPlaceholder" data-i18n-attr="placeholder" placeholder="搜索工具..." onkeypress="handleSearchKeyPress(event)" oninput="if(this.value.trim() === '') clearSearch()" />
<button class="btn-search" onclick="searchTools()" data-i18n="common.search" data-i18n-attr="title" title="搜索">🔍</button>
@@ -1132,7 +1137,7 @@
<!-- 批量任务队列列表 -->
<div class="batch-queues-section" id="batch-queues-section" style="display: none;">
<!-- 筛选控件 -->
<div class="batch-queues-filters tasks-filters">
<div class="batch-queues-filters tasks-filters batch-queues-filters--compact">
<label>
<span data-i18n="tasksPage.statusFilter">状态筛选</span>
<select id="batch-queues-status-filter" onchange="filterBatchQueues()">
@@ -1144,7 +1149,7 @@
<option value="cancelled" data-i18n="tasksPage.statusCancelled">已取消</option>
</select>
</label>
<label style="flex: 1; max-width: 300px;">
<label class="batch-queues-filters__search">
<span data-i18n="tasksPage.searchQueuePlaceholder">搜索队列ID、标题或创建时间</span>
<input type="text" id="batch-queues-search" data-i18n="tasksPage.searchKeywordPlaceholder" data-i18n-attr="placeholder" placeholder="输入关键字搜索..."
oninput="filterBatchQueues()">
@@ -2336,6 +2341,13 @@ version: 1.0.0<br>
<input type="text" id="batch-queue-cron-expr" data-i18n="batchImportModal.cronExprPlaceholder" data-i18n-attr="placeholder" placeholder="例如:0 */2 * * *(每2小时执行一次)" />
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.cronExprHint">采用标准 5 段 Cron:分 时 日 月 周,例如 `0 2 * * *` 表示每天 02:00 执行。</div>
</div>
<div class="form-group">
<label for="batch-queue-execute-now" class="batch-execute-now-label">
<input type="checkbox" id="batch-queue-execute-now" />
<span data-i18n="batchImportModal.executeNow">创建后立即执行</span>
</label>
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.executeNowHint">默认不立即执行;关闭后队列保持待执行,可在需要时手动开始。</div>
</div>
<div class="form-group">
<label for="batch-tasks-input"><span data-i18n="batchImportModal.tasksList">任务列表(每行一个任务)</span><span style="color: red;">*</span></label>
<textarea id="batch-tasks-input" rows="15" data-i18n="batchImportModal.tasksListPlaceholderExample" data-i18n-attr="placeholder" placeholder="请输入任务列表,每行一个任务,例如:&#10;扫描 192.168.1.1 的开放端口&#10;检查 https://example.com 是否存在SQL注入&#10;枚举 example.com 的子域名" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>