mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-18 03:50:10 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e2b30c029 | |||
| 8c7c22369e | |||
| 9b1aba692b | |||
| db730b48c1 | |||
| dfb7dd7390 | |||
| 9f6eb33047 | |||
| 616d87f4cc | |||
| 8d999792b8 | |||
| afae8970d1 | |||
| 4d7330c5c3 | |||
| 8884bfb0b4 | |||
| fb351c80b6 | |||
| 664834e338 | |||
| 95bf62db88 | |||
| 656242614d | |||
| a9d6d8c00e | |||
| 0d6a43c0a8 | |||
| 702f286eb1 |
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.6.37"
|
version: "v1.6.38"
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||||
|
|||||||
@@ -69,12 +69,12 @@ func buildAuditLogsWhere(filter ListAuditLogsFilter) (string, []interface{}) {
|
|||||||
args = append(args, filter.ResourceID)
|
args = append(args, filter.ResourceID)
|
||||||
}
|
}
|
||||||
if filter.Since != nil {
|
if filter.Since != nil {
|
||||||
conditions = append(conditions, "created_at >= ?")
|
conditions = append(conditions, sqliteEpochGE("created_at", ">="))
|
||||||
args = append(args, *filter.Since)
|
args = append(args, formatSQLiteUTC(*filter.Since))
|
||||||
}
|
}
|
||||||
if filter.Until != nil {
|
if filter.Until != nil {
|
||||||
conditions = append(conditions, "created_at <= ?")
|
conditions = append(conditions, sqliteEpochGE("created_at", "<="))
|
||||||
args = append(args, *filter.Until)
|
args = append(args, formatSQLiteUTC(*filter.Until))
|
||||||
}
|
}
|
||||||
if q := strings.TrimSpace(filter.Query); q != "" {
|
if q := strings.TrimSpace(filter.Query); q != "" {
|
||||||
like := "%" + q + "%"
|
like := "%" + q + "%"
|
||||||
@@ -93,7 +93,9 @@ func (db *DB) AppendAuditLog(row *AuditLog) error {
|
|||||||
return errors.New("audit id is required")
|
return errors.New("audit id is required")
|
||||||
}
|
}
|
||||||
if row.CreatedAt.IsZero() {
|
if row.CreatedAt.IsZero() {
|
||||||
row.CreatedAt = time.Now()
|
row.CreatedAt = time.Now().UTC()
|
||||||
|
} else {
|
||||||
|
row.CreatedAt = row.CreatedAt.UTC()
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(row.Level) == "" {
|
if strings.TrimSpace(row.Level) == "" {
|
||||||
row.Level = "info"
|
row.Level = "info"
|
||||||
@@ -111,7 +113,7 @@ func (db *DB) AppendAuditLog(row *AuditLog) error {
|
|||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
_, err := db.Exec(query,
|
_, err := db.Exec(query,
|
||||||
row.ID, row.CreatedAt, row.Level, row.Category, row.Action, row.Result,
|
row.ID, formatSQLiteUTC(row.CreatedAt), row.Level, row.Category, row.Action, row.Result,
|
||||||
row.Actor, row.SessionHint, row.ClientIP, row.UserAgent,
|
row.Actor, row.SessionHint, row.ClientIP, row.UserAgent,
|
||||||
row.ResourceType, row.ResourceID, row.Message, detailJSON,
|
row.ResourceType, row.ResourceID, row.Message, detailJSON,
|
||||||
)
|
)
|
||||||
@@ -202,7 +204,7 @@ func (db *DB) ListAuditLogs(filter ListAuditLogsFilter) ([]*AuditLog, error) {
|
|||||||
|
|
||||||
// DeleteAuditLogsBefore removes rows older than cutoff.
|
// DeleteAuditLogsBefore removes rows older than cutoff.
|
||||||
func (db *DB) DeleteAuditLogsBefore(cutoff time.Time) (int64, error) {
|
func (db *DB) DeleteAuditLogsBefore(cutoff time.Time) (int64, error) {
|
||||||
res, err := db.Exec(`DELETE FROM audit_logs WHERE created_at < ?`, cutoff)
|
res, err := db.Exec(`DELETE FROM audit_logs WHERE `+sqliteEpochGE("created_at", "<"), formatSQLiteUTC(cutoff))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildAuditLogsWhere_timeFilterSQL(t *testing.T) {
|
||||||
|
since := time.Date(2026, 6, 16, 17, 2, 0, 0, time.UTC)
|
||||||
|
until := time.Date(2026, 6, 17, 3, 3, 0, 0, time.UTC)
|
||||||
|
where, args := buildAuditLogsWhere(ListAuditLogsFilter{Since: &since, Until: &until})
|
||||||
|
if !strings.Contains(where, "strftime('%s', created_at) >=") {
|
||||||
|
t.Fatalf("expected epoch comparison for since, got %q", where)
|
||||||
|
}
|
||||||
|
if !strings.Contains(where, "strftime('%s', created_at) <=") {
|
||||||
|
t.Fatalf("expected epoch comparison for until, got %q", where)
|
||||||
|
}
|
||||||
|
if len(args) != 2 {
|
||||||
|
t.Fatalf("expected 2 time args, got %d", len(args))
|
||||||
|
}
|
||||||
|
for i, arg := range args {
|
||||||
|
s, ok := arg.(string)
|
||||||
|
if !ok || s == "" {
|
||||||
|
t.Fatalf("arg %d: want non-empty UTC RFC3339 string, got %v", i, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAuditLogs_timeFilterMixedStorageFormats(t *testing.T) {
|
||||||
|
root, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
dbPath := filepath.Join(root, "..", "..", "data", "conversations.db")
|
||||||
|
if _, err := os.Stat(dbPath); err != nil {
|
||||||
|
t.Skip("conversations.db not found")
|
||||||
|
}
|
||||||
|
db, err := NewDB(dbPath, zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
since, _ := ParseRFC3339Time("2026-06-16T17:02:00Z")
|
||||||
|
until, _ := ParseRFC3339Time("2026-06-17T03:03:00Z")
|
||||||
|
filter := ListAuditLogsFilter{Since: &since, Until: &until, Limit: 50}
|
||||||
|
logs, err := db.ListAuditLogs(filter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, row := range logs {
|
||||||
|
at := row.CreatedAt.UTC()
|
||||||
|
if at.Before(since) || at.After(until) {
|
||||||
|
t.Fatalf("log %s at %s outside [%s, %s]", row.ID, at, since, until)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// formatSQLiteUTC stores instants as UTC RFC3339 for consistent SQLite reads/writes.
|
||||||
|
func formatSQLiteUTC(t time.Time) string {
|
||||||
|
return t.UTC().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqliteEpochGE returns SQL comparing column to param as Unix seconds (timezone-safe).
|
||||||
|
func sqliteEpochGE(column, op string) string {
|
||||||
|
return "strftime('%s', " + column + ") " + op + " strftime('%s', ?)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseRFC3339Time parses API/query timestamps (RFC3339 or RFC3339Nano).
|
||||||
|
func ParseRFC3339Time(value string) (time.Time, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return time.Time{}, errors.New("empty time value")
|
||||||
|
}
|
||||||
|
if t, err := time.Parse(time.RFC3339Nano, value); err == nil {
|
||||||
|
return t.UTC(), nil
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, value)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return t.UTC(), nil
|
||||||
|
}
|
||||||
@@ -1185,6 +1185,8 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
flushResponsePlan()
|
flushResponsePlan()
|
||||||
|
// 助手正文开始前,推理流通常已结束;落库以便刷新后「渗透测试详情」可回放
|
||||||
|
flushThinkingStreams()
|
||||||
respPlan.meta = nil
|
respPlan.meta = nil
|
||||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||||
respPlan.meta = make(map[string]interface{}, len(dataMap))
|
respPlan.meta = make(map[string]interface{}, len(dataMap))
|
||||||
@@ -1220,6 +1222,19 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
}
|
}
|
||||||
if eventType == "response" {
|
if eventType == "response" {
|
||||||
flushResponsePlan()
|
flushResponsePlan()
|
||||||
|
flushThinkingStreams()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if eventType == "done" {
|
||||||
|
flushResponsePlan()
|
||||||
|
flushThinkingStreams()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流式思考/推理结束:聚合落库(与 eino_agent_reply_stream_end 同理)
|
||||||
|
if eventType == "thinking_stream_end" || eventType == "reasoning_chain_stream_end" {
|
||||||
|
flushResponsePlan()
|
||||||
|
flushThinkingStreams()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/config"
|
"cyberstrike-ai/internal/config"
|
||||||
|
"cyberstrike-ai/internal/database"
|
||||||
|
"cyberstrike-ai/internal/openai"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -46,3 +50,50 @@ func TestCreateProgressCallback_ConcurrentToolEvents(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCreateProgressCallback_FlushesReasoningOnDone 流式推理聚合须在 done/response 时落库,刷新后可回放。
|
||||||
|
func TestCreateProgressCallback_FlushesReasoningOnDone(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
db, err := database.NewDB(filepath.Join(tmp, "test.sqlite"), zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewDB: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
conv, err := db.CreateConversation("test", database.ConversationCreateMeta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateConversation: %v", err)
|
||||||
|
}
|
||||||
|
asst, err := db.AddMessage(conv.ID, "assistant", "处理中...", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AddMessage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &AgentHandler{logger: zap.NewNop(), db: db}
|
||||||
|
cb := h.createProgressCallback(context.Background(), nil, conv.ID, asst.ID, nil)
|
||||||
|
|
||||||
|
streamID := "eino-reasoning-test-1"
|
||||||
|
cb("reasoning_chain_stream_start", " ", map[string]interface{}{
|
||||||
|
"streamId": streamID,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
cb("reasoning_chain_stream_delta", "step one", openai.WithSSEAccumulated(map[string]interface{}{
|
||||||
|
"streamId": streamID,
|
||||||
|
}, "step one"))
|
||||||
|
cb("done", "", map[string]interface{}{"conversationId": conv.ID})
|
||||||
|
|
||||||
|
details, err := db.GetProcessDetails(asst.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetProcessDetails: %v", err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, d := range details {
|
||||||
|
if d.EventType == "reasoning_chain" && d.Message == "step one" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected reasoning_chain persisted on done, got %+v", details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"cyberstrike-ai/internal/database"
|
"cyberstrike-ai/internal/database"
|
||||||
|
|
||||||
@@ -20,12 +19,12 @@ func auditFilterFromQuery(c *gin.Context) database.ListAuditLogsFilter {
|
|||||||
ResourceID: c.Query("resource_id"),
|
ResourceID: c.Query("resource_id"),
|
||||||
}
|
}
|
||||||
if since := c.Query("since"); since != "" {
|
if since := c.Query("since"); since != "" {
|
||||||
if t, err := time.Parse(time.RFC3339, since); err == nil {
|
if t, err := database.ParseRFC3339Time(since); err == nil {
|
||||||
filter.Since = &t
|
filter.Since = &t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if until := c.Query("until"); until != "" {
|
if until := c.Query("until"); until != "" {
|
||||||
if t, err := time.Parse(time.RFC3339, until); err == nil {
|
if t, err := database.ParseRFC3339Time(until); err == nil {
|
||||||
filter.Until = &t
|
filter.Until = &t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -785,6 +785,16 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if progress != nil && reasoningStreamID != "" && strings.TrimSpace(reasoningBuf) != "" {
|
||||||
|
progress("reasoning_chain_stream_end", openai.DisplayReasoningContent(strings.TrimSpace(reasoningBuf)), map[string]interface{}{
|
||||||
|
"streamId": reasoningStreamID,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": einoRoleTag(ev.AgentName),
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
if streamsMainAssistant(ev.AgentName) {
|
if streamsMainAssistant(ev.AgentName) {
|
||||||
s := strings.TrimSpace(mainAssistantBuf)
|
s := strings.TrimSpace(mainAssistantBuf)
|
||||||
if mainAssistDupTarget != "" {
|
if mainAssistDupTarget != "" {
|
||||||
|
|||||||
+876
-70
File diff suppressed because it is too large
Load Diff
@@ -1083,6 +1083,7 @@
|
|||||||
"botAgent": "Bot Agent",
|
"botAgent": "Bot Agent",
|
||||||
"ilinkBotId": "iLink Bot ID (filled after bind)",
|
"ilinkBotId": "iLink Bot ID (filled after bind)",
|
||||||
"boundSuccess": "Binding successful. WeChat bot is enabled.",
|
"boundSuccess": "Binding successful. WeChat bot is enabled.",
|
||||||
|
"alreadyBound": "This WeChat account is already bound.",
|
||||||
"openLink": "QR not showing? Open link in WeChat on your phone"
|
"openLink": "QR not showing? Open link in WeChat on your phone"
|
||||||
},
|
},
|
||||||
"wecom": {
|
"wecom": {
|
||||||
@@ -2086,14 +2087,35 @@
|
|||||||
"filterResult": "Result",
|
"filterResult": "Result",
|
||||||
"pageSize": "Per page",
|
"pageSize": "Per page",
|
||||||
"statTotal": "Filtered total",
|
"statTotal": "Filtered total",
|
||||||
|
"statSuccess": "Success",
|
||||||
"statFailures": "Failures",
|
"statFailures": "Failures",
|
||||||
"statRecent7d": "Last 7 days",
|
"statRecent7d": "Last 7 days",
|
||||||
"retentionHint": "Audit records are kept for {{days}} days, then purged automatically.",
|
"retentionHint": "Audit records are kept for {{days}} days, then purged automatically.",
|
||||||
"disabledHint": "Audit logging is disabled; new actions are not written.",
|
"disabledHint": "Audit logging is disabled; new actions are not written.",
|
||||||
"filterSince": "From",
|
"filterSince": "From",
|
||||||
"filterUntil": "Until",
|
"filterUntil": "Until",
|
||||||
|
"filterTimeZone": "Timezone: {{tz}} (filter uses your browser's local time)",
|
||||||
|
"datetimePlaceholder": "Select date & time",
|
||||||
|
"timePresets": "Quick range",
|
||||||
|
"preset15m": "Last 15 min",
|
||||||
|
"preset1h": "Last 1 hour",
|
||||||
|
"preset24h": "Last 24 hours",
|
||||||
|
"preset7d": "Last 7 days",
|
||||||
|
"presetToday": "Today",
|
||||||
|
"pickerHour": "Hour",
|
||||||
|
"pickerMinute": "Min",
|
||||||
|
"pickerClear": "Clear",
|
||||||
|
"pickerToday": "Today",
|
||||||
|
"pickerConfirm": "OK",
|
||||||
"filterQuery": "Keyword",
|
"filterQuery": "Keyword",
|
||||||
"filterQueryPlaceholder": "Message / resource ID / action",
|
"filterQueryPlaceholder": "Message / resource ID / action",
|
||||||
|
"colTime": "Time",
|
||||||
|
"colMessage": "Message",
|
||||||
|
"colCategory": "Category",
|
||||||
|
"colAction": "Action",
|
||||||
|
"colResult": "Result",
|
||||||
|
"colIp": "IP",
|
||||||
|
"colResource": "Resource ID",
|
||||||
"cat": {
|
"cat": {
|
||||||
"auth": "Auth",
|
"auth": "Auth",
|
||||||
"config": "Config",
|
"config": "Config",
|
||||||
@@ -2166,6 +2188,93 @@
|
|||||||
"exportDone": "Export complete",
|
"exportDone": "Export complete",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"empty": "No audit records",
|
"empty": "No audit records",
|
||||||
|
"result": {
|
||||||
|
"success": "success",
|
||||||
|
"failure": "failure"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"auth": {
|
||||||
|
"login": "Login successful",
|
||||||
|
"login_failed": "Login failed: incorrect password",
|
||||||
|
"logout": "Logged out",
|
||||||
|
"change_password": "Login password changed",
|
||||||
|
"change_password_failed": "Password change failed: current password incorrect"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"apply": "Configuration applied",
|
||||||
|
"update": "In-memory configuration updated",
|
||||||
|
"apply_fail_kb_init": "Failed to apply config: knowledge base init",
|
||||||
|
"apply_fail_kb_reinit": "Failed to apply config: knowledge base re-init",
|
||||||
|
"apply_fail_c2": "Failed to apply config: C2"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"create": "Conversation created",
|
||||||
|
"delete": "Conversation deleted",
|
||||||
|
"delete_turn": "Conversation turn deleted"
|
||||||
|
},
|
||||||
|
"c2": {
|
||||||
|
"listener_create": "C2 listener created",
|
||||||
|
"listener_delete": "C2 listener deleted",
|
||||||
|
"listener_start": "C2 listener started",
|
||||||
|
"listener_stop": "C2 listener stopped",
|
||||||
|
"session_delete": "C2 session deleted",
|
||||||
|
"task_create": "C2 task created",
|
||||||
|
"task_cancel": "C2 task cancelled",
|
||||||
|
"task_delete": "C2 tasks deleted (batch)"
|
||||||
|
},
|
||||||
|
"webshell": {
|
||||||
|
"connection_create": "WebShell connection created",
|
||||||
|
"connection_delete": "WebShell connection deleted"
|
||||||
|
},
|
||||||
|
"knowledge": {
|
||||||
|
"item_delete": "Knowledge item deleted",
|
||||||
|
"index_rebuild": "Knowledge index rebuilt"
|
||||||
|
},
|
||||||
|
"vulnerability": {
|
||||||
|
"create": "Vulnerability record created",
|
||||||
|
"update": "Vulnerability record updated",
|
||||||
|
"delete": "Vulnerability record deleted",
|
||||||
|
"delete_batch": "Vulnerability records deleted (batch)"
|
||||||
|
},
|
||||||
|
"external_mcp": {
|
||||||
|
"upsert": "External MCP configuration updated",
|
||||||
|
"delete": "External MCP configuration deleted"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"create_queue": "Batch task queue created",
|
||||||
|
"start_queue": "Batch task queue started",
|
||||||
|
"delete_queue": "Batch task queue deleted",
|
||||||
|
"pause_queue": "Batch task queue paused",
|
||||||
|
"rerun_queue": "Batch task queue rerun",
|
||||||
|
"delete_batch_task": "Batch subtask deleted"
|
||||||
|
},
|
||||||
|
"tool": {
|
||||||
|
"execution_delete": "Tool execution record deleted",
|
||||||
|
"execution_delete_batch": "Tool execution records deleted (batch)"
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"upload": "Chat attachment uploaded",
|
||||||
|
"delete": "Chat attachment deleted"
|
||||||
|
},
|
||||||
|
"hitl": {
|
||||||
|
"decision": "HITL approval decision"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"create": "Role created",
|
||||||
|
"update": "Role updated",
|
||||||
|
"delete": "Role deleted"
|
||||||
|
},
|
||||||
|
"skill": {
|
||||||
|
"create": "Skill created",
|
||||||
|
"update": "Skill updated",
|
||||||
|
"delete": "Skill deleted"
|
||||||
|
},
|
||||||
|
"agent": {
|
||||||
|
"markdown_create": "Markdown sub-agent created",
|
||||||
|
"markdown_update": "Markdown sub-agent updated",
|
||||||
|
"markdown_delete": "Markdown sub-agent deleted"
|
||||||
|
}
|
||||||
|
},
|
||||||
"paginationShow": "{{start}}-{{end}} of {{total}}",
|
"paginationShow": "{{start}}-{{end}} of {{total}}",
|
||||||
"detailTitle": "Audit detail",
|
"detailTitle": "Audit detail",
|
||||||
"detailTime": "Time",
|
"detailTime": "Time",
|
||||||
|
|||||||
@@ -1071,6 +1071,7 @@
|
|||||||
"botAgent": "Bot Agent",
|
"botAgent": "Bot Agent",
|
||||||
"ilinkBotId": "iLink Bot ID(绑定后自动填充)",
|
"ilinkBotId": "iLink Bot ID(绑定后自动填充)",
|
||||||
"boundSuccess": "绑定成功,微信机器人已启用。",
|
"boundSuccess": "绑定成功,微信机器人已启用。",
|
||||||
|
"alreadyBound": "该微信已绑定过,无需重复绑定。",
|
||||||
"openLink": "无法显示二维码?点击用手机微信打开链接"
|
"openLink": "无法显示二维码?点击用手机微信打开链接"
|
||||||
},
|
},
|
||||||
"wecom": {
|
"wecom": {
|
||||||
@@ -2074,14 +2075,35 @@
|
|||||||
"filterResult": "结果",
|
"filterResult": "结果",
|
||||||
"pageSize": "每页",
|
"pageSize": "每页",
|
||||||
"statTotal": "当前筛选",
|
"statTotal": "当前筛选",
|
||||||
|
"statSuccess": "成功",
|
||||||
"statFailures": "失败",
|
"statFailures": "失败",
|
||||||
"statRecent7d": "近 7 天",
|
"statRecent7d": "近 7 天",
|
||||||
"retentionHint": "审计记录保留 {{days}} 天,超期自动清理。",
|
"retentionHint": "审计记录保留 {{days}} 天,超期自动清理。",
|
||||||
"disabledHint": "审计功能已关闭,新操作不会写入审计表。",
|
"disabledHint": "审计功能已关闭,新操作不会写入审计表。",
|
||||||
"filterSince": "开始时间",
|
"filterSince": "开始时间",
|
||||||
"filterUntil": "结束时间",
|
"filterUntil": "结束时间",
|
||||||
|
"filterTimeZone": "时区:{{tz}}(筛选按浏览器本地时间)",
|
||||||
|
"datetimePlaceholder": "选择日期时间",
|
||||||
|
"timePresets": "快捷",
|
||||||
|
"preset15m": "最近15分钟",
|
||||||
|
"preset1h": "最近1小时",
|
||||||
|
"preset24h": "最近24小时",
|
||||||
|
"preset7d": "最近7天",
|
||||||
|
"presetToday": "今天",
|
||||||
|
"pickerHour": "时",
|
||||||
|
"pickerMinute": "分",
|
||||||
|
"pickerClear": "清除",
|
||||||
|
"pickerToday": "今天",
|
||||||
|
"pickerConfirm": "确定",
|
||||||
"filterQuery": "关键词",
|
"filterQuery": "关键词",
|
||||||
"filterQueryPlaceholder": "消息 / 资源 ID / 操作名",
|
"filterQueryPlaceholder": "消息 / 资源 ID / 操作名",
|
||||||
|
"colTime": "时间",
|
||||||
|
"colMessage": "说明",
|
||||||
|
"colCategory": "类别",
|
||||||
|
"colAction": "操作",
|
||||||
|
"colResult": "结果",
|
||||||
|
"colIp": "IP",
|
||||||
|
"colResource": "资源 ID",
|
||||||
"cat": {
|
"cat": {
|
||||||
"auth": "认证",
|
"auth": "认证",
|
||||||
"config": "配置",
|
"config": "配置",
|
||||||
@@ -2154,6 +2176,93 @@
|
|||||||
"exportDone": "导出完成",
|
"exportDone": "导出完成",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"empty": "暂无审计记录",
|
"empty": "暂无审计记录",
|
||||||
|
"result": {
|
||||||
|
"success": "成功",
|
||||||
|
"failure": "失败"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"auth": {
|
||||||
|
"login": "登录成功",
|
||||||
|
"login_failed": "登录失败:密码错误",
|
||||||
|
"logout": "退出登录",
|
||||||
|
"change_password": "登录密码已修改",
|
||||||
|
"change_password_failed": "修改密码失败:当前密码不正确"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"apply": "配置已应用",
|
||||||
|
"update": "更新内存配置",
|
||||||
|
"apply_fail_kb_init": "应用配置失败:初始化知识库",
|
||||||
|
"apply_fail_kb_reinit": "应用配置失败:重新初始化知识库",
|
||||||
|
"apply_fail_c2": "应用配置失败:C2"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"create": "创建对话",
|
||||||
|
"delete": "删除对话",
|
||||||
|
"delete_turn": "删除对话轮次"
|
||||||
|
},
|
||||||
|
"c2": {
|
||||||
|
"listener_create": "创建 C2 监听器",
|
||||||
|
"listener_delete": "删除 C2 监听器",
|
||||||
|
"listener_start": "启动 C2 监听器",
|
||||||
|
"listener_stop": "停止 C2 监听器",
|
||||||
|
"session_delete": "删除 C2 会话",
|
||||||
|
"task_create": "创建 C2 任务",
|
||||||
|
"task_cancel": "取消 C2 任务",
|
||||||
|
"task_delete": "批量删除 C2 任务"
|
||||||
|
},
|
||||||
|
"webshell": {
|
||||||
|
"connection_create": "创建 WebShell 连接",
|
||||||
|
"connection_delete": "删除 WebShell 连接"
|
||||||
|
},
|
||||||
|
"knowledge": {
|
||||||
|
"item_delete": "删除知识项",
|
||||||
|
"index_rebuild": "重建知识库索引"
|
||||||
|
},
|
||||||
|
"vulnerability": {
|
||||||
|
"create": "创建漏洞记录",
|
||||||
|
"update": "更新漏洞记录",
|
||||||
|
"delete": "删除漏洞记录",
|
||||||
|
"delete_batch": "批量删除漏洞记录"
|
||||||
|
},
|
||||||
|
"external_mcp": {
|
||||||
|
"upsert": "更新外部 MCP 配置",
|
||||||
|
"delete": "删除外部 MCP 配置"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"create_queue": "创建批量任务队列",
|
||||||
|
"start_queue": "启动批量任务队列",
|
||||||
|
"delete_queue": "删除批量任务队列",
|
||||||
|
"pause_queue": "暂停批量任务队列",
|
||||||
|
"rerun_queue": "重跑批量任务队列",
|
||||||
|
"delete_batch_task": "删除批量子任务"
|
||||||
|
},
|
||||||
|
"tool": {
|
||||||
|
"execution_delete": "删除工具执行记录",
|
||||||
|
"execution_delete_batch": "批量删除工具执行记录"
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"upload": "上传对话附件",
|
||||||
|
"delete": "删除对话附件"
|
||||||
|
},
|
||||||
|
"hitl": {
|
||||||
|
"decision": "HITL 审批决策"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"create": "创建角色",
|
||||||
|
"update": "更新角色",
|
||||||
|
"delete": "删除角色"
|
||||||
|
},
|
||||||
|
"skill": {
|
||||||
|
"create": "创建 Skill",
|
||||||
|
"update": "更新 Skill",
|
||||||
|
"delete": "删除 Skill"
|
||||||
|
},
|
||||||
|
"agent": {
|
||||||
|
"markdown_create": "创建 Markdown 子代理",
|
||||||
|
"markdown_update": "更新 Markdown 子代理",
|
||||||
|
"markdown_delete": "删除 Markdown 子代理"
|
||||||
|
}
|
||||||
|
},
|
||||||
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条",
|
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条",
|
||||||
"detailTitle": "审计详情",
|
"detailTitle": "审计详情",
|
||||||
"detailTime": "时间",
|
"detailTime": "时间",
|
||||||
|
|||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/**
|
||||||
|
* Audit log datetime picker — cross-browser, locale-aware (SLS-style calendar + time columns).
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var registry = {};
|
||||||
|
var popover = null;
|
||||||
|
var activeFieldId = null;
|
||||||
|
var draft = null;
|
||||||
|
var viewYear = 0;
|
||||||
|
var viewMonth = 0;
|
||||||
|
|
||||||
|
function pad2(n) {
|
||||||
|
return String(n).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickerLocale() {
|
||||||
|
if (typeof auditLocale === 'function') return auditLocale();
|
||||||
|
if (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) return 'zh-CN';
|
||||||
|
return 'en-US';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickerT(key, fallback) {
|
||||||
|
if (typeof auditT === 'function') return auditT(key, null, fallback);
|
||||||
|
if (typeof t === 'function') {
|
||||||
|
var v = t(key);
|
||||||
|
if (v && v !== key) return v;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function partsToStorage(p) {
|
||||||
|
if (!p) return '';
|
||||||
|
return p.y + '-' + pad2(p.m) + '-' + pad2(p.d) + 'T' + pad2(p.h) + ':' + pad2(p.mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStorage(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
var m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(String(value).trim());
|
||||||
|
if (!m) return null;
|
||||||
|
return { y: +m[1], m: +m[2], d: +m[3], h: +m[4], mi: +m[5] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDisplay(parts) {
|
||||||
|
if (!parts) return '';
|
||||||
|
var loc = pickerLocale();
|
||||||
|
try {
|
||||||
|
var d = new Date(parts.y, parts.m - 1, parts.d, parts.h, parts.mi, 0, 0);
|
||||||
|
return d.toLocaleString(loc, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
return partsToStorage(parts).replace('T', ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowParts() {
|
||||||
|
var n = new Date();
|
||||||
|
return { y: n.getFullYear(), m: n.getMonth() + 1, d: n.getDate(), h: n.getHours(), mi: n.getMinutes() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfTodayParts() {
|
||||||
|
var n = new Date();
|
||||||
|
return { y: n.getFullYear(), m: n.getMonth() + 1, d: n.getDate(), h: 0, mi: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthTitle(year, month) {
|
||||||
|
var loc = pickerLocale();
|
||||||
|
if (loc.startsWith('zh')) {
|
||||||
|
return year + '\u5e74' + pad2(month) + '\u6708';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new Date(year, month - 1, 1).toLocaleString(loc, { month: 'long', year: 'numeric' });
|
||||||
|
} catch (_) {
|
||||||
|
return year + '-' + pad2(month);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function weekdayHeaders() {
|
||||||
|
var loc = pickerLocale();
|
||||||
|
if (loc.startsWith('zh')) {
|
||||||
|
return ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'];
|
||||||
|
}
|
||||||
|
return ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMonthGrid(year, month) {
|
||||||
|
var first = new Date(year, month - 1, 1);
|
||||||
|
var start = new Date(first);
|
||||||
|
start.setDate(first.getDate() - first.getDay());
|
||||||
|
var cells = [];
|
||||||
|
var cursor = new Date(start);
|
||||||
|
for (var i = 0; i < 42; i++) {
|
||||||
|
cells.push({
|
||||||
|
y: cursor.getFullYear(),
|
||||||
|
m: cursor.getMonth() + 1,
|
||||||
|
d: cursor.getDate(),
|
||||||
|
inMonth: cursor.getMonth() === month - 1
|
||||||
|
});
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePopover() {
|
||||||
|
if (popover) return popover;
|
||||||
|
popover = document.createElement('div');
|
||||||
|
popover.className = 'audit-dt-popover';
|
||||||
|
popover.hidden = true;
|
||||||
|
popover.setAttribute('role', 'dialog');
|
||||||
|
popover.innerHTML =
|
||||||
|
'<div class="audit-dt-popover-inner">' +
|
||||||
|
'<div class="audit-dt-head">' +
|
||||||
|
'<button type="button" class="audit-dt-nav" data-nav="prev" aria-label="prev">‹</button>' +
|
||||||
|
'<span class="audit-dt-month-label"></span>' +
|
||||||
|
'<button type="button" class="audit-dt-nav" data-nav="next" aria-label="next">›</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="audit-dt-body">' +
|
||||||
|
'<div class="audit-dt-calendar"></div>' +
|
||||||
|
'<div class="audit-dt-time">' +
|
||||||
|
'<div class="audit-dt-time-col" data-part="hour">' +
|
||||||
|
'<span class="audit-dt-time-label audit-dt-hour-label"></span>' +
|
||||||
|
'<div class="audit-dt-time-list"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="audit-dt-time-col" data-part="minute">' +
|
||||||
|
'<span class="audit-dt-time-label audit-dt-minute-label"></span>' +
|
||||||
|
'<div class="audit-dt-time-list"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="audit-dt-footer">' +
|
||||||
|
'<button type="button" class="audit-dt-footer-btn" data-action="clear"></button>' +
|
||||||
|
'<button type="button" class="audit-dt-footer-btn" data-action="today"></button>' +
|
||||||
|
'<button type="button" class="audit-dt-footer-btn audit-dt-footer-btn--primary" data-action="confirm"></button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(popover);
|
||||||
|
|
||||||
|
popover.addEventListener('click', function (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
var btn = ev.target.closest('[data-nav]');
|
||||||
|
if (btn) {
|
||||||
|
if (btn.getAttribute('data-nav') === 'prev') {
|
||||||
|
viewMonth -= 1;
|
||||||
|
if (viewMonth < 1) { viewMonth = 12; viewYear -= 1; }
|
||||||
|
} else {
|
||||||
|
viewMonth += 1;
|
||||||
|
if (viewMonth > 12) { viewMonth = 1; viewYear += 1; }
|
||||||
|
}
|
||||||
|
renderPopover();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dayBtn = ev.target.closest('[data-day]');
|
||||||
|
if (dayBtn && draft) {
|
||||||
|
draft.y = +dayBtn.getAttribute('data-y');
|
||||||
|
draft.m = +dayBtn.getAttribute('data-m');
|
||||||
|
draft.d = +dayBtn.getAttribute('data-d');
|
||||||
|
if (draft.y !== viewYear || draft.m !== viewMonth) {
|
||||||
|
viewYear = draft.y;
|
||||||
|
viewMonth = draft.m;
|
||||||
|
renderCalendar();
|
||||||
|
} else {
|
||||||
|
updateDaySelection();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var timeBtn = ev.target.closest('[data-time]');
|
||||||
|
if (timeBtn && draft) {
|
||||||
|
var part = timeBtn.getAttribute('data-part');
|
||||||
|
var val = +timeBtn.getAttribute('data-time');
|
||||||
|
if (part === 'hour') draft.h = val;
|
||||||
|
if (part === 'minute') draft.mi = val;
|
||||||
|
updateTimeSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var actionBtn = ev.target.closest('[data-action]');
|
||||||
|
if (!actionBtn) return;
|
||||||
|
var action = actionBtn.getAttribute('data-action');
|
||||||
|
if (action === 'clear') {
|
||||||
|
applyValue(activeFieldId, '');
|
||||||
|
closePopover();
|
||||||
|
} else if (action === 'today') {
|
||||||
|
if (draft) {
|
||||||
|
var t = nowParts();
|
||||||
|
draft.y = t.y; draft.m = t.m; draft.d = t.d;
|
||||||
|
viewYear = t.y; viewMonth = t.m;
|
||||||
|
}
|
||||||
|
renderPopover();
|
||||||
|
} else if (action === 'confirm') {
|
||||||
|
applyValue(activeFieldId, partsToStorage(draft));
|
||||||
|
closePopover();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', onDocumentClick);
|
||||||
|
document.addEventListener('keydown', onDocumentKeydown);
|
||||||
|
document.addEventListener('languagechange', function () {
|
||||||
|
if (!popover.hidden) renderPopover();
|
||||||
|
refreshAllDisplays();
|
||||||
|
});
|
||||||
|
|
||||||
|
return popover;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentClick(ev) {
|
||||||
|
if (!popover || popover.hidden) return;
|
||||||
|
if (popover.contains(ev.target)) return;
|
||||||
|
if (activeFieldId && registry[activeFieldId] && registry[activeFieldId].wrap.contains(ev.target)) return;
|
||||||
|
closePopover();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentKeydown(ev) {
|
||||||
|
if (ev.key === 'Escape' && popover && !popover.hidden) {
|
||||||
|
closePopover();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionPopover(fieldWrap) {
|
||||||
|
var rect = fieldWrap.getBoundingClientRect();
|
||||||
|
var width = 320;
|
||||||
|
popover.style.width = width + 'px';
|
||||||
|
var left = rect.left;
|
||||||
|
if (left + width > window.innerWidth - 12) {
|
||||||
|
left = Math.max(12, window.innerWidth - width - 12);
|
||||||
|
}
|
||||||
|
popover.style.left = left + 'px';
|
||||||
|
var top = rect.bottom + 6;
|
||||||
|
if (top + 340 > window.innerHeight - 12) {
|
||||||
|
top = Math.max(12, rect.top - 340 - 6);
|
||||||
|
}
|
||||||
|
popover.style.top = top + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCalendar() {
|
||||||
|
if (!popover || !draft) return;
|
||||||
|
popover.querySelector('.audit-dt-month-label').textContent = monthTitle(viewYear, viewMonth);
|
||||||
|
var cal = popover.querySelector('.audit-dt-calendar');
|
||||||
|
var headers = weekdayHeaders();
|
||||||
|
var html = '<div class="audit-dt-weekdays">';
|
||||||
|
headers.forEach(function (h) { html += '<span>' + h + '</span>'; });
|
||||||
|
html += '</div><div class="audit-dt-days">';
|
||||||
|
buildMonthGrid(viewYear, viewMonth).forEach(function (cell) {
|
||||||
|
var cls = 'audit-dt-day';
|
||||||
|
if (!cell.inMonth) cls += ' is-other-month';
|
||||||
|
if (draft && cell.y === draft.y && cell.m === draft.m && cell.d === draft.d) cls += ' is-selected';
|
||||||
|
html += '<button type="button" class="' + cls + '" data-day="1" data-y="' + cell.y +
|
||||||
|
'" data-m="' + cell.m + '" data-d="' + cell.d + '">' + cell.d + '</button>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
cal.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTimeLists() {
|
||||||
|
if (!popover || !draft) return;
|
||||||
|
var hourList = popover.querySelector('[data-part="hour"] .audit-dt-time-list');
|
||||||
|
var minuteList = popover.querySelector('[data-part="minute"] .audit-dt-time-list');
|
||||||
|
var hourHtml = '';
|
||||||
|
var minuteHtml = '';
|
||||||
|
var h;
|
||||||
|
for (h = 0; h < 24; h++) {
|
||||||
|
hourHtml += '<button type="button" class="audit-dt-time-item' + (draft && draft.h === h ? ' is-selected' : '') +
|
||||||
|
'" data-part="hour" data-time="' + h + '">' + pad2(h) + '</button>';
|
||||||
|
}
|
||||||
|
for (h = 0; h < 60; h++) {
|
||||||
|
minuteHtml += '<button type="button" class="audit-dt-time-item' + (draft && draft.mi === h ? ' is-selected' : '') +
|
||||||
|
'" data-part="minute" data-time="' + h + '">' + pad2(h) + '</button>';
|
||||||
|
}
|
||||||
|
hourList.innerHTML = hourHtml;
|
||||||
|
minuteList.innerHTML = minuteHtml;
|
||||||
|
scrollTimeSelection(hourList, draft.h);
|
||||||
|
scrollTimeSelection(minuteList, draft.mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDaySelection() {
|
||||||
|
if (!popover || !draft) return;
|
||||||
|
popover.querySelectorAll('.audit-dt-day').forEach(function (btn) {
|
||||||
|
var selected = +btn.getAttribute('data-y') === draft.y &&
|
||||||
|
+btn.getAttribute('data-m') === draft.m &&
|
||||||
|
+btn.getAttribute('data-d') === draft.d;
|
||||||
|
btn.classList.toggle('is-selected', selected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimeSelection() {
|
||||||
|
if (!popover || !draft) return;
|
||||||
|
var hourList = popover.querySelector('[data-part="hour"] .audit-dt-time-list');
|
||||||
|
var minuteList = popover.querySelector('[data-part="minute"] .audit-dt-time-list');
|
||||||
|
if (!hourList || !minuteList || !hourList.children.length) {
|
||||||
|
renderTimeLists();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hourList.querySelectorAll('.audit-dt-time-item').forEach(function (btn) {
|
||||||
|
btn.classList.toggle('is-selected', +btn.getAttribute('data-time') === draft.h);
|
||||||
|
});
|
||||||
|
minuteList.querySelectorAll('.audit-dt-time-item').forEach(function (btn) {
|
||||||
|
btn.classList.toggle('is-selected', +btn.getAttribute('data-time') === draft.mi);
|
||||||
|
});
|
||||||
|
scrollTimeSelection(hourList, draft.h);
|
||||||
|
scrollTimeSelection(minuteList, draft.mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPopover() {
|
||||||
|
if (!popover || !draft) return;
|
||||||
|
popover.querySelector('.audit-dt-hour-label').textContent = pickerT('settingsAudit.pickerHour', 'Hour');
|
||||||
|
popover.querySelector('.audit-dt-minute-label').textContent = pickerT('settingsAudit.pickerMinute', 'Min');
|
||||||
|
popover.querySelector('[data-action="clear"]').textContent = pickerT('settingsAudit.pickerClear', 'Clear');
|
||||||
|
popover.querySelector('[data-action="today"]').textContent = pickerT('settingsAudit.pickerToday', 'Today');
|
||||||
|
popover.querySelector('[data-action="confirm"]').textContent = pickerT('settingsAudit.pickerConfirm', 'OK');
|
||||||
|
renderCalendar();
|
||||||
|
renderTimeLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollTimeSelection(listEl, value) {
|
||||||
|
var sel = listEl.querySelector('.is-selected');
|
||||||
|
if (sel && sel.scrollIntoView) {
|
||||||
|
sel.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPopover(fieldId) {
|
||||||
|
ensurePopover();
|
||||||
|
var entry = registry[fieldId];
|
||||||
|
if (!entry) return;
|
||||||
|
activeFieldId = fieldId;
|
||||||
|
var stored = entry.wrap.dataset.value || '';
|
||||||
|
draft = parseStorage(stored) || nowParts();
|
||||||
|
viewYear = draft.y;
|
||||||
|
viewMonth = draft.m;
|
||||||
|
renderPopover();
|
||||||
|
positionPopover(entry.wrap);
|
||||||
|
popover.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePopover() {
|
||||||
|
if (!popover) return;
|
||||||
|
popover.hidden = true;
|
||||||
|
activeFieldId = null;
|
||||||
|
draft = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDisplay(fieldId) {
|
||||||
|
var entry = registry[fieldId];
|
||||||
|
if (!entry) return;
|
||||||
|
var parts = parseStorage(entry.wrap.dataset.value || '');
|
||||||
|
entry.input.value = parts ? formatDisplay(parts) : '';
|
||||||
|
entry.input.placeholder = pickerT('settingsAudit.datetimePlaceholder', 'Select date & time');
|
||||||
|
entry.clearBtn.hidden = !parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAllDisplays() {
|
||||||
|
Object.keys(registry).forEach(refreshDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyValue(fieldId, storageValue) {
|
||||||
|
var entry = registry[fieldId];
|
||||||
|
if (!entry) return;
|
||||||
|
entry.wrap.dataset.value = storageValue || '';
|
||||||
|
refreshDisplay(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindField(fieldId) {
|
||||||
|
var wrap = document.getElementById(fieldId);
|
||||||
|
if (!wrap || wrap.dataset.auditDtBound === '1') return;
|
||||||
|
var input = wrap.querySelector('.audit-datetime-input');
|
||||||
|
var openBtn = wrap.querySelector('.audit-datetime-open-btn');
|
||||||
|
var clearBtn = wrap.querySelector('.audit-datetime-clear-btn');
|
||||||
|
if (!input || !openBtn || !clearBtn) return;
|
||||||
|
|
||||||
|
wrap.dataset.auditDtBound = '1';
|
||||||
|
registry[fieldId] = { wrap: wrap, input: input, clearBtn: clearBtn };
|
||||||
|
|
||||||
|
openBtn.addEventListener('click', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!popover || popover.hidden || activeFieldId !== fieldId) {
|
||||||
|
openPopover(fieldId);
|
||||||
|
} else {
|
||||||
|
closePopover();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.addEventListener('click', function (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
openPopover(fieldId);
|
||||||
|
});
|
||||||
|
clearBtn.addEventListener('click', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
applyValue(fieldId, '');
|
||||||
|
});
|
||||||
|
refreshDisplay(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AuditDatetimePicker = {
|
||||||
|
init: function () {
|
||||||
|
bindField('audit-filter-since-field');
|
||||||
|
bindField('audit-filter-until-field');
|
||||||
|
refreshAllDisplays();
|
||||||
|
},
|
||||||
|
getValue: function (inputId) {
|
||||||
|
var fieldId = inputId === 'audit-filter-since' ? 'audit-filter-since-field' : 'audit-filter-until-field';
|
||||||
|
var entry = registry[fieldId];
|
||||||
|
return entry ? (entry.wrap.dataset.value || '') : '';
|
||||||
|
},
|
||||||
|
setValue: function (inputId, dateObj) {
|
||||||
|
if (!dateObj || Number.isNaN(dateObj.getTime())) return;
|
||||||
|
var fieldId = inputId === 'audit-filter-since' ? 'audit-filter-since-field' : 'audit-filter-until-field';
|
||||||
|
var p = {
|
||||||
|
y: dateObj.getFullYear(),
|
||||||
|
m: dateObj.getMonth() + 1,
|
||||||
|
d: dateObj.getDate(),
|
||||||
|
h: dateObj.getHours(),
|
||||||
|
mi: dateObj.getMinutes()
|
||||||
|
};
|
||||||
|
applyValue(fieldId, partsToStorage(p));
|
||||||
|
},
|
||||||
|
clearAll: function () {
|
||||||
|
applyValue('audit-filter-since-field', '');
|
||||||
|
applyValue('audit-filter-until-field', '');
|
||||||
|
closePopover();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
+352
-56
@@ -4,6 +4,7 @@
|
|||||||
let auditLogsPage = 1;
|
let auditLogsPage = 1;
|
||||||
let auditLogsPageSize = 20;
|
let auditLogsPageSize = 20;
|
||||||
let auditLogsTotal = 0;
|
let auditLogsTotal = 0;
|
||||||
|
let auditLogsCache = [];
|
||||||
|
|
||||||
const AUDIT_PAGE_SIZE_KEY = 'cyberstrike_audit_page_size';
|
const AUDIT_PAGE_SIZE_KEY = 'cyberstrike_audit_page_size';
|
||||||
|
|
||||||
@@ -52,24 +53,113 @@ function auditActionLabel(action) {
|
|||||||
return auditT('settingsAudit.act.' + action, null, action);
|
return auditT('settingsAudit.act.' + action, null, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stored DB messages that share category+action but need distinct i18n keys. */
|
||||||
|
const AUDIT_MSG_BY_STORED_TEXT = {
|
||||||
|
'登录失败:密码错误': 'settingsAudit.msg.auth.login_failed',
|
||||||
|
'修改密码失败:当前密码不正确': 'settingsAudit.msg.auth.change_password_failed',
|
||||||
|
'应用配置失败:初始化知识库': 'settingsAudit.msg.config.apply_fail_kb_init',
|
||||||
|
'应用配置失败:重新初始化知识库': 'settingsAudit.msg.config.apply_fail_kb_reinit',
|
||||||
|
'应用配置失败:C2': 'settingsAudit.msg.config.apply_fail_c2'
|
||||||
|
};
|
||||||
|
|
||||||
|
function auditMessageLabel(log) {
|
||||||
|
if (!log) return '';
|
||||||
|
const raw = (log.message || '').trim();
|
||||||
|
if (raw && AUDIT_MSG_BY_STORED_TEXT[raw]) {
|
||||||
|
return auditT(AUDIT_MSG_BY_STORED_TEXT[raw], null, raw);
|
||||||
|
}
|
||||||
|
const cat = (log.category || '').trim();
|
||||||
|
const act = (log.action || '').trim();
|
||||||
|
const res = (log.result || '').trim();
|
||||||
|
if (cat && act) {
|
||||||
|
if (cat === 'auth' && act === 'login' && res === 'failure') {
|
||||||
|
return auditT('settingsAudit.msg.auth.login_failed', null, raw);
|
||||||
|
}
|
||||||
|
if (cat === 'auth' && act === 'change_password' && res === 'failure') {
|
||||||
|
return auditT('settingsAudit.msg.auth.change_password_failed', null, raw);
|
||||||
|
}
|
||||||
|
const key = 'settingsAudit.msg.' + cat + '.' + act;
|
||||||
|
const translated = auditT(key, null, null);
|
||||||
|
if (translated && translated !== key) return translated;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditResultLabel(result) {
|
||||||
|
if (!result) return '';
|
||||||
|
return auditT('settingsAudit.result.' + result, null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditLocale() {
|
||||||
|
if (typeof window.__locale === 'string' && window.__locale.length) {
|
||||||
|
return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US';
|
||||||
|
}
|
||||||
|
return (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : 'en-US';
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditTimezoneShortLabel() {
|
||||||
|
try {
|
||||||
|
const parts = new Intl.DateTimeFormat(auditLocale(), { timeZoneName: 'short' }).formatToParts(new Date());
|
||||||
|
const tz = parts.find(function (p) { return p.type === 'timeZoneName'; });
|
||||||
|
return tz ? tz.value : '';
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatAuditTime(iso) {
|
function formatAuditTime(iso) {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
try {
|
try {
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
if (Number.isNaN(d.getTime())) return iso;
|
if (Number.isNaN(d.getTime())) return iso;
|
||||||
return d.toLocaleString();
|
return d.toLocaleString(auditLocale(), {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
timeZoneName: 'short'
|
||||||
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return iso;
|
return iso;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read stored local datetime (YYYY-MM-DDTHH:mm) from custom picker or raw input. */
|
||||||
|
function getAuditFilterDatetimeValue(inputId) {
|
||||||
|
if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.getValue === 'function') {
|
||||||
|
return window.AuditDatetimePicker.getValue(inputId) || '';
|
||||||
|
}
|
||||||
|
var el = document.getElementById(inputId);
|
||||||
|
return el ? (el.value || '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** datetime-local / picker storage -> UTC RFC3339 for API. */
|
||||||
function auditDatetimeLocalToRFC3339(value) {
|
function auditDatetimeLocalToRFC3339(value) {
|
||||||
if (!value || !value.trim()) return '';
|
if (!value || !value.trim()) return '';
|
||||||
const d = new Date(value);
|
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value.trim());
|
||||||
|
if (!m) return '';
|
||||||
|
const d = new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0, 0);
|
||||||
if (Number.isNaN(d.getTime())) return '';
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
return d.toISOString();
|
return d.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAuditTimezoneHint() {
|
||||||
|
const el = document.getElementById('audit-filter-timezone-hint');
|
||||||
|
if (!el) return;
|
||||||
|
const tz = auditTimezoneShortLabel();
|
||||||
|
if (!tz) {
|
||||||
|
el.hidden = true;
|
||||||
|
el.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.hidden = false;
|
||||||
|
el.textContent = auditT('settingsAudit.filterTimeZone', { tz: tz },
|
||||||
|
'时区:' + tz + '(筛选按浏览器本地时间,API 使用 UTC)');
|
||||||
|
}
|
||||||
|
|
||||||
function initAuditPageSizeFromStorage() {
|
function initAuditPageSizeFromStorage() {
|
||||||
try {
|
try {
|
||||||
const saved = parseInt(localStorage.getItem(AUDIT_PAGE_SIZE_KEY), 10);
|
const saved = parseInt(localStorage.getItem(AUDIT_PAGE_SIZE_KEY), 10);
|
||||||
@@ -113,6 +203,7 @@ function rebuildAuditActionSelect() {
|
|||||||
actEl.disabled = true;
|
actEl.disabled = true;
|
||||||
actEl.value = '';
|
actEl.value = '';
|
||||||
actEl.title = hint;
|
actEl.title = hint;
|
||||||
|
syncAuditCustomSelect('audit-filter-action');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +220,7 @@ function rebuildAuditActionSelect() {
|
|||||||
if (prev && Array.prototype.some.call(actEl.options, function (o) { return o.value === prev; })) {
|
if (prev && Array.prototype.some.call(actEl.options, function (o) { return o.value === prev; })) {
|
||||||
actEl.value = prev;
|
actEl.value = prev;
|
||||||
}
|
}
|
||||||
|
syncAuditCustomSelect('audit-filter-action');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuditCategoryFilterChange() {
|
function onAuditCategoryFilterChange() {
|
||||||
@@ -145,43 +237,17 @@ function buildAuditQueryParams(forExport) {
|
|||||||
const act = document.getElementById('audit-filter-action');
|
const act = document.getElementById('audit-filter-action');
|
||||||
const res = document.getElementById('audit-filter-result');
|
const res = document.getElementById('audit-filter-result');
|
||||||
const q = document.getElementById('audit-filter-q');
|
const q = document.getElementById('audit-filter-q');
|
||||||
const since = document.getElementById('audit-filter-since');
|
|
||||||
const until = document.getElementById('audit-filter-until');
|
|
||||||
if (cat && cat.value) params.set('category', cat.value);
|
if (cat && cat.value) params.set('category', cat.value);
|
||||||
if (act && !act.disabled && act.value) params.set('action', act.value);
|
if (act && !act.disabled && act.value) params.set('action', act.value);
|
||||||
if (res && res.value) params.set('result', res.value);
|
if (res && res.value) params.set('result', res.value);
|
||||||
if (q && q.value.trim()) params.set('q', q.value.trim());
|
if (q && q.value.trim()) params.set('q', q.value.trim());
|
||||||
const sinceISO = since ? auditDatetimeLocalToRFC3339(since.value) : '';
|
const sinceISO = auditDatetimeLocalToRFC3339(getAuditFilterDatetimeValue('audit-filter-since'));
|
||||||
const untilISO = until ? auditDatetimeLocalToRFC3339(until.value) : '';
|
const untilISO = auditDatetimeLocalToRFC3339(getAuditFilterDatetimeValue('audit-filter-until'));
|
||||||
if (sinceISO) params.set('since', sinceISO);
|
if (sinceISO) params.set('since', sinceISO);
|
||||||
if (untilISO) params.set('until', untilISO);
|
if (untilISO) params.set('until', untilISO);
|
||||||
return params.toString();
|
return params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAuditMeta() {
|
|
||||||
if (typeof apiFetch !== 'function') return;
|
|
||||||
const hint = document.getElementById('audit-retention-hint');
|
|
||||||
try {
|
|
||||||
const r = await apiFetch('/api/audit/meta');
|
|
||||||
if (!r.ok) return;
|
|
||||||
const data = await r.json();
|
|
||||||
if (!hint) return;
|
|
||||||
if (!data.enabled) {
|
|
||||||
hint.hidden = false;
|
|
||||||
hint.textContent = auditT('settingsAudit.disabledHint', null, '审计功能已关闭,新操作不会写入审计表。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const days = data.retention_days;
|
|
||||||
if (days > 0) {
|
|
||||||
hint.hidden = false;
|
|
||||||
hint.textContent = auditT('settingsAudit.retentionHint', { days: days },
|
|
||||||
'审计记录保留 ' + days + ' 天,超期自动清理。');
|
|
||||||
} else {
|
|
||||||
hint.hidden = true;
|
|
||||||
}
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAuditSummary() {
|
async function loadAuditSummary() {
|
||||||
if (typeof apiFetch !== 'function') return;
|
if (typeof apiFetch !== 'function') return;
|
||||||
const wrap = document.getElementById('audit-summary-stats');
|
const wrap = document.getElementById('audit-summary-stats');
|
||||||
@@ -191,10 +257,14 @@ async function loadAuditSummary() {
|
|||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (wrap) wrap.hidden = false;
|
if (wrap) wrap.hidden = false;
|
||||||
const elTotal = document.getElementById('audit-stat-total');
|
const elTotal = document.getElementById('audit-stat-total');
|
||||||
|
const elSuccess = document.getElementById('audit-stat-success');
|
||||||
const elFail = document.getElementById('audit-stat-failures');
|
const elFail = document.getElementById('audit-stat-failures');
|
||||||
const elRecent = document.getElementById('audit-stat-recent');
|
const elRecent = document.getElementById('audit-stat-recent');
|
||||||
if (elTotal) elTotal.textContent = String(data.total != null ? data.total : 0);
|
const total = data.total != null ? data.total : 0;
|
||||||
if (elFail) elFail.textContent = String(data.failures != null ? data.failures : 0);
|
const failures = data.failures != null ? data.failures : 0;
|
||||||
|
if (elTotal) elTotal.textContent = String(total);
|
||||||
|
if (elSuccess) elSuccess.textContent = String(Math.max(0, total - failures));
|
||||||
|
if (elFail) elFail.textContent = String(failures);
|
||||||
if (elRecent) elRecent.textContent = String(data.recent_7d != null ? data.recent_7d : 0);
|
if (elRecent) elRecent.textContent = String(data.recent_7d != null ? data.recent_7d : 0);
|
||||||
} catch (_) { /* ignore */ }
|
} catch (_) { /* ignore */ }
|
||||||
}
|
}
|
||||||
@@ -214,7 +284,8 @@ async function loadAuditLogs(page) {
|
|||||||
throw new Error(err.error || r.statusText);
|
throw new Error(err.error || r.statusText);
|
||||||
}
|
}
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
renderAuditLogs(data.logs || []);
|
auditLogsCache = data.logs || [];
|
||||||
|
renderAuditLogs(auditLogsCache);
|
||||||
auditLogsTotal = typeof data.total === 'number' ? data.total : 0;
|
auditLogsTotal = typeof data.total === 'number' ? data.total : 0;
|
||||||
const maxPage = Math.max(1, Math.ceil(auditLogsTotal / auditLogsPageSize));
|
const maxPage = Math.max(1, Math.ceil(auditLogsTotal / auditLogsPageSize));
|
||||||
if (auditLogsPage > maxPage) {
|
if (auditLogsPage > maxPage) {
|
||||||
@@ -234,37 +305,57 @@ async function loadAuditLogs(page) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function auditResultTagClass(result) {
|
||||||
|
return result === 'failure' ? 'audit-tag--fail' : 'audit-tag--ok';
|
||||||
|
}
|
||||||
|
|
||||||
function renderAuditLogs(logs) {
|
function renderAuditLogs(logs) {
|
||||||
const listEl = document.getElementById('audit-log-list');
|
const listEl = document.getElementById('audit-log-list');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
||||||
if (!logs.length) {
|
if (!logs.length) {
|
||||||
listEl.innerHTML = '<div class="c2-empty">' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '</div>';
|
listEl.innerHTML = '<div class="audit-log-empty">' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
listEl.innerHTML = logs.map(function (log) {
|
const dash = '<span class="audit-log-cell-muted">—</span>';
|
||||||
const lvl = log.result === 'failure' ? 'warn' : (log.level || 'info');
|
const head = (
|
||||||
|
'<div class="audit-log-table-wrap">' +
|
||||||
|
'<table class="audit-log-table">' +
|
||||||
|
'<thead><tr>' +
|
||||||
|
'<th data-i18n="settingsAudit.colTime">时间</th>' +
|
||||||
|
'<th data-i18n="settingsAudit.colMessage">说明</th>' +
|
||||||
|
'<th data-i18n="settingsAudit.colCategory">类别</th>' +
|
||||||
|
'<th data-i18n="settingsAudit.colAction">操作</th>' +
|
||||||
|
'<th data-i18n="settingsAudit.colResult">结果</th>' +
|
||||||
|
'<th data-i18n="settingsAudit.colIp">IP</th>' +
|
||||||
|
'<th data-i18n="settingsAudit.colResource">资源 ID</th>' +
|
||||||
|
'</tr></thead><tbody>'
|
||||||
|
);
|
||||||
|
const rows = logs.map(function (log) {
|
||||||
const catLabel = esc(auditCategoryLabel(log.category || ''));
|
const catLabel = esc(auditCategoryLabel(log.category || ''));
|
||||||
const actionLabel = esc(auditActionLabel(log.action || ''));
|
const actionLabel = esc(auditActionLabel(log.action || ''));
|
||||||
const msg = esc(log.message || '');
|
const msg = esc(auditMessageLabel(log));
|
||||||
const ip = esc(log.clientIp || '');
|
const ip = esc(log.clientIp || '');
|
||||||
const when = esc(formatAuditTime(log.createdAt));
|
const when = esc(formatAuditTime(log.createdAt));
|
||||||
const res = esc(log.result || '');
|
const res = esc(auditResultLabel(log.result || ''));
|
||||||
const rid = log.resourceId || '';
|
const rid = log.resourceId ? esc(log.resourceId) : '';
|
||||||
const meta = rid ? (' · ' + esc(rid)) : '';
|
|
||||||
const eid = esc(log.id || '');
|
const eid = esc(log.id || '');
|
||||||
|
const resultCls = auditResultTagClass(log.result || '');
|
||||||
|
const rowClick = 'onclick="showAuditLogDetail(\'' + eid + '\')" ' +
|
||||||
|
'onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();showAuditLogDetail(\'' + eid + '\')}"';
|
||||||
return (
|
return (
|
||||||
'<div class="c2-event-item audit-log-item" role="button" tabindex="0" ' +
|
'<tr class="audit-log-row" role="button" tabindex="0" ' + rowClick + '>' +
|
||||||
'onclick="showAuditLogDetail(\'' + eid + '\')" ' +
|
'<td class="audit-log-col-time">' + when + '</td>' +
|
||||||
'onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();showAuditLogDetail(\'' + eid + '\')}">' +
|
'<td class="audit-log-col-msg" title="' + msg + '">' + (msg || dash) + '</td>' +
|
||||||
'<div class="c2-event-level ' + esc(lvl) + '"></div>' +
|
'<td>' + (catLabel ? '<span class="audit-tag audit-tag--cat">' + catLabel + '</span>' : dash) + '</td>' +
|
||||||
'<div class="c2-event-content">' +
|
'<td>' + (actionLabel ? '<span class="audit-tag audit-tag--act">' + actionLabel + '</span>' : dash) + '</td>' +
|
||||||
'<div class="c2-event-message">' + msg + '</div>' +
|
'<td>' + (res ? '<span class="audit-tag ' + resultCls + '">' + res + '</span>' : dash) + '</td>' +
|
||||||
'<div class="c2-event-meta">' + when + ' · ' + catLabel + '/' + actionLabel + ' · ' + res + meta +
|
'<td class="audit-log-col-ip">' + (ip || dash) + '</td>' +
|
||||||
(ip ? ' · IP ' + ip : '') +
|
'<td class="audit-log-col-resource" title="' + rid + '">' + (rid || dash) + '</td>' +
|
||||||
'</div></div></div>'
|
'</tr>'
|
||||||
);
|
);
|
||||||
}).join('');
|
}).join('');
|
||||||
|
listEl.innerHTML = head + rows + '</tbody></table></div>';
|
||||||
if (typeof applyTranslations === 'function') {
|
if (typeof applyTranslations === 'function') {
|
||||||
applyTranslations(listEl);
|
applyTranslations(listEl);
|
||||||
}
|
}
|
||||||
@@ -326,17 +417,58 @@ function resetAuditLogFilters() {
|
|||||||
const act = document.getElementById('audit-filter-action');
|
const act = document.getElementById('audit-filter-action');
|
||||||
const res = document.getElementById('audit-filter-result');
|
const res = document.getElementById('audit-filter-result');
|
||||||
const q = document.getElementById('audit-filter-q');
|
const q = document.getElementById('audit-filter-q');
|
||||||
const since = document.getElementById('audit-filter-since');
|
|
||||||
const until = document.getElementById('audit-filter-until');
|
|
||||||
if (cat) cat.value = '';
|
if (cat) cat.value = '';
|
||||||
if (res) res.value = '';
|
if (res) res.value = '';
|
||||||
if (q) q.value = '';
|
if (q) q.value = '';
|
||||||
if (since) since.value = '';
|
if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.clearAll === 'function') {
|
||||||
if (until) until.value = '';
|
window.AuditDatetimePicker.clearAll();
|
||||||
|
}
|
||||||
rebuildAuditActionSelect();
|
rebuildAuditActionSelect();
|
||||||
|
syncAuditCustomSelect('audit-filter-category');
|
||||||
|
syncAuditCustomSelect('audit-filter-result');
|
||||||
filterAuditLogs();
|
filterAuditLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyAuditTimePreset(preset) {
|
||||||
|
if (typeof window.AuditDatetimePicker === 'undefined') return;
|
||||||
|
const now = new Date();
|
||||||
|
let since = new Date(now.getTime());
|
||||||
|
let until = new Date(now.getTime());
|
||||||
|
switch (preset) {
|
||||||
|
case '15m':
|
||||||
|
since = new Date(now.getTime() - 15 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case '1h':
|
||||||
|
since = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case '24h':
|
||||||
|
since = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case '7d':
|
||||||
|
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case 'today':
|
||||||
|
since = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.AuditDatetimePicker.setValue('audit-filter-since', since);
|
||||||
|
window.AuditDatetimePicker.setValue('audit-filter-until', until);
|
||||||
|
filterAuditLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAuditTimePresets() {
|
||||||
|
const wrap = document.getElementById('audit-time-presets');
|
||||||
|
if (!wrap || wrap.dataset.bound === '1') return;
|
||||||
|
wrap.dataset.bound = '1';
|
||||||
|
wrap.addEventListener('click', function (ev) {
|
||||||
|
const btn = ev.target.closest('[data-preset]');
|
||||||
|
if (!btn) return;
|
||||||
|
applyAuditTimePreset(btn.getAttribute('data-preset'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 资源已被删除/移除的审计操作,不再提供「打开关联资源」 */
|
/** 资源已被删除/移除的审计操作,不再提供「打开关联资源」 */
|
||||||
const AUDIT_ACTIONS_RESOURCE_REMOVED = {
|
const AUDIT_ACTIONS_RESOURCE_REMOVED = {
|
||||||
delete: true,
|
delete: true,
|
||||||
@@ -565,8 +697,8 @@ async function showAuditLogDetail(id) {
|
|||||||
'<div class="modal-body audit-detail-body">' +
|
'<div class="modal-body audit-detail-body">' +
|
||||||
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
|
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
|
||||||
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
|
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
|
||||||
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' +
|
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(auditResultLabel(log.result || '')) + '</p>' +
|
||||||
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' +
|
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(auditMessageLabel(log)) + '</p>' +
|
||||||
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
|
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
|
||||||
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
|
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
|
||||||
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
|
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
|
||||||
@@ -597,7 +729,171 @@ async function showAuditLogDetail(id) {
|
|||||||
function initAuditLogsSection() {
|
function initAuditLogsSection() {
|
||||||
if (!document.getElementById('audit-log-list')) return;
|
if (!document.getElementById('audit-log-list')) return;
|
||||||
initAuditPageSizeFromStorage();
|
initAuditPageSizeFromStorage();
|
||||||
|
initAuditFilterSelects();
|
||||||
rebuildAuditActionSelect();
|
rebuildAuditActionSelect();
|
||||||
loadAuditMeta();
|
if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.init === 'function') {
|
||||||
|
window.AuditDatetimePicker.init();
|
||||||
|
}
|
||||||
|
initAuditTimePresets();
|
||||||
|
updateAuditTimezoneHint();
|
||||||
loadAuditLogs(1);
|
loadAuditLogs(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshAuditFilterI18n() {
|
||||||
|
const section = document.getElementById('settings-section-audit');
|
||||||
|
if (section && typeof applyTranslations === 'function') {
|
||||||
|
applyTranslations(section);
|
||||||
|
}
|
||||||
|
rebuildAuditActionSelect();
|
||||||
|
syncAuditCustomSelect('audit-filter-category');
|
||||||
|
syncAuditCustomSelect('audit-filter-action');
|
||||||
|
syncAuditCustomSelect('audit-filter-result');
|
||||||
|
updateAuditTimezoneHint();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuditLogsI18n() {
|
||||||
|
if (!document.getElementById('audit-log-list')) return;
|
||||||
|
refreshAuditFilterI18n();
|
||||||
|
if (auditLogsCache.length) {
|
||||||
|
renderAuditLogs(auditLogsCache);
|
||||||
|
renderAuditLogsPagination();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('languagechange', function () {
|
||||||
|
try {
|
||||||
|
refreshAuditLogsI18n();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('languagechange audit refresh failed', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var auditCustomSelectMap = {};
|
||||||
|
var auditFilterSelectsDocListener = false;
|
||||||
|
|
||||||
|
function closeAllAuditCustomSelects() {
|
||||||
|
Object.keys(auditCustomSelectMap).forEach(function (id) {
|
||||||
|
auditCustomSelectMap[id].wrapper.classList.remove('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAuditCustomSelect(selectId) {
|
||||||
|
var reg = auditCustomSelectMap[selectId];
|
||||||
|
if (!reg) return;
|
||||||
|
var select = reg.select;
|
||||||
|
var dropdown = reg.dropdown;
|
||||||
|
var trigger = reg.trigger;
|
||||||
|
var wrapper = reg.wrapper;
|
||||||
|
var valueSpan = trigger.querySelector('.audit-custom-select-value');
|
||||||
|
|
||||||
|
dropdown.innerHTML = '';
|
||||||
|
Array.prototype.forEach.call(select.options, function (opt) {
|
||||||
|
var item = document.createElement('div');
|
||||||
|
item.className = 'audit-custom-select-option';
|
||||||
|
item.setAttribute('role', 'option');
|
||||||
|
item.setAttribute('data-value', opt.value);
|
||||||
|
if (opt.value === select.value) {
|
||||||
|
item.classList.add('is-selected');
|
||||||
|
item.setAttribute('aria-selected', 'true');
|
||||||
|
}
|
||||||
|
var check = document.createElement('span');
|
||||||
|
check.className = 'audit-custom-select-check';
|
||||||
|
check.setAttribute('aria-hidden', 'true');
|
||||||
|
check.textContent = '✓';
|
||||||
|
var label = document.createElement('span');
|
||||||
|
label.className = 'audit-custom-select-label';
|
||||||
|
label.textContent = opt.textContent;
|
||||||
|
item.appendChild(check);
|
||||||
|
item.appendChild(label);
|
||||||
|
dropdown.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
var selectedOpt = select.options[select.selectedIndex];
|
||||||
|
if (valueSpan) {
|
||||||
|
valueSpan.textContent = selectedOpt ? selectedOpt.textContent : '';
|
||||||
|
}
|
||||||
|
trigger.disabled = !!select.disabled;
|
||||||
|
wrapper.classList.toggle('is-disabled', !!select.disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceAuditFilterSelect(selectId) {
|
||||||
|
var select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
if (select.dataset.auditCustom === '1') {
|
||||||
|
syncAuditCustomSelect(selectId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.dataset.auditCustom = '1';
|
||||||
|
select.classList.add('audit-native-select');
|
||||||
|
select.tabIndex = -1;
|
||||||
|
select.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
var wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'audit-custom-select';
|
||||||
|
|
||||||
|
var trigger = document.createElement('button');
|
||||||
|
trigger.type = 'button';
|
||||||
|
trigger.className = 'audit-custom-select-trigger';
|
||||||
|
trigger.setAttribute('aria-haspopup', 'listbox');
|
||||||
|
var valueSpan = document.createElement('span');
|
||||||
|
valueSpan.className = 'audit-custom-select-value';
|
||||||
|
trigger.appendChild(valueSpan);
|
||||||
|
var caret = document.createElement('span');
|
||||||
|
caret.className = 'audit-custom-select-caret';
|
||||||
|
caret.setAttribute('aria-hidden', 'true');
|
||||||
|
caret.textContent = '▾';
|
||||||
|
trigger.appendChild(caret);
|
||||||
|
|
||||||
|
var dropdown = document.createElement('div');
|
||||||
|
dropdown.className = 'audit-custom-select-dropdown';
|
||||||
|
dropdown.setAttribute('role', 'listbox');
|
||||||
|
|
||||||
|
var parent = select.parentNode;
|
||||||
|
parent.insertBefore(wrapper, select);
|
||||||
|
wrapper.appendChild(trigger);
|
||||||
|
wrapper.appendChild(dropdown);
|
||||||
|
wrapper.appendChild(select);
|
||||||
|
|
||||||
|
auditCustomSelectMap[selectId] = {
|
||||||
|
wrapper: wrapper,
|
||||||
|
trigger: trigger,
|
||||||
|
dropdown: dropdown,
|
||||||
|
select: select
|
||||||
|
};
|
||||||
|
|
||||||
|
trigger.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (select.disabled) return;
|
||||||
|
var open = wrapper.classList.contains('open');
|
||||||
|
closeAllAuditCustomSelects();
|
||||||
|
if (!open) wrapper.classList.add('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.addEventListener('click', function (e) {
|
||||||
|
var opt = e.target.closest('.audit-custom-select-option');
|
||||||
|
if (!opt) return;
|
||||||
|
var val = opt.getAttribute('data-value');
|
||||||
|
if (val === null) val = '';
|
||||||
|
if (select.value !== val) {
|
||||||
|
select.value = val;
|
||||||
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
wrapper.classList.remove('open');
|
||||||
|
syncAuditCustomSelect(selectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
syncAuditCustomSelect(selectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAuditFilterSelects() {
|
||||||
|
if (!document.getElementById('audit-filter-category')) return;
|
||||||
|
if (!auditFilterSelectsDocListener) {
|
||||||
|
document.addEventListener('click', function () {
|
||||||
|
closeAllAuditCustomSelects();
|
||||||
|
});
|
||||||
|
auditFilterSelectsDocListener = true;
|
||||||
|
}
|
||||||
|
enhanceAuditFilterSelect('audit-filter-category');
|
||||||
|
enhanceAuditFilterSelect('audit-filter-action');
|
||||||
|
enhanceAuditFilterSelect('audit-filter-result');
|
||||||
|
}
|
||||||
|
|||||||
+119
-7
@@ -2164,6 +2164,97 @@ function showCopySuccess(button) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Claude extended thinking 内部尾缀(与后端 DisplayReasoningContent 一致,UI 不展示) */
|
||||||
|
const CLAUDE_REASONING_UI_SUFFIX = '\n---CSAI_CLAUDE_THINKING_BLOCKS---\n';
|
||||||
|
|
||||||
|
function normalizeReasoningContentForDisplay(text) {
|
||||||
|
if (text == null) return '';
|
||||||
|
let s = String(text).trim();
|
||||||
|
if (!s) return '';
|
||||||
|
const idx = s.lastIndexOf(CLAUDE_REASONING_UI_SUFFIX);
|
||||||
|
if (idx >= 0) {
|
||||||
|
s = s.slice(0, idx).trim();
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMessageReasoningContent(messageIdOrEl, reasoningContent) {
|
||||||
|
const el = typeof messageIdOrEl === 'string' ? document.getElementById(messageIdOrEl) : messageIdOrEl;
|
||||||
|
if (!el || !el.dataset) return;
|
||||||
|
const rc = normalizeReasoningContentForDisplay(reasoningContent);
|
||||||
|
if (rc) {
|
||||||
|
el.dataset.reasoningContent = rc;
|
||||||
|
} else {
|
||||||
|
delete el.dataset.reasoningContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageReasoningContent(messageIdOrEl) {
|
||||||
|
const el = typeof messageIdOrEl === 'string' ? document.getElementById(messageIdOrEl) : messageIdOrEl;
|
||||||
|
if (!el || !el.dataset) return '';
|
||||||
|
return normalizeReasoningContentForDisplay(el.dataset.reasoningContent || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasoningTextAlreadyInProcessDetails(processDetails, rc) {
|
||||||
|
if (!rc) return true;
|
||||||
|
const list = Array.isArray(processDetails) ? processDetails : [];
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const d = list[i];
|
||||||
|
if (!d) continue;
|
||||||
|
const et = d.eventType || '';
|
||||||
|
if (et !== 'reasoning_chain' && et !== 'thinking') continue;
|
||||||
|
const msg = normalizeReasoningContentForDisplay(d.message || '');
|
||||||
|
if (!msg) continue;
|
||||||
|
if (msg === rc || msg.includes(rc) || rc.includes(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 合并 messages.reasoningContent 与 process_details 中的 reasoning_chain,两者都读、都展示(去重后) */
|
||||||
|
function mergeMessageReasoningContentIntoProcessDetails(processDetails, reasoningContent) {
|
||||||
|
const rc = normalizeReasoningContentForDisplay(reasoningContent);
|
||||||
|
const details = Array.isArray(processDetails) ? processDetails.slice() : [];
|
||||||
|
if (!rc || reasoningTextAlreadyInProcessDetails(details, rc)) {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
eventType: 'reasoning_chain',
|
||||||
|
message: rc,
|
||||||
|
data: { source: 'message.reasoningContent' }
|
||||||
|
});
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAssistantReasoningContentFromServer(backendMessageId, domAssistantId) {
|
||||||
|
if (!backendMessageId || !domAssistantId || !currentConversationId || typeof apiFetch !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const convRes = await apiFetch(`/api/conversations/${encodeURIComponent(currentConversationId)}?include_process_details=0`);
|
||||||
|
const conv = await convRes.json().catch(() => ({}));
|
||||||
|
if (!convRes.ok || !Array.isArray(conv.messages)) return;
|
||||||
|
const msg = conv.messages.find((m) => m && String(m.id) === String(backendMessageId));
|
||||||
|
if (!msg || !msg.reasoningContent) return;
|
||||||
|
setMessageReasoningContent(domAssistantId, msg.reasoningContent);
|
||||||
|
const pdRes = await apiFetch(`/api/messages/${encodeURIComponent(String(backendMessageId))}/process-details`);
|
||||||
|
const pdJson = await pdRes.json().catch(() => ({}));
|
||||||
|
const details = pdRes.ok && Array.isArray(pdJson.processDetails) ? pdJson.processDetails : [];
|
||||||
|
if (typeof renderProcessDetails === 'function') {
|
||||||
|
renderProcessDetails(domAssistantId, details);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('syncAssistantReasoningContentFromServer failed', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.normalizeReasoningContentForDisplay = normalizeReasoningContentForDisplay;
|
||||||
|
window.setMessageReasoningContent = setMessageReasoningContent;
|
||||||
|
window.getMessageReasoningContent = getMessageReasoningContent;
|
||||||
|
window.mergeMessageReasoningContentIntoProcessDetails = mergeMessageReasoningContentIntoProcessDetails;
|
||||||
|
window.syncAssistantReasoningContentFromServer = syncAssistantReasoningContentFromServer;
|
||||||
|
|
||||||
/** 相邻且类型/正文/data 完全一致的过程详情只保留一条(与后端去重一致,避免时间线叠多条相同块) */
|
/** 相邻且类型/正文/data 完全一致的过程详情只保留一条(与后端去重一致,避免时间线叠多条相同块) */
|
||||||
function dedupeConsecutiveProcessDetailRows(details) {
|
function dedupeConsecutiveProcessDetailRows(details) {
|
||||||
if (!Array.isArray(details) || details.length < 2) {
|
if (!Array.isArray(details) || details.length < 2) {
|
||||||
@@ -2282,20 +2373,27 @@ function renderProcessDetails(messageId, processDetails) {
|
|||||||
detailsContainer.appendChild(contentDiv);
|
detailsContainer.appendChild(contentDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// processDetails === null 表示“尚未加载(懒加载)”
|
// processDetails === null 表示“尚未加载(懒加载)”;messages.reasoningContent 可先展示
|
||||||
const isLazyNotLoaded = (processDetails === null);
|
const isLazyNotLoaded = (processDetails === null);
|
||||||
if (isLazyNotLoaded) {
|
const reasoningFromMessage = getMessageReasoningContent(messageElement);
|
||||||
|
if (isLazyNotLoaded && !reasoningFromMessage) {
|
||||||
detailsContainer.dataset.lazyNotLoaded = '1';
|
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||||
detailsContainer.dataset.loaded = '0';
|
detailsContainer.dataset.loaded = '0';
|
||||||
timeline.innerHTML = '<div class="progress-timeline-empty">' +
|
timeline.innerHTML = '<div class="progress-timeline-empty">' +
|
||||||
(typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
|
(typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
|
||||||
'(点击后加载)</div>';
|
'(点击后加载)</div>';
|
||||||
// 默认折叠
|
|
||||||
timeline.classList.remove('expanded');
|
timeline.classList.remove('expanded');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
detailsContainer.dataset.lazyNotLoaded = '0';
|
if (isLazyNotLoaded) {
|
||||||
detailsContainer.dataset.loaded = '1';
|
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||||
|
detailsContainer.dataset.loaded = '0';
|
||||||
|
processDetails = [];
|
||||||
|
} else {
|
||||||
|
detailsContainer.dataset.lazyNotLoaded = '0';
|
||||||
|
detailsContainer.dataset.loaded = '1';
|
||||||
|
}
|
||||||
|
processDetails = mergeMessageReasoningContentIntoProcessDetails(processDetails, reasoningFromMessage);
|
||||||
processDetails = dedupeConsecutiveProcessDetailRows(processDetails);
|
processDetails = dedupeConsecutiveProcessDetailRows(processDetails);
|
||||||
if (typeof window.coalesceProcessDetailsToolPairs === 'function') {
|
if (typeof window.coalesceProcessDetailsToolPairs === 'function') {
|
||||||
processDetails = window.coalesceProcessDetailsToolPairs(processDetails);
|
processDetails = window.coalesceProcessDetailsToolPairs(processDetails);
|
||||||
@@ -2427,6 +2525,14 @@ function renderProcessDetails(messageId, processDetails) {
|
|||||||
addTimelineItem(timeline, eventType, timelineOpts);
|
addTimelineItem(timeline, eventType, timelineOpts);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isLazyNotLoaded && reasoningFromMessage) {
|
||||||
|
const lazyHint = document.createElement('div');
|
||||||
|
lazyHint.className = 'progress-timeline-empty progress-timeline-lazy-hint';
|
||||||
|
lazyHint.textContent = (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
|
||||||
|
'(点击后加载完整过程详情)';
|
||||||
|
timeline.appendChild(lazyHint);
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否有错误或取消事件,如果有,确保详情默认折叠(但仍有待审批 HITL 时保持展开,由 restoreHitlInlineForConversation 处理)
|
// 检查是否有错误或取消事件,如果有,确保详情默认折叠(但仍有待审批 HITL 时保持展开,由 restoreHitlInlineForConversation 处理)
|
||||||
const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt');
|
const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt');
|
||||||
const hasErrorOrCancelled = processDetails.some(d =>
|
const hasErrorOrCancelled = processDetails.some(d =>
|
||||||
@@ -3193,6 +3299,9 @@ async function loadConversation(conversationId) {
|
|||||||
attachDeleteTurnButton(messageEl);
|
attachDeleteTurnButton(messageEl);
|
||||||
}
|
}
|
||||||
if (msg.role === 'assistant') {
|
if (msg.role === 'assistant') {
|
||||||
|
if (messageEl && msg.reasoningContent) {
|
||||||
|
setMessageReasoningContent(messageEl, msg.reasoningContent);
|
||||||
|
}
|
||||||
const hasField = msg && Object.prototype.hasOwnProperty.call(msg, 'processDetails');
|
const hasField = msg && Object.prototype.hasOwnProperty.call(msg, 'processDetails');
|
||||||
renderProcessDetails(messageId, hasField ? (msg.processDetails || []) : null);
|
renderProcessDetails(messageId, hasField ? (msg.processDetails || []) : null);
|
||||||
if (msg.processDetails && msg.processDetails.length > 0) {
|
if (msg.processDetails && msg.processDetails.length > 0) {
|
||||||
@@ -7359,8 +7468,11 @@ async function deleteSelectedConversations() {
|
|||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
await deleteConversation(id, true); // 跳过内部确认,因为批量删除时已经确认过了
|
await deleteConversation(id, true); // 跳过内部确认,因为批量删除时已经确认过了
|
||||||
}
|
}
|
||||||
closeBatchManageModal();
|
// 删除后保持弹窗打开,便于继续管理剩余对话
|
||||||
loadConversationsWithGroups();
|
const selectAll = document.getElementById('batch-select-all');
|
||||||
|
if (selectAll) {
|
||||||
|
selectAll.checked = false;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除失败:', error);
|
console.error('删除失败:', error);
|
||||||
const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.deleteFailed') : '删除失败';
|
const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.deleteFailed') : '删除失败';
|
||||||
|
|||||||
@@ -172,6 +172,59 @@ function einoMainStreamPlanningTitle(responseData) {
|
|||||||
return prefix + '📝 ' + plan;
|
return prefix + '📝 ' + plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主通道 response 结束时:将流式占位条目固化为 planning(与后端 flushResponsePlan 落库类型一致),
|
||||||
|
* 避免 integrateProgressToMCPSection 快照前删除占位导致「助手输出」仅刷新后才出现。
|
||||||
|
*/
|
||||||
|
function finalizeMainResponseStreamItem(streamState, finalMessage, responseData) {
|
||||||
|
if (!streamState || !streamState.itemId) return false;
|
||||||
|
const item = document.getElementById(streamState.itemId);
|
||||||
|
if (!item || !item.parentNode) return false;
|
||||||
|
|
||||||
|
const fullText = (finalMessage != null && String(finalMessage).trim() !== '')
|
||||||
|
? String(finalMessage)
|
||||||
|
: (streamState.buffer || '');
|
||||||
|
if (!String(fullText).trim()) {
|
||||||
|
item.parentNode.removeChild(item);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = Object.assign({}, streamState.streamMeta || {}, responseData || {});
|
||||||
|
|
||||||
|
item.classList.remove('timeline-item-thinking');
|
||||||
|
item.classList.add('timeline-item-planning');
|
||||||
|
item.dataset.timelineType = 'planning';
|
||||||
|
delete item.dataset.responseStreamPlaceholder;
|
||||||
|
if (meta.orchestration != null && String(meta.orchestration).trim() !== '') {
|
||||||
|
item.dataset.orchestration = String(meta.orchestration).trim();
|
||||||
|
}
|
||||||
|
if (meta.einoAgent != null && String(meta.einoAgent).trim() !== '') {
|
||||||
|
item.dataset.einoAgent = String(meta.einoAgent).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleEl = item.querySelector('.timeline-item-title');
|
||||||
|
if (titleEl && typeof einoMainStreamPlanningTitle === 'function') {
|
||||||
|
titleEl.textContent = einoMainStreamPlanningTitle(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentEl = item.querySelector('.timeline-item-content');
|
||||||
|
if (!contentEl) {
|
||||||
|
contentEl = document.createElement('div');
|
||||||
|
contentEl.className = 'timeline-item-content';
|
||||||
|
item.appendChild(contentEl);
|
||||||
|
}
|
||||||
|
flushStreamPlainTextUpdate(contentEl);
|
||||||
|
const body = typeof formatTimelineStreamBody === 'function'
|
||||||
|
? formatTimelineStreamBody(fullText, meta)
|
||||||
|
: fullText;
|
||||||
|
if (typeof formatMarkdown === 'function') {
|
||||||
|
setTimelineItemContentStreamRich(contentEl, formatMarkdown(body, timelineMarkdownOpts));
|
||||||
|
} else {
|
||||||
|
setTimelineItemContentStreamPlain(contentEl, body);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function translateProgressMessage(message, data) {
|
function translateProgressMessage(message, data) {
|
||||||
if (!message || typeof message !== 'string') return message;
|
if (!message || typeof message !== 'string') return message;
|
||||||
if (typeof window.t !== 'function') return message;
|
if (typeof window.t !== 'function') return message;
|
||||||
@@ -224,6 +277,7 @@ if (typeof window !== 'undefined') {
|
|||||||
window.translateProgressMessage = translateProgressMessage;
|
window.translateProgressMessage = translateProgressMessage;
|
||||||
window.translatePlanExecuteAgentName = translatePlanExecuteAgentName;
|
window.translatePlanExecuteAgentName = translatePlanExecuteAgentName;
|
||||||
window.einoMainStreamPlanningTitle = einoMainStreamPlanningTitle;
|
window.einoMainStreamPlanningTitle = einoMainStreamPlanningTitle;
|
||||||
|
window.finalizeMainResponseStreamItem = finalizeMainResponseStreamItem;
|
||||||
window.formatTimelineStreamBody = formatTimelineStreamBody;
|
window.formatTimelineStreamBody = formatTimelineStreamBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2401,14 +2455,18 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
updateAssistantBubbleContent(assistantIdFinal, event.message, true);
|
updateAssistantBubbleContent(assistantIdFinal, event.message, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除 response_start/response_delta 阶段创建的「规划中」占位条目。
|
// 将 response_start/response_delta 占位固化为 planning,与后端落库一致后再快照过程详情
|
||||||
// 该条目属于 UI-only 的流式展示,不应被拷贝到最终的过程详情里;
|
|
||||||
// 否则会出现“不刷新页面仍显示规划中,刷新后消失”的不一致。
|
|
||||||
if (streamState && streamState.itemId) {
|
if (streamState && streamState.itemId) {
|
||||||
const planningItem = document.getElementById(streamState.itemId);
|
finalizeMainResponseStreamItem(streamState, event.message, responseData);
|
||||||
if (planningItem && planningItem.parentNode) {
|
} else if (event.message && String(event.message).trim()) {
|
||||||
planningItem.parentNode.removeChild(planningItem);
|
addTimelineItem(timeline, 'planning', {
|
||||||
}
|
title: typeof einoMainStreamPlanningTitle === 'function'
|
||||||
|
? einoMainStreamPlanningTitle(responseData)
|
||||||
|
: ('📝 ' + (typeof window.t === 'function' ? window.t('chat.planning') : '规划中')),
|
||||||
|
message: event.message,
|
||||||
|
data: responseData,
|
||||||
|
expanded: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终回复时隐藏进度卡片(多代理模式下,迭代过程已完整展示)
|
// 最终回复时隐藏进度卡片(多代理模式下,迭代过程已完整展示)
|
||||||
@@ -2429,6 +2487,11 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
const respMid = responseData.messageId;
|
const respMid = responseData.messageId;
|
||||||
if (respMid) {
|
if (respMid) {
|
||||||
applyBackendMessageIdToAssistantDom(assistantIdFinal, respMid);
|
applyBackendMessageIdToAssistantDom(assistantIdFinal, respMid);
|
||||||
|
if (typeof window.syncAssistantReasoningContentFromServer === 'function') {
|
||||||
|
setTimeout(function () {
|
||||||
|
window.syncAssistantReasoningContentFromServer(respMid, assistantIdFinal);
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -3824,7 +3887,7 @@ function buildMcpTimelineSvg(points, rangeKey) {
|
|||||||
const tipTime = formatMcpTimelineLabel(c.p.t, rangeKey, locale);
|
const tipTime = formatMcpTimelineLabel(c.p.t, rangeKey, locale);
|
||||||
const isPeak = c.i === peakIdx && (c.p.total || 0) > 0;
|
const isPeak = c.i === peakIdx && (c.p.total || 0) > 0;
|
||||||
const dotClass = 'mcp-stats-timeline-dot' + (isPeak ? ' mcp-stats-timeline-dot--peak' : '');
|
const dotClass = 'mcp-stats-timeline-dot' + (isPeak ? ' mcp-stats-timeline-dot--peak' : '');
|
||||||
return `<circle class="${dotClass}" cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${isPeak ? 3 : 2.5}"
|
return `<circle class="${dotClass}" cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${isPeak ? 2 : 1.5}"
|
||||||
data-time="${escapeHtml(tipTime)}"
|
data-time="${escapeHtml(tipTime)}"
|
||||||
data-total="${c.p.total || 0}"
|
data-total="${c.p.total || 0}"
|
||||||
data-failed="${c.p.failed || 0}" />`;
|
data-failed="${c.p.failed || 0}" />`;
|
||||||
@@ -3832,7 +3895,7 @@ function buildMcpTimelineSvg(points, rangeKey) {
|
|||||||
|
|
||||||
const peakC = coords[peakIdx];
|
const peakC = coords[peakIdx];
|
||||||
const peakMarker = (peakC.p.total || 0) > 0
|
const peakMarker = (peakC.p.total || 0) > 0
|
||||||
? `<circle class="mcp-stats-timeline-peak-glow" cx="${peakC.x.toFixed(2)}" cy="${peakC.y.toFixed(2)}" r="7" />`
|
? `<circle class="mcp-stats-timeline-peak-glow" cx="${peakC.x.toFixed(2)}" cy="${peakC.y.toFixed(2)}" r="5" />`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `<svg class="mcp-stats-timeline__chart" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true">
|
return `<svg class="mcp-stats-timeline__chart" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true">
|
||||||
|
|||||||
@@ -1270,12 +1270,18 @@ async function saveProjectModal() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fromChat = !!window._projectModalFromChat;
|
const fromChat = !!window._projectModalFromChat;
|
||||||
|
const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
|
||||||
window._projectModalFromChat = false;
|
window._projectModalFromChat = false;
|
||||||
|
window._projectModalFromWebshellConnId = '';
|
||||||
closeProjectModal();
|
closeProjectModal();
|
||||||
const saved = await res.json();
|
const saved = await res.json();
|
||||||
await loadProjectsList();
|
await loadProjectsList();
|
||||||
if (saved.id) {
|
if (saved.id) {
|
||||||
if (fromChat && !editId) {
|
if (fromWebshellConnId && !editId) {
|
||||||
|
if (typeof applyWebshellAiProjectSelection === 'function') {
|
||||||
|
await applyWebshellAiProjectSelection(saved.id);
|
||||||
|
}
|
||||||
|
} else if (fromChat && !editId) {
|
||||||
await applyChatProjectSelection(saved.id);
|
await applyChatProjectSelection(saved.id);
|
||||||
} else {
|
} else {
|
||||||
await selectProject(saved.id);
|
await selectProject(saved.id);
|
||||||
|
|||||||
+263
-8
@@ -27,6 +27,9 @@ const WEBSHELL_HISTORY_MAX = 100;
|
|||||||
let webshellClearInProgress = false;
|
let webshellClearInProgress = false;
|
||||||
// AI 助手:按连接 ID 保存对话 ID,便于多轮对话
|
// AI 助手:按连接 ID 保存对话 ID,便于多轮对话
|
||||||
let webshellAiConvMap = {};
|
let webshellAiConvMap = {};
|
||||||
|
// AI 助手:项目绑定(已有对话按 convId,新对话按 connId 草稿)
|
||||||
|
let webshellAiProjectByConvId = {};
|
||||||
|
let webshellAiDraftProjectByConn = {};
|
||||||
let webshellAiSending = false;
|
let webshellAiSending = false;
|
||||||
let webshellAiAbortController = null; // AbortController for current AI stream
|
let webshellAiAbortController = null; // AbortController for current AI stream
|
||||||
let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader
|
let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader
|
||||||
@@ -266,6 +269,7 @@ function wsToggleRolePanel() {
|
|||||||
var isOpen = panel.style.display === 'flex';
|
var isOpen = panel.style.display === 'flex';
|
||||||
if (isOpen) { wsCloseRolePanel(); return; }
|
if (isOpen) { wsCloseRolePanel(); return; }
|
||||||
wsCloseAgentModePanel();
|
wsCloseAgentModePanel();
|
||||||
|
wsCloseProjectPanel();
|
||||||
panel.style.display = 'flex';
|
panel.style.display = 'flex';
|
||||||
}
|
}
|
||||||
function wsCloseRolePanel() {
|
function wsCloseRolePanel() {
|
||||||
@@ -340,6 +344,7 @@ function wsToggleAgentModePanel() {
|
|||||||
var isOpen = panel.style.display === 'flex';
|
var isOpen = panel.style.display === 'flex';
|
||||||
if (isOpen) { wsCloseAgentModePanel(); return; }
|
if (isOpen) { wsCloseAgentModePanel(); return; }
|
||||||
wsCloseRolePanel();
|
wsCloseRolePanel();
|
||||||
|
wsCloseProjectPanel();
|
||||||
panel.style.display = 'flex';
|
panel.style.display = 'flex';
|
||||||
}
|
}
|
||||||
function wsCloseAgentModePanel() {
|
function wsCloseAgentModePanel() {
|
||||||
@@ -347,10 +352,204 @@ function wsCloseAgentModePanel() {
|
|||||||
if (panel) panel.style.display = 'none';
|
if (panel) panel.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── WebShell AI 项目选择器(与主「对话」页对齐) ───
|
||||||
|
|
||||||
|
function wsProjectT(key, fallback) {
|
||||||
|
if (typeof window.t === 'function') {
|
||||||
|
var v = window.t(key);
|
||||||
|
if (v && v !== key) return v;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebshellAiConvId(conn) {
|
||||||
|
if (!conn || !conn.id) return '';
|
||||||
|
return webshellAiConvMap[conn.id] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebshellAiProjectSelection(conn) {
|
||||||
|
if (!conn || !conn.id) return '';
|
||||||
|
var convId = getWebshellAiConvId(conn);
|
||||||
|
if (convId) return webshellAiProjectByConvId[convId] || '';
|
||||||
|
return webshellAiDraftProjectByConn[conn.id] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsSetWebshellAiProject(conn, projectId) {
|
||||||
|
if (!conn || !conn.id) return;
|
||||||
|
var pid = projectId || '';
|
||||||
|
var convId = getWebshellAiConvId(conn);
|
||||||
|
if (convId) {
|
||||||
|
if (pid) webshellAiProjectByConvId[convId] = pid;
|
||||||
|
else delete webshellAiProjectByConvId[convId];
|
||||||
|
} else if (pid) {
|
||||||
|
webshellAiDraftProjectByConn[conn.id] = pid;
|
||||||
|
} else {
|
||||||
|
delete webshellAiDraftProjectByConn[conn.id];
|
||||||
|
}
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsIsActiveProjectId(id) {
|
||||||
|
if (!id) return false;
|
||||||
|
var map = window.projectNameById || {};
|
||||||
|
return !!map[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsResolveWebshellAiProjectSelection(conn) {
|
||||||
|
var raw = getWebshellAiProjectSelection(conn);
|
||||||
|
if (!raw) return '';
|
||||||
|
return wsIsActiveProjectId(raw) ? raw : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsUpdateProjectButtonLabel() {
|
||||||
|
var textEl = document.getElementById('ws-project-text');
|
||||||
|
if (!textEl || !webshellCurrentConn) return;
|
||||||
|
var id = wsResolveWebshellAiProjectSelection(webshellCurrentConn);
|
||||||
|
var nameMap = window.projectNameById || {};
|
||||||
|
textEl.textContent = id && nameMap[id] ? nameMap[id] : wsProjectT('projects.noProject', '无项目');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wsRenderProjectPanelList() {
|
||||||
|
var list = document.getElementById('ws-project-list');
|
||||||
|
if (!list || !webshellCurrentConn) return;
|
||||||
|
var conn = webshellCurrentConn;
|
||||||
|
var selected = wsResolveWebshellAiProjectSelection(conn);
|
||||||
|
var projects = [];
|
||||||
|
try {
|
||||||
|
if (typeof window.fetchAllProjects === 'function') {
|
||||||
|
projects = await window.fetchAllProjects(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
list.innerHTML = '<div class="chat-project-panel-empty">' + escapeHtml(wsProjectT('projects.loadFailedRetry', '加载失败,请重试')) + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window.rebuildProjectNameMap === 'function') {
|
||||||
|
window.rebuildProjectNameMap(projects);
|
||||||
|
}
|
||||||
|
var activeProjects = projects.filter(function (p) { return p.status !== 'archived'; });
|
||||||
|
var items = [{ id: '', name: wsProjectT('projects.noProject', '无项目'), description: wsProjectT('projects.noProjectDescription', '不绑定项目') }].concat(activeProjects);
|
||||||
|
list.innerHTML = '';
|
||||||
|
items.forEach(function (p) {
|
||||||
|
var isNone = !p.id;
|
||||||
|
var isSelected = isNone ? !selected : selected === p.id;
|
||||||
|
var desc = isNone
|
||||||
|
? (p.description || '')
|
||||||
|
: ((p.description || '').trim().slice(0, 80) || wsProjectT('projects.sharedFactBoard', '共享事实黑板'));
|
||||||
|
var btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
|
||||||
|
btn.setAttribute('role', 'option');
|
||||||
|
btn.onclick = function () { wsSelectProject(p.id || ''); };
|
||||||
|
btn.innerHTML = '<div class="role-selection-item-icon-main">' + (isNone ? '—' : '📁') + '</div>' +
|
||||||
|
'<div class="role-selection-item-content-main">' +
|
||||||
|
'<div class="role-selection-item-name-main">' + escapeHtml(p.name || '未命名') + '</div>' +
|
||||||
|
'<div class="role-selection-item-description-main">' + escapeHtml(desc) + '</div></div>' +
|
||||||
|
(isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : '');
|
||||||
|
list.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wsRenderProjectPanel() {
|
||||||
|
var list = document.getElementById('ws-project-list');
|
||||||
|
if (!list) return;
|
||||||
|
list.innerHTML = '<div class="chat-project-panel-loading">' + escapeHtml(wsProjectT('common.loading', '加载中...')) + '</div>';
|
||||||
|
await wsRenderProjectPanelList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsCloseProjectPanel() {
|
||||||
|
var panel = document.getElementById('ws-project-panel');
|
||||||
|
var btn = document.getElementById('ws-project-btn');
|
||||||
|
if (panel) panel.style.display = 'none';
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wsToggleProjectPanel() {
|
||||||
|
var panel = document.getElementById('ws-project-panel');
|
||||||
|
var btn = document.getElementById('ws-project-btn');
|
||||||
|
if (!panel) return;
|
||||||
|
var isHidden = panel.style.display === 'none' || !panel.style.display;
|
||||||
|
if (!isHidden) {
|
||||||
|
wsCloseProjectPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wsCloseRolePanel();
|
||||||
|
wsCloseAgentModePanel();
|
||||||
|
panel.style.display = 'flex';
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
btn.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
await wsRenderProjectPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wsSelectProject(projectId) {
|
||||||
|
wsCloseProjectPanel();
|
||||||
|
await applyWebshellAiProjectSelection(projectId || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyWebshellAiProjectSelection(projectId) {
|
||||||
|
var conn = webshellCurrentConn;
|
||||||
|
if (!conn || !conn.id) return;
|
||||||
|
var prev = getWebshellAiProjectSelection(conn);
|
||||||
|
if (projectId === prev) {
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var convId = getWebshellAiConvId(conn);
|
||||||
|
if (convId) {
|
||||||
|
try {
|
||||||
|
var res = await apiFetch('/api/conversations/' + encodeURIComponent(convId) + '/project', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ projectId: projectId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
var err = await res.json().catch(function () { return {}; });
|
||||||
|
throw new Error(err.error || res.statusText);
|
||||||
|
}
|
||||||
|
wsSetWebshellAiProject(conn, projectId);
|
||||||
|
if (typeof showNotification === 'function') {
|
||||||
|
showNotification(
|
||||||
|
projectId ? wsProjectT('projects.projectBound', '已绑定项目') : wsProjectT('projects.projectUnbound', '已解除项目绑定'),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(wsProjectT('projects.updateProjectBindingFailed', '更新项目绑定失败') + ': ' + (e.message || e));
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wsSetWebshellAiProject(conn, projectId);
|
||||||
|
}
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNewProjectModalFromWebshellAi() {
|
||||||
|
wsCloseProjectPanel();
|
||||||
|
if (webshellCurrentConn && webshellCurrentConn.id) {
|
||||||
|
window._projectModalFromWebshellConnId = webshellCurrentConn.id;
|
||||||
|
}
|
||||||
|
window._projectModalFromChat = false;
|
||||||
|
if (typeof showNewProjectModal === 'function') showNewProjectModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.applyWebshellAiProjectSelection = applyWebshellAiProjectSelection;
|
||||||
|
window.showNewProjectModalFromWebshellAi = showNewProjectModalFromWebshellAi;
|
||||||
|
window.wsToggleProjectPanel = wsToggleProjectPanel;
|
||||||
|
window.wsCloseProjectPanel = wsCloseProjectPanel;
|
||||||
|
|
||||||
|
// ─── end WebShell AI 项目选择器 ───
|
||||||
|
|
||||||
/** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */
|
/** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */
|
||||||
function wsRefreshSelectors() {
|
function wsRefreshSelectors() {
|
||||||
wsUpdateRoleSelectorDisplay();
|
wsUpdateRoleSelectorDisplay();
|
||||||
wsRenderRoleList();
|
wsRenderRoleList();
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'eino_single';
|
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'eino_single';
|
||||||
if (stored !== 'eino_single' && stored !== 'deep' && stored !== 'plan_execute' && stored !== 'supervisor') {
|
if (stored !== 'eino_single' && stored !== 'deep' && stored !== 'plan_execute' && stored !== 'supervisor') {
|
||||||
stored = 'eino_single';
|
stored = 'eino_single';
|
||||||
@@ -370,6 +569,11 @@ document.addEventListener('click', function (e) {
|
|||||||
if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) {
|
if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) {
|
||||||
wsCloseAgentModePanel();
|
wsCloseAgentModePanel();
|
||||||
}
|
}
|
||||||
|
var projectPanel = document.getElementById('ws-project-panel');
|
||||||
|
var projectBtn = document.getElementById('ws-project-btn');
|
||||||
|
if (projectPanel && projectPanel.style.display !== 'none' && projectBtn && !projectPanel.contains(e.target) && !projectBtn.contains(e.target)) {
|
||||||
|
wsCloseProjectPanel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── end WebShell AI 选择器 ───
|
// ─── end WebShell AI 选择器 ───
|
||||||
@@ -1873,6 +2077,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
|
|||||||
apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' })
|
apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' })
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
|
wsSetWebshellAiProject(conn, data.projectId || data.project_id || '');
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
var list = data.messages || [];
|
var list = data.messages || [];
|
||||||
list.forEach(function (msg) {
|
list.forEach(function (msg) {
|
||||||
@@ -1893,9 +2098,14 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
messagesContainer.appendChild(div);
|
messagesContainer.appendChild(div);
|
||||||
if (role === 'assistant' && msg.processDetails && msg.processDetails.length > 0) {
|
if (role === 'assistant') {
|
||||||
var block = renderWebshellProcessDetailsBlock(msg.processDetails, true);
|
var wsMergedDetails = (typeof window.mergeMessageReasoningContentIntoProcessDetails === 'function')
|
||||||
if (block) messagesContainer.appendChild(block);
|
? window.mergeMessageReasoningContentIntoProcessDetails(msg.processDetails || [], msg.reasoningContent)
|
||||||
|
: (msg.processDetails || []);
|
||||||
|
if (wsMergedDetails.length > 0) {
|
||||||
|
var block = renderWebshellProcessDetailsBlock(wsMergedDetails, true);
|
||||||
|
if (block) messagesContainer.appendChild(block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
@@ -2003,6 +2213,25 @@ function selectWebshell(id, stateReady) {
|
|||||||
'<div id="webshell-ai-messages" class="webshell-ai-messages"></div>' +
|
'<div id="webshell-ai-messages" class="webshell-ai-messages"></div>' +
|
||||||
'<div class="webshell-ai-input-area">' +
|
'<div class="webshell-ai-input-area">' +
|
||||||
'<div class="webshell-ai-selectors-row">' +
|
'<div class="webshell-ai-selectors-row">' +
|
||||||
|
'<div class="ws-project-selector-wrapper project-selector-wrapper">' +
|
||||||
|
'<button type="button" id="ws-project-btn" class="role-selector-btn" onclick="wsToggleProjectPanel()" aria-label="' + escapeHtml(wsProjectT('projects.chatSelectorButton', '选择项目')) + '" aria-haspopup="listbox" aria-expanded="false" title="' + escapeHtml(wsProjectT('projects.chatSelectorButton', '绑定项目后共享事实黑板(跨对话)')) + '">' +
|
||||||
|
'<span class="role-selector-icon" aria-hidden="true">📁</span>' +
|
||||||
|
'<span id="ws-project-text" class="role-selector-text">' + escapeHtml(wsProjectT('projects.noProject', '无项目')) + '</span>' +
|
||||||
|
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
||||||
|
'</button>' +
|
||||||
|
'<div id="ws-project-panel" class="role-selection-panel chat-project-panel" style="display:none;" role="listbox">' +
|
||||||
|
'<div class="role-selection-panel-header">' +
|
||||||
|
'<h3 class="role-selection-panel-title">' + escapeHtml(wsProjectT('projects.selectProject', '选择项目')) + '</h3>' +
|
||||||
|
'<button type="button" class="role-selection-panel-close" onclick="wsCloseProjectPanel()" title="' + escapeHtml(wsProjectT('common.close', '关闭')) + '" aria-label="' + escapeHtml(wsProjectT('common.close', '关闭')) + '">' +
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="chat-project-panel-body">' +
|
||||||
|
'<div id="ws-project-list" class="role-selection-list-main"></div>' +
|
||||||
|
'<div class="chat-project-panel-footer">' +
|
||||||
|
'<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromWebshellAi()">' +
|
||||||
|
'<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>' +
|
||||||
|
'<span class="chat-project-panel-create-label">' + escapeHtml(wsProjectT('projects.newProject', '新建项目')) + '</span>' +
|
||||||
|
'</button></div></div></div></div>' +
|
||||||
'<div class="ws-role-selector-wrapper">' +
|
'<div class="ws-role-selector-wrapper">' +
|
||||||
'<button type="button" class="role-selector-btn ws-role-selector-btn" id="ws-role-selector-btn" onclick="wsToggleRolePanel()">' +
|
'<button type="button" class="role-selector-btn ws-role-selector-btn" id="ws-role-selector-btn" onclick="wsToggleRolePanel()">' +
|
||||||
'<span id="ws-role-selector-icon" class="role-selector-icon">\ud83d\udd35</span>' +
|
'<span id="ws-role-selector-icon" class="role-selector-icon">\ud83d\udd35</span>' +
|
||||||
@@ -2174,9 +2403,11 @@ function selectWebshell(id, stateReady) {
|
|||||||
var aiNewConvBtn = document.getElementById('webshell-ai-new-conv');
|
var aiNewConvBtn = document.getElementById('webshell-ai-new-conv');
|
||||||
var aiConvListEl = document.getElementById('webshell-ai-conv-list');
|
var aiConvListEl = document.getElementById('webshell-ai-conv-list');
|
||||||
|
|
||||||
// 初始化角色 + 模式选择器
|
// 初始化角色 + 模式 + 项目选择器
|
||||||
wsLoadRoles();
|
wsLoadRoles();
|
||||||
wsInitAgentMode();
|
wsInitAgentMode();
|
||||||
|
if (typeof prefetchProjectsForChat === 'function') prefetchProjectsForChat();
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
var aiMemoInput = document.getElementById('webshell-ai-memo-input');
|
var aiMemoInput = document.getElementById('webshell-ai-memo-input');
|
||||||
var aiMemoStatus = document.getElementById('webshell-ai-memo-status');
|
var aiMemoStatus = document.getElementById('webshell-ai-memo-status');
|
||||||
var aiMemoClearBtn = document.getElementById('webshell-ai-memo-clear');
|
var aiMemoClearBtn = document.getElementById('webshell-ai-memo-clear');
|
||||||
@@ -2225,6 +2456,8 @@ function selectWebshell(id, stateReady) {
|
|||||||
if (aiNewConvBtn) {
|
if (aiNewConvBtn) {
|
||||||
aiNewConvBtn.addEventListener('click', function () {
|
aiNewConvBtn.addEventListener('click', function () {
|
||||||
delete webshellAiConvMap[conn.id];
|
delete webshellAiConvMap[conn.id];
|
||||||
|
delete webshellAiDraftProjectByConn[conn.id];
|
||||||
|
wsUpdateProjectButtonLabel();
|
||||||
if (aiMessages) {
|
if (aiMessages) {
|
||||||
aiMessages.innerHTML = '';
|
aiMessages.innerHTML = '';
|
||||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||||
@@ -2767,7 +3000,15 @@ function loadWebshellAiHistory(conn, messagesContainer) {
|
|||||||
return apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id) + '/ai-history', { method: 'GET' })
|
return apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id) + '/ai-history', { method: 'GET' })
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
if (data.conversationId) webshellAiConvMap[conn.id] = data.conversationId;
|
if (data.conversationId) {
|
||||||
|
webshellAiConvMap[conn.id] = data.conversationId;
|
||||||
|
apiFetch('/api/conversations/' + encodeURIComponent(data.conversationId), { method: 'GET' })
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (conv) {
|
||||||
|
if (conv) wsSetWebshellAiProject(conn, conv.projectId || conv.project_id || '');
|
||||||
|
})
|
||||||
|
.catch(function () { /* ignore */ });
|
||||||
|
}
|
||||||
var list = Array.isArray(data.messages) ? data.messages : [];
|
var list = Array.isArray(data.messages) ? data.messages : [];
|
||||||
list.forEach(function (msg) {
|
list.forEach(function (msg) {
|
||||||
var role = (msg.role || '').toLowerCase();
|
var role = (msg.role || '').toLowerCase();
|
||||||
@@ -2787,9 +3028,14 @@ function loadWebshellAiHistory(conn, messagesContainer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
messagesContainer.appendChild(div);
|
messagesContainer.appendChild(div);
|
||||||
if (role === 'assistant' && msg.processDetails && msg.processDetails.length > 0) {
|
if (role === 'assistant') {
|
||||||
var block = renderWebshellProcessDetailsBlock(msg.processDetails, true);
|
var wsHistMerged = (typeof window.mergeMessageReasoningContentIntoProcessDetails === 'function')
|
||||||
if (block) messagesContainer.appendChild(block);
|
? window.mergeMessageReasoningContentIntoProcessDetails(msg.processDetails || [], msg.reasoningContent)
|
||||||
|
: (msg.processDetails || []);
|
||||||
|
if (wsHistMerged.length > 0) {
|
||||||
|
var block = renderWebshellProcessDetailsBlock(wsHistMerged, true);
|
||||||
|
if (block) messagesContainer.appendChild(block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
@@ -2922,6 +3168,10 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
role: wsRole
|
role: wsRole
|
||||||
};
|
};
|
||||||
|
if (!convId) {
|
||||||
|
var wsPid = getWebshellAiProjectSelection(conn);
|
||||||
|
if (wsPid) body.projectId = wsPid;
|
||||||
|
}
|
||||||
|
|
||||||
// 流式输出:支持 progress 实时更新、response 打字机效果;若后端发送多段 response 则追加
|
// 流式输出:支持 progress 实时更新、response 打字机效果;若后端发送多段 response 则追加
|
||||||
var streamingTarget = ''; // 当前要打字显示的目标全文(用于打字机效果)
|
var streamingTarget = ''; // 当前要打字显示的目标全文(用于打字机效果)
|
||||||
@@ -2970,6 +3220,11 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
|
|
||||||
if (_et === 'conversation' && _ed.conversationId) {
|
if (_et === 'conversation' && _ed.conversationId) {
|
||||||
var convId = _ed.conversationId;
|
var convId = _ed.conversationId;
|
||||||
|
var prevDraft = webshellAiDraftProjectByConn[conn.id];
|
||||||
|
if (prevDraft) {
|
||||||
|
webshellAiProjectByConvId[convId] = prevDraft;
|
||||||
|
delete webshellAiDraftProjectByConn[conn.id];
|
||||||
|
}
|
||||||
webshellAiConvMap[conn.id] = convId;
|
webshellAiConvMap[conn.id] = convId;
|
||||||
var listEl = document.getElementById('webshell-ai-conv-list');
|
var listEl = document.getElementById('webshell-ai-conv-list');
|
||||||
if (listEl) fetchAndRenderWebshellAiConvList(conn, listEl).then(function () {
|
if (listEl) fetchAndRenderWebshellAiConvList(conn, listEl).then(function () {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
let wechatBindSessionKey = null;
|
let wechatBindSessionKey = null;
|
||||||
let wechatBindPollTimer = null;
|
let wechatBindPollTimer = null;
|
||||||
|
let wechatBindFlashTimer = null;
|
||||||
|
|
||||||
function wechatT(key, fallback) {
|
function wechatT(key, fallback) {
|
||||||
return typeof t === 'function' ? t(key) : fallback;
|
return typeof t === 'function' ? t(key) : fallback;
|
||||||
@@ -88,13 +89,50 @@ function stopWechatBindPoll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 已绑定:仅展示成功状态,不显示二维码/配对码 */
|
function clearWechatBindSuccessNotice() {
|
||||||
|
if (wechatBindFlashTimer) {
|
||||||
|
clearTimeout(wechatBindFlashTimer);
|
||||||
|
wechatBindFlashTimer = null;
|
||||||
|
}
|
||||||
|
const flash = document.getElementById('robot-wechat-bound-flash');
|
||||||
|
if (flash) {
|
||||||
|
flash.classList.remove('is-visible');
|
||||||
|
flash.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定成功后的内联提示(约 4.5 秒后自动淡出) */
|
||||||
|
function showWechatBindSuccessNotice(message) {
|
||||||
|
const text = message || wechatT('settings.robots.wechat.boundSuccess', '绑定成功,微信机器人已启用。');
|
||||||
|
const flash = document.getElementById('robot-wechat-bound-flash');
|
||||||
|
const flashText = document.getElementById('robot-wechat-bound-flash-text');
|
||||||
|
|
||||||
|
if (flash) {
|
||||||
|
if (flashText) flashText.textContent = text;
|
||||||
|
flash.hidden = false;
|
||||||
|
requestAnimationFrame(() => flash.classList.add('is-visible'));
|
||||||
|
if (wechatBindFlashTimer) clearTimeout(wechatBindFlashTimer);
|
||||||
|
wechatBindFlashTimer = setTimeout(() => {
|
||||||
|
flash.classList.remove('is-visible');
|
||||||
|
wechatBindFlashTimer = setTimeout(() => {
|
||||||
|
flash.hidden = true;
|
||||||
|
wechatBindFlashTimer = null;
|
||||||
|
}, 300);
|
||||||
|
}, 4500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showChatToast === 'function') {
|
||||||
|
window.showChatToast(text, 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已绑定:收起二维码区,仅展示紧凑摘要 */
|
||||||
function showWechatBoundUI(wechat) {
|
function showWechatBoundUI(wechat) {
|
||||||
const wc = wechat || {};
|
const wc = wechat || {};
|
||||||
const wrap = document.getElementById('robot-wechat-qr-wrap');
|
const wrap = document.getElementById('robot-wechat-qr-wrap');
|
||||||
const boundPanel = document.getElementById('robot-wechat-bound-panel');
|
const boundPanel = document.getElementById('robot-wechat-bound-panel');
|
||||||
const scanPanel = document.getElementById('robot-wechat-scan-panel');
|
const scanPanel = document.getElementById('robot-wechat-scan-panel');
|
||||||
const boundId = document.getElementById('robot-wechat-bound-id');
|
const summary = document.getElementById('robot-wechat-bound-summary');
|
||||||
const btn = document.getElementById('robot-wechat-bind-btn');
|
const btn = document.getElementById('robot-wechat-bind-btn');
|
||||||
|
|
||||||
stopWechatBindPoll();
|
stopWechatBindPoll();
|
||||||
@@ -102,8 +140,8 @@ function showWechatBoundUI(wechat) {
|
|||||||
setWechatBadge('bound');
|
setWechatBadge('bound');
|
||||||
setWechatCardBound(true);
|
setWechatCardBound(true);
|
||||||
|
|
||||||
if (wrap) wrap.hidden = false;
|
if (wrap) wrap.hidden = true;
|
||||||
if (boundPanel) boundPanel.hidden = false;
|
if (boundPanel) boundPanel.hidden = true;
|
||||||
if (scanPanel) scanPanel.hidden = true;
|
if (scanPanel) scanPanel.hidden = true;
|
||||||
|
|
||||||
const verifyWrap = document.getElementById('robot-wechat-verify-wrap');
|
const verifyWrap = document.getElementById('robot-wechat-verify-wrap');
|
||||||
@@ -117,14 +155,15 @@ function showWechatBoundUI(wechat) {
|
|||||||
}
|
}
|
||||||
if (ph) ph.hidden = false;
|
if (ph) ph.hidden = false;
|
||||||
|
|
||||||
if (boundId) {
|
const id = wc.ilink_bot_id || document.getElementById('robot-wechat-ilink-bot-id')?.value?.trim() || '';
|
||||||
const id = wc.ilink_bot_id || document.getElementById('robot-wechat-ilink-bot-id')?.value?.trim() || '';
|
if (summary) {
|
||||||
if (id) {
|
if (id) {
|
||||||
boundId.textContent = wechatT('settings.robots.wechat.boundBotId', '已绑定 Bot ID:') + id;
|
const prefix = wechatT('settings.robots.wechat.boundBotId', '已绑定 Bot ID:');
|
||||||
boundId.hidden = false;
|
summary.innerHTML = `${prefix}<code>${escapeHtml(id)}</code>`;
|
||||||
|
summary.hidden = false;
|
||||||
} else {
|
} else {
|
||||||
boundId.textContent = '';
|
summary.textContent = '';
|
||||||
boundId.hidden = true;
|
summary.hidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,21 +172,32 @@ function showWechatBoundUI(wechat) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
/** 扫码绑定进行中 */
|
/** 扫码绑定进行中 */
|
||||||
function showWechatScanUI() {
|
function showWechatScanUI() {
|
||||||
const wrap = document.getElementById('robot-wechat-qr-wrap');
|
const wrap = document.getElementById('robot-wechat-qr-wrap');
|
||||||
const boundPanel = document.getElementById('robot-wechat-bound-panel');
|
const boundPanel = document.getElementById('robot-wechat-bound-panel');
|
||||||
const scanPanel = document.getElementById('robot-wechat-scan-panel');
|
const scanPanel = document.getElementById('robot-wechat-scan-panel');
|
||||||
|
const summary = document.getElementById('robot-wechat-bound-summary');
|
||||||
const btn = document.getElementById('robot-wechat-bind-btn');
|
const btn = document.getElementById('robot-wechat-bind-btn');
|
||||||
|
|
||||||
setWechatBadge('scanning');
|
setWechatBadge('scanning');
|
||||||
setWechatCardBound(false);
|
setWechatCardBound(false);
|
||||||
|
clearWechatBindSuccessNotice();
|
||||||
ensureWechatSteps();
|
ensureWechatSteps();
|
||||||
updateWechatSteps('generate');
|
updateWechatSteps('generate');
|
||||||
|
|
||||||
if (wrap) wrap.hidden = false;
|
if (wrap) wrap.hidden = false;
|
||||||
if (boundPanel) boundPanel.hidden = true;
|
if (boundPanel) boundPanel.hidden = true;
|
||||||
if (scanPanel) scanPanel.hidden = false;
|
if (scanPanel) scanPanel.hidden = false;
|
||||||
|
if (summary) summary.hidden = true;
|
||||||
|
|
||||||
const verifyWrap = document.getElementById('robot-wechat-verify-wrap');
|
const verifyWrap = document.getElementById('robot-wechat-verify-wrap');
|
||||||
if (verifyWrap) verifyWrap.hidden = true;
|
if (verifyWrap) verifyWrap.hidden = true;
|
||||||
@@ -163,7 +213,10 @@ function showWechatScanUI() {
|
|||||||
/** 未绑定且未在扫码:隐藏面板 */
|
/** 未绑定且未在扫码:隐藏面板 */
|
||||||
function hideWechatQrWrap() {
|
function hideWechatQrWrap() {
|
||||||
const wrap = document.getElementById('robot-wechat-qr-wrap');
|
const wrap = document.getElementById('robot-wechat-qr-wrap');
|
||||||
|
const summary = document.getElementById('robot-wechat-bound-summary');
|
||||||
if (wrap) wrap.hidden = true;
|
if (wrap) wrap.hidden = true;
|
||||||
|
if (summary) summary.hidden = true;
|
||||||
|
clearWechatBindSuccessNotice();
|
||||||
setWechatBadge('idle');
|
setWechatBadge('idle');
|
||||||
setWechatCardBound(false);
|
setWechatCardBound(false);
|
||||||
}
|
}
|
||||||
@@ -278,6 +331,9 @@ async function pollWechatBindStatus() {
|
|||||||
const idEl = document.getElementById('robot-wechat-ilink-bot-id');
|
const idEl = document.getElementById('robot-wechat-ilink-bot-id');
|
||||||
if (idEl) idEl.value = data.ilink_bot_id;
|
if (idEl) idEl.value = data.ilink_bot_id;
|
||||||
}
|
}
|
||||||
|
showWechatBindSuccessNotice(
|
||||||
|
data.message || wechatT('settings.robots.wechat.boundSuccess', '绑定成功,微信机器人已启用。')
|
||||||
|
);
|
||||||
if (typeof loadConfig === 'function') {
|
if (typeof loadConfig === 'function') {
|
||||||
await loadConfig(false);
|
await loadConfig(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -299,6 +355,9 @@ async function pollWechatBindStatus() {
|
|||||||
break;
|
break;
|
||||||
case 'binded_redirect':
|
case 'binded_redirect':
|
||||||
stopWechatBindPoll();
|
stopWechatBindPoll();
|
||||||
|
showWechatBindSuccessNotice(
|
||||||
|
data.message || wechatT('settings.robots.wechat.alreadyBound', '该微信已绑定过,无需重复绑定。')
|
||||||
|
);
|
||||||
showWechatBoundUI({ bound: true });
|
showWechatBoundUI({ bound: true });
|
||||||
return;
|
return;
|
||||||
case 'expired':
|
case 'expired':
|
||||||
|
|||||||
Vendored
+32
File diff suppressed because one or more lines are too long
Vendored
+6696
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+22
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={775:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0;var r=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core;if(0!==e._renderService.dimensions.actualCellWidth&&0!==e._renderService.dimensions.actualCellHeight){var t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),i=Math.max(0,parseInt(t.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),o=r-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=i-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(o/e._renderService.dimensions.actualCellHeight))}}}},e}();t.FitAddon=r}},t={};return function r(i){if(t[i])return t[i].exports;var n=t[i]={exports:{}};return e[i](n,n.exports,r),n.exports}(775)})()}));
|
||||||
|
//# sourceMappingURL=xterm-addon-fit.js.map
|
||||||
Vendored
+190
@@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||||
|
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||||
|
* https://github.com/chjj/term.js
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* Originally forked from (with the author's permission):
|
||||||
|
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||||
|
* http://bellard.org/jslinux/
|
||||||
|
* Copyright (c) 2011 Fabrice Bellard
|
||||||
|
* The original design remains. The terminal itself
|
||||||
|
* has been extended to include xterm CSI codes, among
|
||||||
|
* other features.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default styles for xterm.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
.xterm {
|
||||||
|
cursor: text;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm.focus,
|
||||||
|
.xterm:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-helpers {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
/**
|
||||||
|
* The z-index of the helpers must be higher than the canvases in order for
|
||||||
|
* IMEs to appear on top.
|
||||||
|
*/
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-helper-textarea {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
left: -9999em;
|
||||||
|
top: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
z-index: -5;
|
||||||
|
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .composition-view {
|
||||||
|
/* TODO: Composition position got messed up somewhere */
|
||||||
|
background: #000;
|
||||||
|
color: #FFF;
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .composition-view.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-viewport {
|
||||||
|
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||||
|
background-color: #000;
|
||||||
|
overflow-y: scroll;
|
||||||
|
cursor: default;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-screen {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-screen canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-scroll-area {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-char-measure-element {
|
||||||
|
display: inline-block;
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -9999em;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm.enable-mouse-events {
|
||||||
|
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm.xterm-cursor-pointer,
|
||||||
|
.xterm .xterm-cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm.column-select.focus {
|
||||||
|
/* Column selection mode */
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-accessibility,
|
||||||
|
.xterm .xterm-message {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .live-region {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-dim {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||||
|
z-index: 6;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-decoration-overview-ruler {
|
||||||
|
z-index: 7;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-decoration-top {
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -934,7 +934,7 @@ Content-Type: application/json
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
|
<script src="/static/vendor/i18next.min.js"></script>
|
||||||
<script src="/static/js/i18n.js"></script>
|
<script src="/static/js/i18n.js"></script>
|
||||||
<script src="/static/js/api-docs.js"></script>
|
<script src="/static/js/api-docs.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+73
-39
@@ -8,7 +8,7 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/static/favicon.ico">
|
<link rel="shortcut icon" type="image/png" href="/static/favicon.ico">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/c2.css">
|
<link rel="stylesheet" href="/static/css/c2.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
|
<link rel="stylesheet" href="/static/vendor/xterm.css">
|
||||||
<script src="/static/js/router.js"></script>
|
<script src="/static/js/router.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -2817,6 +2817,13 @@
|
|||||||
<button type="button" class="btn-primary" id="robot-wechat-bind-btn" onclick="startWechatRobotBind()" data-i18n="settings.robots.wechat.bindButton">生成二维码并绑定</button>
|
<button type="button" class="btn-primary" id="robot-wechat-bind-btn" onclick="startWechatRobotBind()" data-i18n="settings.robots.wechat.bindButton">生成二维码并绑定</button>
|
||||||
<p class="robot-wechat-hint" id="robot-wechat-bind-hint" data-i18n="settings.robots.wechat.bindHint">用微信扫码确认后会自动保存并启用。</p>
|
<p class="robot-wechat-hint" id="robot-wechat-bind-hint" data-i18n="settings.robots.wechat.bindHint">用微信扫码确认后会自动保存并启用。</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="robot-wechat-bound-flash" class="robot-wechat-bound-flash" hidden role="status">
|
||||||
|
<span class="robot-wechat-bound-flash-icon" aria-hidden="true">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||||
|
</span>
|
||||||
|
<span id="robot-wechat-bound-flash-text" data-i18n="settings.robots.wechat.boundSuccess">绑定成功,微信机器人已启用。</span>
|
||||||
|
</div>
|
||||||
|
<p id="robot-wechat-bound-summary" class="robot-wechat-bound-summary" hidden></p>
|
||||||
<div id="robot-wechat-qr-wrap" class="robot-wechat-panel" hidden>
|
<div id="robot-wechat-qr-wrap" class="robot-wechat-panel" hidden>
|
||||||
<div id="robot-wechat-bound-panel" class="robot-wechat-bound-panel" hidden>
|
<div id="robot-wechat-bound-panel" class="robot-wechat-bound-panel" hidden>
|
||||||
<div class="robot-wechat-bound-icon" aria-hidden="true">
|
<div class="robot-wechat-bound-icon" aria-hidden="true">
|
||||||
@@ -3010,19 +3017,27 @@
|
|||||||
|
|
||||||
<!-- 日志审计 -->
|
<!-- 日志审计 -->
|
||||||
<div id="settings-section-audit" class="settings-section-content">
|
<div id="settings-section-audit" class="settings-section-content">
|
||||||
<div class="settings-section-header">
|
<div class="audit-section-head">
|
||||||
<h3 data-i18n="settingsAudit.title">日志审计</h3>
|
<h3 data-i18n="settingsAudit.title">日志审计</h3>
|
||||||
<p class="settings-description" data-i18n="settingsAudit.description">记录平台管理类操作(登录、配置、删除等),不记录对话正文、终端/WebShell 每次命令与工具调用明细。</p>
|
<div id="audit-summary-stats" class="audit-summary-tags" hidden>
|
||||||
<p id="audit-retention-hint" class="settings-description audit-retention-hint" hidden></p>
|
<span class="audit-summary-tag"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statTotal">当前筛选</span><strong id="audit-stat-total">0</strong></span>
|
||||||
|
<span class="audit-summary-tag audit-summary-tag--ok"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statSuccess">成功</span><strong id="audit-stat-success">0</strong></span>
|
||||||
|
<span class="audit-summary-tag audit-summary-tag--warn"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statFailures">失败</span><strong id="audit-stat-failures">0</strong></span>
|
||||||
|
<span class="audit-summary-tag"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statRecent7d">近 7 天</span><strong id="audit-stat-recent">0</strong></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="audit-summary-stats" class="audit-summary-stats" hidden>
|
<div class="audit-filter-card">
|
||||||
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statTotal">当前筛选</span><strong id="audit-stat-total">0</strong></div>
|
<div class="audit-time-presets" id="audit-time-presets">
|
||||||
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statFailures">失败</span><strong id="audit-stat-failures">0</strong></div>
|
<span class="audit-time-presets-label" data-i18n="settingsAudit.timePresets">快捷</span>
|
||||||
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statRecent7d">近 7 天</span><strong id="audit-stat-recent">0</strong></div>
|
<button type="button" class="audit-time-preset-btn" data-preset="15m" data-i18n="settingsAudit.preset15m">最近15分钟</button>
|
||||||
</div>
|
<button type="button" class="audit-time-preset-btn" data-preset="1h" data-i18n="settingsAudit.preset1h">最近1小时</button>
|
||||||
<div class="audit-logs-toolbar">
|
<button type="button" class="audit-time-preset-btn" data-preset="24h" data-i18n="settingsAudit.preset24h">最近24小时</button>
|
||||||
<div class="audit-logs-filters">
|
<button type="button" class="audit-time-preset-btn" data-preset="7d" data-i18n="settingsAudit.preset7d">最近7天</button>
|
||||||
<label class="audit-filter-cascade-group">
|
<button type="button" class="audit-time-preset-btn" data-preset="today" data-i18n="settingsAudit.presetToday">今天</button>
|
||||||
|
</div>
|
||||||
|
<div class="audit-filter-fields">
|
||||||
|
<div class="audit-filter-row">
|
||||||
|
<label class="audit-field audit-field--event">
|
||||||
<span data-i18n="settingsAudit.filterEvent">事件类型</span>
|
<span data-i18n="settingsAudit.filterEvent">事件类型</span>
|
||||||
<div class="audit-filter-cascade">
|
<div class="audit-filter-cascade">
|
||||||
<select id="audit-filter-category" onchange="onAuditCategoryFilterChange()" aria-label="类别">
|
<select id="audit-filter-category" onchange="onAuditCategoryFilterChange()" aria-label="类别">
|
||||||
@@ -3049,7 +3064,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="audit-field audit-field--result">
|
||||||
<span data-i18n="settingsAudit.filterResult">结果</span>
|
<span data-i18n="settingsAudit.filterResult">结果</span>
|
||||||
<select id="audit-filter-result">
|
<select id="audit-filter-result">
|
||||||
<option value="" data-i18n="settingsAudit.filterAll">全部</option>
|
<option value="" data-i18n="settingsAudit.filterAll">全部</option>
|
||||||
@@ -3057,36 +3072,54 @@
|
|||||||
<option value="failure">failure</option>
|
<option value="failure">failure</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<div class="audit-filter-time-group">
|
||||||
|
<label class="audit-field audit-field--time">
|
||||||
<span data-i18n="settingsAudit.filterSince">开始时间</span>
|
<span data-i18n="settingsAudit.filterSince">开始时间</span>
|
||||||
<input type="datetime-local" id="audit-filter-since" />
|
<div class="audit-datetime-field" id="audit-filter-since-field">
|
||||||
|
<input type="text" id="audit-filter-since" class="audit-datetime-input" readonly autocomplete="off" data-i18n="settingsAudit.datetimePlaceholder" data-i18n-attr="placeholder" placeholder="选择日期时间" />
|
||||||
|
<button type="button" class="audit-datetime-btn audit-datetime-clear-btn" title="Clear" aria-label="Clear" hidden>×</button>
|
||||||
|
<button type="button" class="audit-datetime-btn audit-datetime-open-btn" title="Open" aria-label="Open">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/><path d="M16 2v4M8 2v4M3 10h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="audit-field audit-field--time">
|
||||||
<span data-i18n="settingsAudit.filterUntil">结束时间</span>
|
<span data-i18n="settingsAudit.filterUntil">结束时间</span>
|
||||||
<input type="datetime-local" id="audit-filter-until" />
|
<div class="audit-datetime-field" id="audit-filter-until-field">
|
||||||
|
<input type="text" id="audit-filter-until" class="audit-datetime-input" readonly autocomplete="off" data-i18n="settingsAudit.datetimePlaceholder" data-i18n-attr="placeholder" placeholder="选择日期时间" />
|
||||||
|
<button type="button" class="audit-datetime-btn audit-datetime-clear-btn" title="Clear" aria-label="Clear" hidden>×</button>
|
||||||
|
<button type="button" class="audit-datetime-btn audit-datetime-open-btn" title="Open" aria-label="Open">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/><path d="M16 2v4M8 2v4M3 10h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="audit-filter-bottom">
|
||||||
|
<label class="audit-field audit-field--keyword">
|
||||||
<span data-i18n="settingsAudit.filterQuery">关键词</span>
|
<span data-i18n="settingsAudit.filterQuery">关键词</span>
|
||||||
<input type="text" id="audit-filter-q" data-i18n="settingsAudit.filterQueryPlaceholder" data-i18n-attr="placeholder" placeholder="消息 / 资源 ID / 操作名" />
|
<input type="text" id="audit-filter-q" data-i18n="settingsAudit.filterQueryPlaceholder" data-i18n-attr="placeholder" placeholder="消息 / 资源 ID / 操作名" />
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="btn-secondary" onclick="filterAuditLogs()" data-i18n="settingsAudit.filterBtn">筛选</button>
|
<div class="audit-logs-actions">
|
||||||
<button type="button" class="btn-secondary" onclick="resetAuditLogFilters()" data-i18n="settingsAudit.resetBtn">重置</button>
|
<button type="button" class="btn-primary" onclick="filterAuditLogs()" data-i18n="settingsAudit.filterBtn">筛选</button>
|
||||||
</div>
|
<button type="button" class="btn-secondary" onclick="resetAuditLogFilters()" data-i18n="settingsAudit.resetBtn">重置</button>
|
||||||
<div class="audit-logs-actions">
|
<button type="button" class="btn-secondary" onclick="refreshAuditLogs()" data-i18n="common.refresh">刷新</button>
|
||||||
<button type="button" class="btn-secondary" onclick="refreshAuditLogs()" data-i18n="common.refresh">刷新</button>
|
<div class="audit-export-dropdown">
|
||||||
<div class="audit-export-dropdown">
|
<button type="button" class="btn-secondary audit-export-trigger" id="audit-export-trigger" onclick="toggleAuditExportMenu(event)" aria-haspopup="true" aria-expanded="false">
|
||||||
<button type="button" class="btn-secondary audit-export-trigger" id="audit-export-trigger" onclick="toggleAuditExportMenu(event)" aria-haspopup="true" aria-expanded="false">
|
<span data-i18n="settingsAudit.exportBtn">导出</span>
|
||||||
<span data-i18n="settingsAudit.exportBtn">导出</span>
|
<span class="audit-export-caret" aria-hidden="true">▾</span>
|
||||||
<span class="audit-export-caret" aria-hidden="true">▾</span>
|
</button>
|
||||||
</button>
|
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
|
||||||
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
|
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
|
||||||
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
|
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
|
||||||
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p id="audit-filter-timezone-hint" class="audit-timezone-hint" hidden></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="audit-log-list" class="audit-log-list c2-event-list"></div>
|
<div id="audit-log-list" class="audit-log-list"></div>
|
||||||
<div id="audit-logs-pagination" class="pagination-container audit-logs-pagination"></div>
|
<div id="audit-logs-pagination" class="pagination-container audit-logs-pagination"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3484,16 +3517,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Marked.js + DOMPurify:本地 vendor,避免 CDN 不可用导致 Markdown 降级为纯文本 -->
|
<!-- Marked.js + DOMPurify + 其他前端依赖:本地 vendor,内网/离线部署不依赖 CDN -->
|
||||||
<script src="/static/vendor/marked.min.js"></script>
|
<script src="/static/vendor/marked.min.js"></script>
|
||||||
<script src="/static/vendor/purify.min.js"></script>
|
<script src="/static/vendor/purify.min.js"></script>
|
||||||
<script src="/static/js/sanitize-markdown.js"></script>
|
<script src="/static/js/sanitize-markdown.js"></script>
|
||||||
<!-- Cytoscape.js for attack chain visualization -->
|
<!-- Cytoscape.js for attack chain visualization -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>
|
<script src="/static/vendor/cytoscape.min.js"></script>
|
||||||
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
|
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/elkjs@0.9.2/lib/elk.bundled.js"></script>
|
<script src="/static/vendor/elk.bundled.js"></script>
|
||||||
<!-- SheetJS for XLSX export (info-collect) -->
|
<!-- SheetJS for XLSX export (info-collect) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
<script src="/static/vendor/xlsx.full.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 确保ELK对象全局可用
|
// 确保ELK对象全局可用
|
||||||
if (typeof ELK === 'undefined' && typeof elk !== 'undefined') {
|
if (typeof ELK === 'undefined' && typeof elk !== 'undefined') {
|
||||||
@@ -4287,7 +4320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
|
<script src="/static/vendor/i18next.min.js"></script>
|
||||||
<script src="/static/js/i18n.js"></script>
|
<script src="/static/js/i18n.js"></script>
|
||||||
<script src="/static/js/builtin-tools.js"></script>
|
<script src="/static/js/builtin-tools.js"></script>
|
||||||
<script src="/static/js/auth.js"></script>
|
<script src="/static/js/auth.js"></script>
|
||||||
@@ -4301,10 +4334,11 @@
|
|||||||
<script src="/static/js/chat.js"></script>
|
<script src="/static/js/chat.js"></script>
|
||||||
<script src="/static/js/hitl.js"></script>
|
<script src="/static/js/hitl.js"></script>
|
||||||
<script src="/static/js/settings.js"></script>
|
<script src="/static/js/settings.js"></script>
|
||||||
|
<script src="/static/js/audit-datetime-picker.js"></script>
|
||||||
<script src="/static/js/audit.js"></script>
|
<script src="/static/js/audit.js"></script>
|
||||||
<script src="/static/js/wechat-robot.js"></script>
|
<script src="/static/js/wechat-robot.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.js"></script>
|
<script src="/static/vendor/xterm.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
|
<script src="/static/vendor/xterm-addon-fit.js"></script>
|
||||||
<script src="/static/js/terminal.js"></script>
|
<script src="/static/js/terminal.js"></script>
|
||||||
<script src="/static/js/knowledge.js"></script>
|
<script src="/static/js/knowledge.js"></script>
|
||||||
<script src="/static/js/skills.js"></script>
|
<script src="/static/js/skills.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user