Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8c9d25857 | |||
| 660ac4b659 | |||
| 91685ebfdd | |||
| 65910f5417 | |||
| 17b87290c8 | |||
| bd5735f970 | |||
| 05f9112fab | |||
| b926b35703 | |||
| 8df1d4a83c | |||
| aad5a6b317 | |||
| ad34750075 | |||
| cd7e24cd3c | |||
| 0521b7b6f0 | |||
| fc30f91183 | |||
| 8c72ff0565 |
+23
-3
@@ -62,12 +62,30 @@ jobs:
|
||||
(echo '=== RETRY WITH VERBOSE ===' && \
|
||||
PYTHONPATH=$(pwd) /tmp/ci-venv-test/bin/pytest tests/ -m "not e2e" -x -v 2>&1 | tail -30)
|
||||
|
||||
# ── Job 3: CI 失败通知 ───────────────────────────────
|
||||
# ── Job 3: Frontend Build ───────────────────────────
|
||||
frontend:
|
||||
runs-on: macos-arm64
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install & Build
|
||||
run: |
|
||||
cd src/frontend
|
||||
npm ci || npm install
|
||||
npm run build
|
||||
|
||||
# ── Job 4: CI 失败通知 ───────────────────────────────
|
||||
# 使用 needs.<job>.result 直接判断,不查询 commit status API
|
||||
# 根因:notify 自身的 pending status 会污染 commit status 查询结果(竞态条件)
|
||||
notify-on-failure:
|
||||
runs-on: macos-arm64
|
||||
needs: [lint, test]
|
||||
needs: [lint, test, frontend]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check results and notify
|
||||
@@ -75,12 +93,13 @@ jobs:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
LINT_RESULT: ${{ needs.lint.result }}
|
||||
TEST_RESULT: ${{ needs.test.result }}
|
||||
FRONTEND_RESULT: ${{ needs.frontend.result }}
|
||||
run: |
|
||||
echo "Lint result: $LINT_RESULT"
|
||||
echo "Test result: $TEST_RESULT"
|
||||
|
||||
# 只有 lint 或 test 明确失败时才发通知
|
||||
if [ "$LINT_RESULT" = "failure" ] || [ "$TEST_RESULT" = "failure" ]; then
|
||||
if [ "$LINT_RESULT" = "failure" ] || [ "$TEST_RESULT" = "failure" ] || [ "$FRONTEND_RESULT" = "failure" ]; then
|
||||
echo "CI has failures, sending notification..."
|
||||
|
||||
# 如果是 PR 事件,写评论通知
|
||||
@@ -90,6 +109,7 @@ jobs:
|
||||
FAILED_JOBS=""
|
||||
[ "$LINT_RESULT" = "failure" ] && FAILED_JOBS="${FAILED_JOBS}lint "
|
||||
[ "$TEST_RESULT" = "failure" ] && FAILED_JOBS="${FAILED_JOBS}test "
|
||||
[ "$FRONTEND_RESULT" = "failure" ] && FAILED_JOBS="${FAILED_JOBS}frontend "
|
||||
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
|
||||
@@ -110,15 +110,7 @@ jobs:
|
||||
PR_AUTHOR=$(curl --max-time 5 -sf \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"$API_URL/repos/$REPO/pulls?state=closed&sort=updated&order=desc&limit=10" | \
|
||||
python3 -c "
|
||||
import json, sys
|
||||
sha = '$COMMIT_SHA'
|
||||
for pr in json.load(sys.stdin):
|
||||
merge_sha = pr.get('merge_commit_sha', '') or ''
|
||||
if merge_sha.startswith(sha) or sha.startswith(merge_sha):
|
||||
print(pr['user']['login'])
|
||||
break
|
||||
" 2>/dev/null || echo "")
|
||||
python3 -c "import json,sys; sha='$COMMIT_SHA'; matches=[pr['user']['login'] for pr in json.load(sys.stdin) if (pr.get('merge_commit_sha','') or '').startswith(sha) or sha.startswith(pr.get('merge_commit_sha','') or '')]; print(matches[0] if matches else '')" 2>/dev/null || echo "")
|
||||
|
||||
# 确定通知对象
|
||||
if [ -n "$PR_AUTHOR" ]; then
|
||||
|
||||
@@ -208,7 +208,7 @@ class Blackboard:
|
||||
params.append(parent_task)
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
query += " ORDER BY priority ASC, created_at ASC"
|
||||
query += " ORDER BY priority ASC, created_at DESC"
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
return [Task.from_row(r) for r in rows]
|
||||
finally:
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection, DeliveryChecklistSection
|
||||
from src.blackboard.db import get_connection
|
||||
|
||||
logger = logging.getLogger("moziplus-v2.handler.mail")
|
||||
@@ -36,7 +36,7 @@ class MailHandler(BaseTaskHandler):
|
||||
return composer.compose(context)
|
||||
|
||||
def get_sections(self) -> list:
|
||||
return [MailContextSection(), MailApiSection(), MailConstraintsSection(), GiteaConventionSection(), WikiGuideSection()]
|
||||
return [MailContextSection(), MailApiSection(), MailConstraintsSection(), GiteaConventionSection(), WikiGuideSection(), DeliveryChecklistSection()]
|
||||
|
||||
def verify_completion(self, task_id: str, db_path: Path) -> VerifyResult:
|
||||
"""Mail 完成验证:区分 inform/request。
|
||||
|
||||
@@ -174,3 +174,27 @@ class WikiGuideSection:
|
||||
|
||||
def should_include(self, context: "PromptContext") -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DeliveryChecklistSection — 交付检查清单
|
||||
# ---------------------------------------------------------------------------
|
||||
class DeliveryChecklistSection:
|
||||
"""交付检查清单 — 提醒 Agent 完成前同步关联成果物。"""
|
||||
|
||||
name: str = "delivery_checklist"
|
||||
priority: int = 55 # CONSTRAINTS(50) 和 EXTENSION(60) 之间
|
||||
|
||||
CHECKLIST_TEXT = (
|
||||
"## 交付检查\n"
|
||||
"完成代码改动前确认:\n"
|
||||
"- 改了实现 → docs/design/ 对应设计文档是否需要更新\n"
|
||||
"- 改了实现 → tests/ 是否有对应测试脚本需要更新\n"
|
||||
"- 所有成果物变更通过 PR 流程:PR review 把关设计合理性,CI 把关代码质量,CD 把关部署正确性\n"
|
||||
)
|
||||
|
||||
def render(self, context: "PromptContext") -> str:
|
||||
return self.CHECKLIST_TEXT
|
||||
|
||||
def should_include(self, context: "PromptContext") -> bool:
|
||||
return True
|
||||
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection, DeliveryChecklistSection
|
||||
from src.blackboard.db import get_connection
|
||||
|
||||
logger = logging.getLogger("moziplus-v2.handler")
|
||||
@@ -315,6 +315,7 @@ class TaskHandler(BaseTaskHandler):
|
||||
TaskConstraintsSection(),
|
||||
GiteaConventionSection(),
|
||||
WikiGuideSection(),
|
||||
DeliveryChecklistSection(),
|
||||
]
|
||||
|
||||
def build_prompt(self, context: PromptContext) -> str:
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection, DeliveryChecklistSection
|
||||
from src.daemon.toolchain_templates import render_template, _TEMPLATE_MAP
|
||||
from src.blackboard.db import get_connection
|
||||
|
||||
@@ -51,17 +51,41 @@ class ToolchainContextSection:
|
||||
name: str = "toolchain_context"
|
||||
priority: int = 10
|
||||
|
||||
EVENT_LABELS_ZH: Dict[str, str] = {
|
||||
"review_request": "Review 请求",
|
||||
"review_result": "Review 结果",
|
||||
"review_merged": "PR 合并",
|
||||
"review_comment": "Review 评论",
|
||||
"review_updated": "Review 更新",
|
||||
"ci_failure": "CI 失败",
|
||||
"deploy_failure": "部署失败",
|
||||
"issue_assigned": "Issue 指派",
|
||||
"mention": "@提及",
|
||||
}
|
||||
|
||||
def render(self, context: PromptContext) -> str:
|
||||
event_type = context.event_type
|
||||
event_data: Dict = context.event_data or {}
|
||||
|
||||
# 事件类型中文标签
|
||||
event_label = self.EVENT_LABELS_ZH.get(event_type, event_type or '未知')
|
||||
|
||||
# from / to 信息
|
||||
to_agent = context.agent_id or ''
|
||||
from_agent = 'system'
|
||||
|
||||
# Part 1: 事件信息(现有模板引擎)
|
||||
if event_type in _TEMPLATE_MAP:
|
||||
variables = {k: str(v) for k, v in event_data.items()}
|
||||
event_text = render_template(event_type, variables)
|
||||
# 补充事件类型中文标签 + from/to
|
||||
header = f"- **事件类型**: {event_label}\n- **来源**: {from_agent}\n- **指派**: {to_agent}\n"
|
||||
event_text = header + "\n" + event_text
|
||||
else:
|
||||
lines = ["## 工具链事件", ""]
|
||||
lines.append(f"- **事件类型**: {event_type or '未知'}")
|
||||
lines.append(f"- **事件类型**: {event_label}")
|
||||
lines.append(f"- **来源**: {from_agent}")
|
||||
lines.append(f"- **指派**: {to_agent}")
|
||||
if event_data:
|
||||
lines.append("- **事件详情**:")
|
||||
for key, value in event_data.items():
|
||||
@@ -228,6 +252,7 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
ToolchainConstraintsSection(),
|
||||
GiteaConventionSection(),
|
||||
WikiGuideSection(),
|
||||
DeliveryChecklistSection(),
|
||||
]
|
||||
|
||||
def build_prompt(self, context: PromptContext) -> str:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { api, AgentsStatusData } from '../api';
|
||||
import ToolchainPanel from './ToolchainPanel';
|
||||
|
||||
interface ServiceCheckResult {
|
||||
name: string;
|
||||
@@ -15,7 +16,7 @@ interface ServiceCheckResult {
|
||||
}
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const [tab, setTab] = useState<'connections' | 'security' | 'version' | 'logs'>('connections');
|
||||
const [tab, setTab] = useState<'connections' | 'security' | 'version' | 'logs' | 'toolchain'>('connections');
|
||||
|
||||
// 接线状态巡检
|
||||
const [checking, setChecking] = useState(false);
|
||||
@@ -95,6 +96,7 @@ export default function SettingsPanel() {
|
||||
{ key: 'security' as const, label: '🛡️ 安全防务' },
|
||||
{ key: 'version' as const, label: '📦 版本更新' },
|
||||
{ key: 'logs' as const, label: '📋 城防日志' },
|
||||
{ key: 'toolchain' as const, label: '⛓️ 工具链' },
|
||||
].map((t) => (
|
||||
<button key={t.key} className={`btn ${tab === t.key ? 'btn-primary' : ''}`} onClick={() => setTab(t.key)}>
|
||||
{t.label}
|
||||
@@ -288,6 +290,9 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ========== 工具链 ========== */}
|
||||
{tab === 'toolchain' && <ToolchainPanel />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* ToolchainPanel — 工具链事件(系统级)
|
||||
* 展示 _toolchain 项目的 tasks:CI/PR/部署/Review 通知
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const AGENT_NAMES: Record<string, string> = {
|
||||
'pangtong-fujunshi': '庞统',
|
||||
'simayi-challenger': '司马懿',
|
||||
'zhangfei-dev': '张飞',
|
||||
'guanyu-dev': '关羽',
|
||||
'zhaoyun-data': '赵云',
|
||||
'jiangwei-infra': '姜维',
|
||||
'system': '系统',
|
||||
};
|
||||
|
||||
const EVENT_LABELS: Record<string, string> = {
|
||||
'review_request': 'Review 请求',
|
||||
'review_result': 'Review 结果',
|
||||
'review_merged': 'PR 合并',
|
||||
'review_comment': 'Review 评论',
|
||||
'review_updated': 'Review 更新',
|
||||
'ci_failure': 'CI 失败',
|
||||
'deploy_failure': '部署失败',
|
||||
'issue_assigned': 'Issue 指派',
|
||||
'mention': '@提及',
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
pending: '#f59e0b22', claimed: '#6a9eff22', working: '#6a9eff22',
|
||||
review: '#818cf822', done: '#2ecc8a22', failed: '#ef444422',
|
||||
cancelled: '#6b728022', blocked: '#ef444422',
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
pending: '待处理', claimed: '已认领', working: '处理中',
|
||||
review: '审查中', done: '已完成', failed: '失败',
|
||||
cancelled: '已取消', blocked: '已拦截',
|
||||
};
|
||||
|
||||
function fmtTime(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z');
|
||||
const now = Date.now();
|
||||
const diff = now - d.getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return '刚刚';
|
||||
if (mins < 60) return `${mins}分钟前`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}小时前`;
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
} catch { return iso; }
|
||||
}
|
||||
|
||||
export default function ToolchainPanel() {
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [detail, setDetail] = useState<any>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filterMode, setFilterMode] = useState<'all' | 'pending'>('all');
|
||||
|
||||
const loadTasks = async (q?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const url = q
|
||||
? `/api/projects/_toolchain/tasks?q=${encodeURIComponent(q)}`
|
||||
: `/api/projects/_toolchain/tasks`;
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setTasks(data.tasks || []);
|
||||
}
|
||||
} catch { /* */ }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const displayed = filterMode === 'pending'
|
||||
? tasks.filter(t => !['done', 'failed', 'cancelled'].includes(t.status))
|
||||
: tasks;
|
||||
|
||||
useEffect(() => { loadTasks(); }, []);
|
||||
|
||||
// 搜索防抖 300ms
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (searchQuery !== undefined) loadTasks(searchQuery || undefined);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedId) { setDetail(null); return; }
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/projects/_toolchain/tasks/${selectedId}?expand=comments`
|
||||
);
|
||||
if (res.ok) setDetail(await res.json());
|
||||
} catch { /* */ }
|
||||
})();
|
||||
}, [selectedId]);
|
||||
|
||||
// 渲染评论列表(兼容 expand 和裸 list 格式)
|
||||
const renderComments = (comments: any[]) => {
|
||||
if (!comments || comments.length === 0) return null;
|
||||
return (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 8, fontWeight: 600 }}>
|
||||
📋 处理记录 ({comments.length})
|
||||
</div>
|
||||
{comments.map((c: any, i: number) => (
|
||||
<div key={c.id || i} style={{
|
||||
padding: '8px 12px', background: 'var(--panel2)', borderRadius: 6, marginBottom: 6,
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 10, color: 'var(--acc)', fontWeight: 600 }}>
|
||||
{c.author || 'system'}
|
||||
</span>
|
||||
<span style={{ fontSize: 9, color: 'var(--muted)' }}>{fmtTime(c.created_at)}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{c.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 0, height: '100%', minHeight: 500 }}>
|
||||
{/* 左侧列表 */}
|
||||
<div style={{ width: 380, borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column', flexShrink: 0 }}>
|
||||
{/* 搜索栏 + 刷新 */}
|
||||
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--line)', display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索工具链事件..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
flex: 1, padding: '4px 8px', borderRadius: 4, fontSize: 11,
|
||||
border: '1px solid #2a3550', background: '#161b2e', color: '#dde4f8',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => loadTasks(searchQuery || undefined)} style={{
|
||||
padding: '3px 8px', borderRadius: 4, fontSize: 10,
|
||||
border: '1px solid #2a3550', background: '#161b2e', color: '#8899aa', cursor: 'pointer',
|
||||
}}>🔄</button>
|
||||
<button onClick={() => setFilterMode('all')} style={{
|
||||
padding: '3px 8px', borderRadius: 4, fontSize: 10,
|
||||
border: `1px solid ${filterMode === 'all' ? 'var(--acc)' : '#2a3550'}`,
|
||||
background: filterMode === 'all' ? 'var(--acc)22' : '#161b2e',
|
||||
color: filterMode === 'all' ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
|
||||
}}>全部</button>
|
||||
<button onClick={() => setFilterMode('pending')} style={{
|
||||
padding: '3px 8px', borderRadius: 4, fontSize: 10,
|
||||
border: `1px solid ${filterMode === 'pending' ? 'var(--acc)' : '#2a3550'}`,
|
||||
background: filterMode === 'pending' ? 'var(--acc)22' : '#161b2e',
|
||||
color: filterMode === 'pending' ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
|
||||
}}>未处理</button>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{filterMode === 'pending' ? displayed.length : tasks.length} 条</span>
|
||||
</div>
|
||||
|
||||
{/* 事件列表 */}
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
{tasks.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 40, color: 'var(--muted)', fontSize: 12 }}>
|
||||
{loading ? '加载中...' : '暂无工具链事件'}
|
||||
</div>
|
||||
)}
|
||||
{displayed.map((t: any) => (
|
||||
<div key={t.id} onClick={() => setSelectedId(t.id)} style={{
|
||||
padding: '10px 14px', borderBottom: '1px solid var(--line)',
|
||||
cursor: 'pointer', transition: 'background .15s',
|
||||
background: selectedId === t.id ? 'var(--panel2)' : 'transparent',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--panel2)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = selectedId === t.id ? 'var(--panel2)' : 'transparent'}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||
<span style={{
|
||||
fontSize: 9, padding: '1px 5px', borderRadius: 3,
|
||||
background: STATUS_COLORS[t.status] || '#2a3550',
|
||||
color: '#dde4f8',
|
||||
}}>{STATUS_LABELS[t.status] || t.status}</span>
|
||||
<span style={{ fontSize: 9, color: 'var(--muted)' }}>{fmtTime(t.created_at)}</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 12, fontWeight: 500, color: '#dde4f8',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>{t.title}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 2 }}>
|
||||
{AGENT_NAMES['system'] || '系统'} → {AGENT_NAMES[t.assignee] || t.assignee || '?'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧详情 */}
|
||||
<div style={{ flex: 1, padding: '16px 20px', overflowY: 'auto' }}>
|
||||
{!detail ? (
|
||||
<div style={{ textAlign: 'center', padding: 60, color: 'var(--muted)' }}>
|
||||
<div style={{ fontSize: 36, marginBottom: 12 }}>⛓️</div>
|
||||
<div style={{ fontSize: 13 }}>选择一条事件查看详情</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 头部 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: STATUS_COLORS[detail.status] || '#2a3550', color: '#dde4f8' }}>
|
||||
{STATUS_LABELS[detail.status] || detail.status}
|
||||
</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{detail.id}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.3 }}>{detail.title}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 4 }}>
|
||||
{AGENT_NAMES['system'] || '系统'} → {AGENT_NAMES[detail.assignee] || detail.assignee || '?'}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>
|
||||
{fmtTime(detail.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 正文 */}
|
||||
{detail.description && (
|
||||
<div style={{
|
||||
padding: '14px 16px', background: 'var(--panel2)', borderRadius: 10,
|
||||
fontSize: 13, color: '#a0aec0', lineHeight: 1.7, whiteSpace: 'pre-wrap',
|
||||
}}>
|
||||
{detail.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* action_report 评论 — expand 格式 {items, total_count} */}
|
||||
{detail.comments && detail.comments.items && detail.comments.items.length > 0 &&
|
||||
renderComments(detail.comments.items)
|
||||
}
|
||||
{/* 兼容裸 list 格式 */}
|
||||
{detail.comments && Array.isArray(detail.comments) && detail.comments.length > 0 &&
|
||||
renderComments(detail.comments)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user