Skip to content
Closed

Dev #672

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9c7d1c7
chore: sync main into dev after main-5a5c5a74fae6
github-actions[bot] Jul 29, 2026
7c2a5ee
Merge pull request #646 from AgentFlocks/chore/sync-main-into-dev-mai…
duguwanglong Jul 29, 2026
b3bc4e9
fix(qingteng): remove public login tool
Jul 29, 2026
e94fa90
Merge pull request #647 from AgentFlocks/fix/remove-qingteng-login-tool
duguwanglong Jul 29, 2026
ed21428
Build clean SOC workflow branch
Jul 30, 2026
015504e
fix(feishu): support both dispatcher method conventions
Jul 30, 2026
c57d482
Add SOC dashboard mock activity data
Jul 30, 2026
df13423
Merge pull request #649 from AgentFlocks/fix/feishu-dispatcher-compat
duguwanglong Jul 30, 2026
de77f30
fix(session): restore create actions and refine search
Jul 30, 2026
b23baf8
Gate SOC dashboard mock activity data
Jul 30, 2026
a476195
Gate SOC dashboard mock data and cache usage metrics
Jul 30, 2026
1911319
Merge pull request #651 from AgentFlocks/codex/zhiwang-ycc-workflow-c…
duguwanglong Jul 30, 2026
c52ca90
fix(cli): warn agents to use server-only restart
Jul 30, 2026
d0fa271
Merge pull request #652 from AgentFlocks/fix/restore-session-create-a…
duguwanglong Jul 30, 2026
a666d59
feat(workflow): add incremental stream alert triage
duguwanglong Jul 30, 2026
5c26575
fix(channel): keep previous session active on /new
duguwanglong Jul 30, 2026
d5f0320
feat(webui): refine chat reasoning previews
duguwanglong Jul 30, 2026
c150f7c
Merge pull request #654 from AgentFlocks/fix/new-channel-conversation
stephamie7 Jul 30, 2026
39b8147
fix(workflow): harden incremental triage consistency
duguwanglong Jul 30, 2026
704d2f6
Merge pull request #653 from AgentFlocks/feat/stream-alert-triage-inc…
stephamie7 Jul 30, 2026
a42890d
fix(session): isolate direct commands from model context
Jul 30, 2026
d4ef5a0
refactor(memory): establish filesystem memory foundation
Jul 30, 2026
49dc144
fix(updater): fall back to snapshot backup
Jul 30, 2026
69be9b3
Merge pull request #657 from AgentFlocks/fix/updater-backup-snapshot-…
stephamie7 Jul 31, 2026
bfb7318
Merge pull request #656 from AgentFlocks/fix/slash-command-context-usage
stephamie7 Jul 31, 2026
b32a12d
refactor: remove category-based task delegation
Jul 31, 2026
41c830b
fix(agent): preserve legacy prompt builder arguments
Jul 31, 2026
593802b
fix(delegation): preserve routing compatibility
Jul 31, 2026
4f52cc9
fix: correct workflow task center status
Jul 31, 2026
801dfd6
Merge pull request #659 from AgentFlocks/refactor/remove-delegate-cat…
stephamie7 Jul 31, 2026
dcbe179
Merge pull request #661 from AgentFlocks/codex/fix-workflow-task-status
stephamie7 Jul 31, 2026
c2c3b29
fix(webui): position reasoning toggle after preview
duguwanglong Aug 3, 2026
3df1b63
feat(session): refine channel session presentation
duguwanglong Aug 3, 2026
2aa1632
fix(memory): sync index before every search
Aug 3, 2026
5cb3bfb
feat(session): show process group duration
duguwanglong Aug 3, 2026
3ea5617
fix(session): refine process duration label
duguwanglong Aug 3, 2026
e50d3fc
fix(channel): preserve captions and derived titles
duguwanglong Aug 3, 2026
4ffc3c7
Merge pull request #662 from AgentFlocks/feat/thinking-preview-menu-i…
xiami762 Aug 3, 2026
d9b0fd5
feat(workspace): organize and edit memory files
Aug 3, 2026
fdee13f
fix(chat): prevent first message from staying in sending state
duguwanglong Aug 3, 2026
cb16e01
Merge pull request #658 from AgentFlocks/feat/memory-foundation
duguwanglong Aug 3, 2026
2bd3a9b
Revert "refactor(memory): establish filesystem memory foundation"
xiami762 Aug 3, 2026
f0e8963
Merge pull request #668 from AgentFlocks/revert-658-feat/memory-found…
duguwanglong Aug 3, 2026
5b63a82
feat(provider): add deepseek-v4-flash-0731
duguwanglong Aug 3, 2026
70862f5
chore(release): bump version to v2026.8.3
duguwanglong Aug 3, 2026
a678976
fix(provider): correct deepseek model metadata display
duguwanglong Aug 3, 2026
5d85e48
fix(model): persist cache pricing and convert currencies
duguwanglong Aug 3, 2026
bca6246
fix(model): prevent pricing data loss
duguwanglong Aug 3, 2026
8ef5368
chore(release): bump version to v2026.8.4
duguwanglong Aug 4, 2026
f15ec7c
Merge pull request #667 from AgentFlocks/feat/add-deepseek-v4-flash-0…
xiami762 Aug 4, 2026
d5e6b63
fix(workflow): isolate stream triage memory growth
duguwanglong Aug 4, 2026
037aeda
fix(workflow): address process isolation review findings
duguwanglong Aug 4, 2026
3effd89
chore(workflow): bump stream alert triage to 1.1.1
duguwanglong Aug 4, 2026
98f60df
fix(webui): render LaTeX in session messages
Aug 4, 2026
e00de1d
fix(flockshub): sync soc_ui index version
duguwanglong Aug 4, 2026
2194dfe
Merge pull request #670 from AgentFlocks/fix/stream-alert-triage-oom
xiami762 Aug 4, 2026
b2ace66
chore(merge): sync upstream changes
duguwanglong Aug 4, 2026
5d9e27d
Merge pull request #671 from AgentFlocks/fix/session-math-rendering
duguwanglong Aug 4, 2026
462f75c
Merge pull request #664 from AgentFlocks/fix/first-message-stuck-sending
xiami762 Aug 4, 2026
aea28ec
test(hub): sync expectations with bundled metadata
duguwanglong Aug 4, 2026
ac598aa
Merge pull request #673 from AgentFlocks/codex/fix-ci-dashboard-expec…
xiami762 Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .flocks/flockshub/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -14640,7 +14640,7 @@
"name": "SOC Workspace WebUI",
"description": "SOC workspace pages for posture, overview, and alert investigation.",
"descriptionCn": "SOC 工作区页面,包含态势、SOC 总览和告警调查。",
"version": "1.1.4",
"version": "1.1.5",
"category": "workflow-automation",
"tags": [
"siem",
Expand Down Expand Up @@ -14708,7 +14708,7 @@
"nameCn": "HTTP研判工作流",
"description": "Downstream alert triage workflow that writes triage results to SOC DB by default with optional JSONL output.",
"descriptionCn": "下游告警研判工作流,默认写入 SOC DB,并保留 JSONL 输出配置。",
"version": "1.0.0",
"version": "1.1.1",
"category": "workflow-automation",
"tags": [
"siem",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"is_duplicate",
"_syslog_meta",
)
DRIVER_FIELDS = frozenset(DISPLAY_FIELDS)
DRIVER_FIELDS = frozenset((*DISPLAY_FIELDS, "triage_attack_verdict", "triage_attack_success"))
FILTER_FIELDS = frozenset(
{
"_source_type",
Expand All @@ -89,7 +89,8 @@
"threat_name",
"threat_type",
"threat_phase",
"threat_result",
"triage_attack_verdict",
"triage_attack_success",
"rsp_status_code",
"sip",
"dport",
Expand All @@ -104,7 +105,7 @@
{"key": "threat_name", "label": "Threat Name"},
{"key": "threat_type", "label": "Threat Type"},
{"key": "threat_phase", "label": "Attack Stage"},
{"key": "threat_result", "label": "Attack Result"},
{"key": "triage_attack_success", "label": "Attack Result"},
{"key": "direction", "label": "Direction"},
{"key": "sip", "label": "Source IP"},
{"key": "sport", "label": "Source Port"},
Expand Down Expand Up @@ -159,6 +160,7 @@ def resolve(self, *, page_id: str, slot_id: str, contract_id: str, contract_vers
"dateColumn": settings["date_column"],
"eventTimeColumn": settings["event_time_column"],
},
predicate_value_resolver=_filter_value,
capabilities=frozenset({"query"}),
)

Expand Down Expand Up @@ -236,13 +238,17 @@ def _incident_from_row(row: InternalDataRow) -> dict[str, Any]:
table_cells = _table_cells(record)
triage_report = _text(record.get("triage_report"))
report_title = _first_text(record, "report_title") or _report_title_from_markdown(triage_report)
triage_attack_verdict = _triage_attack_verdict(record)
triage_attack_success = _triage_attack_success(record)

return {
"id": record_id,
"sourceRecordId": record_id,
"observedAt": observed_at,
"rawAlerts": 1,
"priority": "P1" if verdict == "success" else "P2",
"triageAttackVerdict": triage_attack_verdict,
"triageAttackSuccess": triage_attack_success,
"reportTitle": report_title,
"reason": threat_msg,
"owner": "",
Expand All @@ -265,7 +271,7 @@ def _incident_from_row(row: InternalDataRow) -> dict[str, Any]:
"business": _text(record.get("asset_group_name")),
},
"conclusion": {
"verdict": _verdict_label(verdict),
"verdict": triage_attack_verdict,
"summary": threat_msg or threat_name,
"recommendation": "",
},
Expand All @@ -288,6 +294,8 @@ def _table_cells(record: dict[str, Any]) -> dict[str, dict[str, str]]:
cells[key] = {"value": value}
if "time" not in cells and (observed := _observed_at(record)):
cells["time"] = {"value": observed}
cells["triage_attack_verdict"] = {"value": _triage_attack_verdict(record)}
cells["triage_attack_success"] = {"value": _triage_attack_success(record)}
return cells


Expand Down Expand Up @@ -423,30 +431,51 @@ def _request_method(record: dict[str, Any]) -> str:


def _verdict_bucket(record: dict[str, Any]) -> str:
if record.get("attack_success") is True:
return "success"
raw = " ".join(
_text(record.get(key)).lower()
for key in ("attack_verdict", "threat_result")
)
if any(marker in raw for marker in ("success", "attack_success", "succeeded")):
verdict = _triage_attack_verdict(record)
if verdict == "non_attack":
return "benign"
if verdict == "attack":
attack_success = _triage_attack_success(record)
if attack_success == "success":
return "success"
if attack_success == "failed":
return "failed"
return "attack"
return "unknown"


def _triage_attack_success(record: dict[str, Any]) -> str:
raw_verdict = _text(record.get("triage_attack_verdict")).strip().lower()
if _triage_attack_verdict(record) != "attack":
return "unknown"
value = _text(record.get("triage_attack_success")).strip().lower()
if value in {"success", "failed", "unknown"}:
return value
legacy_value = record.get("triage_attack_success")
if legacy_value is True:
return "success"
if any(marker in raw for marker in ("failed", "blocked", "attack_failed")):
if legacy_value is False and raw_verdict == "attack_failed":
return "failed"
if any(marker in raw for marker in ("benign", "normal", "safe")):
return "benign"
if "attack" in raw:
return "unknown"


def _triage_attack_verdict(record: dict[str, Any]) -> str:
value = _text(record.get("triage_attack_verdict")).strip().lower()
if value in {"attack", "non_attack", "unknown"}:
return value
if value in {"attack_success", "attack_failed"}:
return "attack"
if value == "benign":
return "non_attack"
return "unknown"


def _verdict_label(bucket: str) -> str:
return {
"success": "success",
"failed": "failed",
"benign": "benign",
"attack": "attack",
}.get(bucket, "unknown")
def _filter_value(record: dict[str, Any], field: str) -> Any:
if field == "triage_attack_verdict":
return _triage_attack_verdict(record)
if field == "triage_attack_success":
return _triage_attack_success(record)
return record.get(field)


def _source_file_label(paths: tuple[Path, ...]) -> str:
Expand Down
2 changes: 1 addition & 1 deletion .flocks/flockshub/plugins/webuis/soc_ui/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"name": "SOC Workspace WebUI",
"description": "SOC workspace pages for posture, overview, and alert investigation.",
"descriptionCn": "SOC 工作区页面,包含态势、SOC 总览和告警调查。",
"version": "1.1.4",
"version": "1.1.5",
"author": "Flocks Team",
"license": "MIT",
"homepage": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,14 @@ const FILTER_VALUE_TEXT: Record<string, Record<string, string>> = {
exploit: '利用',
exploitation: '利用',
},
threat_result: {
attack_success: '攻击成功',
success: '成功',
succeeded: '成功',
attack_failed: '攻击失败',
failed: '失败',
blocked: '已阻断',
detected: '已检测',
attack: '攻击行为',
benign: '安全',
safe: '安全',
normal: '正常',
triage_attack_success: {
success: '攻击成功',
failed: '攻击失败',
unknown: '未知',
},
triage_attack_verdict: {
attack: '攻击',
non_attack: '非攻击',
unknown: '未知',
},
};
Expand Down
40 changes: 21 additions & 19 deletions .flocks/flockshub/plugins/webuis/soc_ui/soc_alerts/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { api } from '@flocks/webui-contract-sdk';
import { filterOptionText, matchesFilterOptionSearch } from './filterValues';

type Tone = 'red' | 'orange' | 'blue' | 'green' | 'purple' | 'slate';
type FilterKey = '_source_type' | 'net_type' | 'direction' | 'threat_severity' | 'threat_level' | 'threat_name' | 'threat_type' | 'threat_phase' | 'threat_result' | 'rsp_status_code' | 'sip' | 'dport' | 'dip' | 'req_host' | 'threat_rule_id';
type FilterKey = '_source_type' | 'net_type' | 'direction' | 'threat_severity' | 'threat_level' | 'threat_name' | 'threat_type' | 'threat_phase' | 'triage_attack_verdict' | 'triage_attack_success' | 'rsp_status_code' | 'sip' | 'dport' | 'dip' | 'req_host' | 'threat_rule_id';
type TimeRangeKey = '15m' | '1h' | '2h' | '24h' | 'today' | '7d' | '30d';
type TimeFilterMode = 'relative' | 'custom';
type TimePanelTab = 'auto' | 'custom';
Expand Down Expand Up @@ -50,6 +50,8 @@ interface IncidentCluster {
rawAlerts?: number;
confidence?: number;
priority?: 'P1' | 'P2';
triageAttackVerdict?: 'attack' | 'non_attack' | 'unknown';
triageAttackSuccess?: 'success' | 'failed' | 'unknown';
reportTitle?: string;
reason?: string;
owner?: string;
Expand Down Expand Up @@ -264,6 +266,8 @@ const EN_TEXT: Record<string, string> = {
'展开趋势图': 'Expand timeline',
'攻击成功': 'Attack Success',
'攻击失败': 'Attack Failed',
'攻击': 'Attack',
'非攻击': 'Non-attack',
'入站': 'Inbound',
'出站': 'Outbound',
'横向': 'Lateral',
Expand Down Expand Up @@ -395,7 +399,8 @@ const BASE_FILTER_CONFIGS: FilterConfig[] = [
const MORE_FILTER_CONFIGS: FilterConfig[] = [
{ key: 'threat_type', label: '威胁类型' },
{ key: 'threat_phase', label: '攻击阶段' },
{ key: 'threat_result', label: '攻击结果' },
{ key: 'triage_attack_verdict', label: '攻击行为' },
{ key: 'triage_attack_success', label: '攻击结果' },
{ key: 'rsp_status_code', label: '响应状态' },
{ key: 'sip', label: '源地址' },
{ key: 'dport', label: '目标端口' },
Expand All @@ -415,7 +420,8 @@ const DEFAULT_FILTER_VALUES: Record<FilterKey, string[]> = {
threat_name: [],
threat_type: [],
threat_phase: [],
threat_result: [],
triage_attack_verdict: [],
triage_attack_success: [],
rsp_status_code: [],
sip: [],
dport: [],
Expand Down Expand Up @@ -923,15 +929,9 @@ function cellValue(incident: IncidentCluster, key: string, fallback = '') {
return textValue(incident.tableCells?.[key]?.value, fallback);
}

function rawAttackResultValue(incident: IncidentCluster) {
return cellValue(incident, 'attach_result') || cellValue(incident, 'attack_result') || cellValue(incident, 'threat_result');
}

function verdictBucket(incident: IncidentCluster): 'success' | 'failed' | 'unknown' {
const rawResult = rawAttackResultValue(incident).toLowerCase();
const verdict = incident.conclusion?.verdict || '';
if (rawResult === 'success' || rawResult === 'succeeded' || verdict.includes('成功')) return 'success';
if (rawResult === 'failed' || rawResult === 'blocked' || verdict.includes('失败')) return 'failed';
if (incident.triageAttackSuccess === 'success') return 'success';
if (incident.triageAttackSuccess === 'failed') return 'failed';
return 'unknown';
}

Expand All @@ -942,19 +942,19 @@ function attackResultLabel(incident: IncidentCluster, tr: Translate) {
return tr('未知');
}

function attackVerdictLabel(incident: IncidentCluster, tr: Translate) {
if (incident.triageAttackVerdict === 'attack') return tr('攻击');
if (incident.triageAttackVerdict === 'non_attack') return tr('非攻击');
return tr('未知');
}

function attackResultTone(incident: IncidentCluster): Tone {
const bucket = verdictBucket(incident);
if (bucket === 'success') return 'red';
if (bucket === 'failed') return 'green';
return 'slate';
}

function severityTone(incident: IncidentCluster): Tone {
if (incident.priority === 'P1' || incident.conclusion?.verdict?.includes('成功')) return 'red';
if (incident.conclusion?.verdict?.includes('失败')) return 'green';
return 'orange';
}

function dateFromIncident(incident: IncidentCluster) {
const value = incident.observedAt || cellValue(incident, 'time');
if (!value) return null;
Expand Down Expand Up @@ -1021,6 +1021,8 @@ function niceAxisMax(value: number) {
}

function readFilterValue(incident: IncidentCluster, key: FilterKey) {
if (key === 'triage_attack_verdict') return incident.triageAttackVerdict || 'unknown';
if (key === 'triage_attack_success') return incident.triageAttackSuccess || 'unknown';
return cellValue(incident, key);
}

Expand Down Expand Up @@ -1948,7 +1950,7 @@ function IncidentInlineDetail({ incident, onClose, tr }: { incident: IncidentClu
const [copyDone, setCopyDone] = useState(false);
const report = parseTaggedReport(incident.triageReport);
const markdownReport = report ? null : parseMarkdownReport(incident.triageReport);
const attackJudgement = incident.conclusion?.verdict || tr('待确认');
const attackJudgement = attackVerdictLabel(incident, tr);
const attackResult = attackResultLabel(incident, tr);
const attackResultBucket = verdictBucket(incident);
const srcAddress = incident.srcIp || cellValue(incident, 'sip', '-');
Expand Down Expand Up @@ -2065,7 +2067,7 @@ function IncidentInlineDetail({ incident, onClose, tr }: { incident: IncidentClu
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-slate-200 bg-white px-5 py-4">
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2">
<Badge tone={severityTone(incident)}>{attackJudgement}</Badge>
<Badge tone={attackResultTone(incident)}>{attackResult}</Badge>
<span className="text-xs text-slate-500">{observedAt}</span>
<span className="text-xs text-slate-300">/</span>
<span className="max-w-[320px] truncate font-mono text-xs text-slate-500" title={ruleId}>{ruleId}</span>
Expand Down
Loading
Loading