Merge PR #17: fix transient state reset + ReAct tool guidance + scheme B UI + dev port isolation
Deploy to Production / deploy (push) Waiting to run Details
Test / backend-test (push) Waiting to run Details
Test / frontend-unit (push) Waiting to run Details
Test / api-e2e (push) Waiting to run Details
Test / frontend-e2e (push) Waiting to run Details

This commit is contained in:
chiguyong 2026-07-02 22:25:52 +08:00
commit d17863d01d
45 changed files with 1713 additions and 54249 deletions

View File

@ -0,0 +1,49 @@
# Compound Engineering -- local config
# Copy to .compound-engineering/config.local.yaml in your project root.
# All settings are optional. Invalid values fall through to defaults.
# --- Work delegation (Codex) ---
# work_delegate: codex # codex | false (default: false)
# work_delegate_consent: true # true | false (default: false)
# work_delegate_sandbox: yolo # yolo | full-auto (default: yolo)
# work_delegate_decision: auto # auto | ask (default: auto)
# work_delegate_model: gpt-5.4 # any valid codex model (omit to use ~/.codex/config.toml default)
# work_delegate_effort: high # minimal | low | medium | high | xhigh (omit to use ~/.codex/config.toml default)
# --- Product pulse ---
# Settings written by /ce-product-pulse first-run interview. Re-run the skill with
# argument `setup` or `reconfigure` to edit interactively.
# pulse_product_name: "Spiral" # used in report titles (no default)
# pulse_lookback_default: 24h # 1h | 24h | 7d | 30d (default: 24h)
# pulse_primary_event: "session_started" # the event that means "user showed up"
# pulse_value_event: "task_completed" # the event that means "user got value"
# pulse_completion_events: "onboarded,first_purchase" # comma-separated, 0-3 events
# pulse_quality_scoring: false # true | false (default: false; AI products only)
# pulse_quality_dimension: "answer accuracy" # dimension scored 1-5 when pulse_quality_scoring is true
# pulse_analytics_source: posthog # posthog | mixpanel | custom (no default)
# pulse_tracing_source: sentry # sentry | datadog | custom (no default)
# pulse_payments_source: stripe # stripe | custom (no default)
# pulse_db_enabled: false # true | false (default: false; read-only DB if true)
# pulse_metric_sources: "retention_d7=posthog,nps=delighted" # strategy-metric -> source overrides; comma-separated 'metric=source' pairs; unlisted metrics fall back to pulse_analytics_source
# pulse_pending_metrics: "retention_d7,nps" # comma-separated strategy metrics awaiting instrumentation; render as 'no data'
# pulse_excluded_metrics: "north_star" # comma-separated strategy metrics intentionally not in pulse
# --- Output format ---
# Per-skill output format default. Selects the exclusive format the artifact
# is written in: `md` produces a markdown file, `html` produces a single
# self-contained HTML file. The two are mutually exclusive -- there is no
# sibling artifact. See DESIGN.md or your agent instructions to influence
# HTML styling. CLI arguments override these defaults; pipeline contexts
# (e.g., LFG, disable-model-invocation) always force `md` regardless.
# plan_output: html # md | html (default: md)
# brainstorm_output: html # md | html (default: md)
# ideate_output: md # md | html (default: html -- ideation docs are human-facing, so HTML is the default; set md to opt out)
# --- ce-promote ---
# Written automatically when you decline the Spiral setup offer in /ce-promote.
# Suppresses that one-time setup nudge in this project. Remove the key to re-enable.
# ce_promote_spiral_optout: true # true | (absent) (default: absent -- offer once)

9
.gitignore vendored
View File

@ -44,6 +44,8 @@ src/agentkit/server/static/
# Env # Env
.env .env
.env.dev
.env.local
# Runtime data (auth DB, conversation DB, etc.) # Runtime data (auth DB, conversation DB, etc.)
data/ data/
@ -52,3 +54,10 @@ data/
.agents/ .agents/
.trae/ .trae/
.aider* .aider*
# Knowledge graph tooling (local-only, generated index)
.understand-anything/
# Local temp files
tmp_*.html
/delete_old_cluster.sh

View File

@ -1,315 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fischer AgentKit - Knowledge Graph Dashboard</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; overflow: hidden; height: 100vh; }
.header { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #334155; }
.header h1 { font-size: 18px; font-weight: 600; background: linear-gradient(135deg, #60a5fa, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stats { display: flex; gap: 16px; font-size: 13px; color: #94a3b8; }
.stats span { padding: 4px 10px; background: #1e293b; border-radius: 6px; border: 1px solid #334155; }
.main { display: flex; height: calc(100vh - 52px); }
.sidebar { width: 340px; background: #1e293b; border-right: 1px solid #334155; display: flex; flex-direction: column; overflow: hidden; }
.search-box { padding: 12px; border-bottom: 1px solid #334155; }
.search-box input { width: 100%; padding: 8px 12px; background: #0f172a; border: 1px solid #475569; border-radius: 8px; color: #e2e8f0; font-size: 14px; outline: none; }
.search-box input:focus { border-color: #60a5fa; }
.tabs { display: flex; border-bottom: 1px solid #334155; }
.tab { flex: 1; padding: 8px; text-align: center; font-size: 13px; cursor: pointer; color: #94a3b8; border-bottom: 2px solid transparent; transition: all 0.2s; }
.tab.active { color: #60a5fa; border-bottom-color: #60a5fa; }
.tab:hover { color: #e2e8f0; }
.content { flex: 1; overflow-y: auto; padding: 8px; }
.content::-webkit-scrollbar { width: 6px; }
.content::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
.node-item { padding: 8px 12px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; transition: background 0.15s; border: 1px solid transparent; }
.node-item:hover { background: #334155; border-color: #475569; }
.node-item.selected { background: #1e3a5f; border-color: #60a5fa; }
.node-item .name { font-size: 13px; font-weight: 500; color: #e2e8f0; }
.node-item .meta { font-size: 11px; color: #64748b; margin-top: 2px; }
.badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; margin-left: 6px; }
.badge.file { background: #1e3a5f; color: #60a5fa; }
.badge.class { background: #3b1f5e; color: #c084fc; }
.badge.function { background: #1a3d2e; color: #4ade80; }
.badge.api { background: #7c2d12; color: #fb923c; }
.badge.service { background: #1e3a5f; color: #60a5fa; }
.badge.data { background: #3b1f5e; color: #c084fc; }
.badge.utility { background: #1a3d2e; color: #4ade80; }
.graph-container { flex: 1; position: relative; }
#graph { width: 100%; height: 100%; }
.detail-panel { position: absolute; right: 16px; top: 16px; width: 320px; background: rgba(30,41,59,0.95); border: 1px solid #475569; border-radius: 12px; padding: 16px; display: none; backdrop-filter: blur(12px); max-height: calc(100vh - 100px); overflow-y: auto; }
.detail-panel.show { display: block; }
.detail-panel h3 { font-size: 15px; color: #60a5fa; margin-bottom: 8px; }
.detail-panel .summary { font-size: 13px; color: #cbd5e1; line-height: 1.5; margin-bottom: 12px; }
.detail-panel .field { font-size: 12px; color: #94a3b8; margin-bottom: 6px; }
.detail-panel .field strong { color: #e2e8f0; }
.detail-panel .edges-list { margin-top: 8px; }
.detail-panel .edge-item { font-size: 12px; padding: 4px 8px; background: #0f172a; border-radius: 4px; margin-bottom: 3px; cursor: pointer; }
.detail-panel .edge-item:hover { background: #334155; }
.tour-panel { padding: 8px; }
.tour-card { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.2s; }
.tour-card:hover { border-color: #60a5fa; }
.tour-card h4 { font-size: 13px; color: #e2e8f0; margin-bottom: 4px; }
.tour-card p { font-size: 11px; color: #64748b; }
.tour-steps { margin-top: 8px; }
.tour-step { display: flex; align-items: center; gap: 8px; padding: 6px 8px; font-size: 12px; color: #94a3b8; border-radius: 4px; cursor: pointer; }
.tour-step:hover { background: #334155; color: #e2e8f0; }
.tour-step .num { width: 20px; height: 20px; background: #334155; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #60a5fa; flex-shrink: 0; }
.layer-filter { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 12px; border-bottom: 1px solid #334155; }
.layer-btn { padding: 3px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; border: 1px solid #475569; background: transparent; color: #94a3b8; transition: all 0.15s; }
.layer-btn.active { background: #334155; color: #e2e8f0; border-color: #60a5fa; }
.close-btn { position: absolute; top: 8px; right: 8px; background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 16px; }
.close-btn:hover { color: #e2e8f0; }
.loading { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 16px; color: #60a5fa; }
</style>
</head>
<body>
<div class="header">
<h1>Fischer AgentKit Knowledge Graph</h1>
<div class="stats" id="stats"></div>
</div>
<div class="main">
<div class="sidebar">
<div class="search-box">
<input type="text" id="search" placeholder="Search nodes..." />
</div>
<div class="layer-filter" id="layerFilter"></div>
<div class="tabs">
<div class="tab active" data-tab="nodes">Nodes</div>
<div class="tab" data-tab="tours">Tours</div>
</div>
<div class="content" id="nodeList"></div>
<div class="content" id="tourList" style="display:none"></div>
</div>
<div class="graph-container">
<div id="graph"></div>
<div class="detail-panel" id="detailPanel">
<button class="close-btn" onclick="document.getElementById('detailPanel').classList.remove('show')">&times;</button>
<div id="detailContent"></div>
</div>
</div>
</div>
<script>
let graphData = null;
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let activeLayers = new Set(['api', 'service', 'data', 'utility', 'unknown']);
let selectedNodeId = null;
const LAYER_COLORS = {
api: { bg: '#fb923c', border: '#ea580c', highlight: '#fdba74' },
service: { bg: '#60a5fa', border: '#2563eb', highlight: '#93c5fd' },
data: { bg: '#c084fc', border: '#9333ea', highlight: '#d8b4fe' },
utility: { bg: '#4ade80', border: '#16a34a', highlight: '#86efac' },
unknown: { bg: '#94a3b8', border: '#64748b', highlight: '#cbd5e1' }
};
const TYPE_SHAPES = { file: 'box', class: 'diamond', function: 'dot' };
async function loadGraph() {
try {
const resp = await fetch('knowledge-graph.json');
graphData = await resp.json();
initDashboard();
} catch(e) {
document.getElementById('graph').innerHTML = '<div class="loading">Failed to load knowledge-graph.json</div>';
}
}
function initDashboard() {
const { nodes, edges, tours, project } = graphData;
document.getElementById('stats').innerHTML = `
<span>${nodes.length} Nodes</span>
<span>${edges.length} Edges</span>
<span>${tours.length} Tours</span>
<span>${project.languages.join(', ')}</span>
`;
renderLayerFilter(nodes);
renderNodeList(nodes);
renderTourList(tours);
initGraph(nodes, edges);
initSearch(nodes);
initTabs();
}
function renderLayerFilter(nodes) {
const layers = [...new Set(nodes.map(n => n.layer).filter(Boolean))];
const container = document.getElementById('layerFilter');
container.innerHTML = layers.map(l =>
`<button class="layer-btn active" data-layer="${l}">${l}</button>`
).join('');
container.querySelectorAll('.layer-btn').forEach(btn => {
btn.onclick = () => {
const layer = btn.dataset.layer;
if (activeLayers.has(layer)) { activeLayers.delete(layer); btn.classList.remove('active'); }
else { activeLayers.add(layer); btn.classList.add('active'); }
filterGraph();
};
});
}
function renderNodeList(nodes) {
const container = document.getElementById('nodeList');
const filtered = nodes.filter(n => activeLayers.has(n.layer));
container.innerHTML = filtered.slice(0, 200).map(n => `
<div class="node-item" data-id="${n.id}">
<div class="name">${n.name} <span class="badge ${n.type}">${n.type}</span> <span class="badge ${n.layer}">${n.layer}</span></div>
<div class="meta">${n.filePath || ''}</div>
</div>
`).join('');
container.querySelectorAll('.node-item').forEach(el => {
el.onclick = () => focusNode(el.dataset.id);
});
}
function renderTourList(tours) {
const container = document.getElementById('tourList');
container.innerHTML = tours.map((t, i) => `
<div class="tour-card" data-tour="${i}">
<h4>${t.name}</h4>
<p>${t.description}</p>
<div class="tour-steps">
${t.steps.map((s, j) => `
<div class="tour-step" data-node="${s.nodeId}">
<span class="num">${j+1}</span>
<span>${s.why || s.nodeId}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
container.querySelectorAll('.tour-step').forEach(el => {
el.onclick = (e) => { e.stopPropagation(); focusNode(el.dataset.node); };
});
}
function initGraph(nodes, edges) {
const visNodes = nodes.filter(n => activeLayers.has(n.layer)).map(n => {
const colors = LAYER_COLORS[n.layer] || LAYER_COLORS.unknown;
return {
id: n.id,
label: n.name,
shape: TYPE_SHAPES[n.type] || 'dot',
color: colors,
font: { color: '#e2e8f0', size: n.type === 'file' ? 12 : 10 },
size: n.type === 'file' ? 20 : n.type === 'class' ? 15 : 8,
title: `${n.name}\n${n.summary || ''}\n[${n.layer}]`,
...n
};
});
const nodeIds = new Set(visNodes.map(n => n.id));
const visEdges = edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target)).map((e, i) => ({
id: e.id || `edge-${i}`,
from: e.source,
to: e.target,
arrows: e.type === 'contains' ? '' : 'to',
color: { color: '#334155', highlight: '#60a5fa', hover: '#475569' },
width: 0.5,
dashes: e.type === 'imports',
title: e.type
}));
nodesDataset = new vis.DataSet(visNodes);
edgesDataset = new vis.DataSet(visEdges);
const container = document.getElementById('graph');
const data = { nodes: nodesDataset, edges: edgesDataset };
const options = {
physics: { barnesHut: { gravitationalConstant: -3000, centralGravity: 0.3, springLength: 80, springConstant: 0.04 }, stabilization: { iterations: 100 } },
interaction: { hover: true, tooltipDelay: 200, navigationButtons: true, keyboard: true },
layout: { improvedLayout: true }
};
network = new vis.Network(container, data, options);
network.on('click', params => {
if (params.nodes.length > 0) focusNode(params.nodes[0]);
});
}
function filterGraph() {
const nodes = graphData.nodes.filter(n => activeLayers.has(n.layer));
renderNodeList(nodes);
if (nodesDataset) {
const nodeIds = new Set(nodes.map(n => n.id));
const edges = graphData.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
nodesDataset.clear();
nodesDataset.add(nodes.map(n => {
const colors = LAYER_COLORS[n.layer] || LAYER_COLORS.unknown;
return { id: n.id, label: n.name, shape: TYPE_SHAPES[n.type] || 'dot', color: colors, font: { color: '#e2e8f0', size: n.type === 'file' ? 12 : 10 }, size: n.type === 'file' ? 20 : n.type === 'class' ? 15 : 8, title: `${n.name}\n${n.summary || ''}\n[${n.layer}]`, ...n };
}));
edgesDataset.clear();
edgesDataset.add(edges.map((e, i) => ({ id: e.id || `edge-${i}`, from: e.source, to: e.target, arrows: e.type === 'contains' ? '' : 'to', color: { color: '#334155', highlight: '#60a5fa', hover: '#475569' }, width: 0.5, dashes: e.type === 'imports', title: e.type })));
}
}
function focusNode(nodeId) {
selectedNodeId = nodeId;
const node = graphData.nodes.find(n => n.id === nodeId);
if (!node) return;
document.querySelectorAll('.node-item').forEach(el => el.classList.remove('selected'));
const el = document.querySelector(`.node-item[data-id="${nodeId}"]`);
if (el) { el.classList.add('selected'); el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
if (network) {
network.focus(nodeId, { scale: 1.5, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
network.selectNodes([nodeId]);
}
const inEdges = graphData.edges.filter(e => e.target === nodeId);
const outEdges = graphData.edges.filter(e => e.source === nodeId);
const panel = document.getElementById('detailPanel');
const content = document.getElementById('detailContent');
content.innerHTML = `
<h3>${node.name}</h3>
<div class="summary">${node.summary || 'No summary'}</div>
<div class="field"><strong>Type:</strong> ${node.type}</div>
<div class="field"><strong>Layer:</strong> ${node.layer}</div>
<div class="field"><strong>Path:</strong> ${node.filePath || '-'}</div>
<div class="field"><strong>Complexity:</strong> ${node.complexity || '-'}</div>
${node.tags ? `<div class="field"><strong>Tags:</strong> ${node.tags.join(', ')}</div>` : ''}
${inEdges.length > 0 ? `<div class="edges-list"><strong>Incoming (${inEdges.length}):</strong>${inEdges.slice(0,10).map(e => `<div class="edge-item" onclick="focusNode('${e.source}')">${e.type} ← ${e.source.split(':').pop()}</div>`).join('')}</div>` : ''}
${outEdges.length > 0 ? `<div class="edges-list"><strong>Outgoing (${outEdges.length}):</strong>${outEdges.slice(0,10).map(e => `<div class="edge-item" onclick="focusNode('${e.target}')">${e.type} → ${e.target.split(':').pop()}</div>`).join('')}</div>` : ''}
`;
panel.classList.add('show');
}
function initSearch(nodes) {
const input = document.getElementById('search');
input.oninput = () => {
const q = input.value.toLowerCase();
const filtered = nodes.filter(n =>
activeLayers.has(n.layer) &&
(n.name.toLowerCase().includes(q) || (n.summary || '').toLowerCase().includes(q) || (n.filePath || '').toLowerCase().includes(q))
);
renderNodeList(filtered);
if (q.length > 1 && network) {
const matchIds = filtered.map(n => n.id);
if (matchIds.length > 0 && matchIds.length < 50) {
network.fit({ nodes: matchIds, animation: { duration: 500 } });
}
}
};
}
function initTabs() {
document.querySelectorAll('.tab').forEach(tab => {
tab.onclick = () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const target = tab.dataset.tab;
document.getElementById('nodeList').style.display = target === 'nodes' ? 'block' : 'none';
document.getElementById('tourList').style.display = target === 'tours' ? 'block' : 'none';
};
});
}
loadGraph();
</script>
</body>
</html>

View File

@ -1,526 +0,0 @@
{
"configs/__init__.py": "c177bc3f2e94623f",
"configs/geo_handlers.py": "4df68c7922568aab",
"configs/geo_server.py": "189e38838336cad4",
"configs/geo_tools.py": "3c442267d598dc1c",
"docs/GEO-INTEGRATION-GUIDE.md": "bf3dfb02cf6846e1",
"docs/brainstorms/2026-06-05-agentkit-architecture-gap-analysis-requirements.md": "eb46b87f5cdca0fa",
"docs/brainstorms/2026-06-09-agentkit-capability-matrix/plan.md": "5829f1e545a08f0f",
"docs/brainstorms/2026-06-09-agentkit-capability-matrix/requirements.md": "50e4f91f200089a8",
"docs/brainstorms/2026-06-09-clawith-research-prompt.md": "82cbce39736c76dc",
"docs/brainstorms/2026-06-12-frontend-productization-requirements.md": "41bb5034978e107c",
"docs/brainstorms/2026-06-13-agentkit-platform-experience-upgrade-requirements.md": "97b00dd54e012bfd",
"docs/brainstorms/2026-06-13-gui-productization-requirements.md": "501f89238bc6e504",
"docs/brainstorms/2026-06-13-gui-redesign-requirements.md": "e703c3a1dff253c8",
"docs/plans/2026-06-05-001-feat-agentkit-tdd-validation-plan.md": "cb31514b78616c90",
"docs/plans/2026-06-05-002-design-agentkit-v2-architecture.md": "e64efa0d464352d6",
"docs/plans/2026-06-05-003-feat-agentkit-v2-phase1-plan.md": "8bfaed59f42e58eb",
"docs/plans/2026-06-05-004-geo-migration-mode-a.md": "552e179027bd15cc",
"docs/plans/2026-06-05-005-refactor-agentkit-framework-hardening.md": "00cf6aa686fbe699",
"docs/plans/2026-06-05-006-refactor-agentkit-v2-phase2-plan.md": "c58751ee3720d3f1",
"docs/plans/2026-06-05-007-feat-agentkit-cli-deployment-plan.md": "c21d783c264ce1dc",
"docs/plans/2026-06-06-008-feat-agentkit-phase3-upgrade-plan.md": "1ba1a31257aeb6ee",
"docs/plans/2026-06-06-009-feat-agentkit-rag-optimization-plan.md": "596b971c39b92374",
"docs/plans/2026-06-06-010-feat-agentkit-phase4-production-plan.md": "f5443a0b04f80eca",
"docs/plans/2026-06-06-011-feat-agentkit-phase5-intelligence-plan.md": "35083bba9d52c3a4",
"docs/plans/2026-06-07-012-feat-agentkit-phase6-toolkit-plan.md": "110616fdbf34b501",
"docs/plans/2026-06-07-013-feat-agentkit-phase7-headroom-plan.md": "c4a730d9d1b1e3bc",
"docs/plans/2026-06-07-014-fix-agentkit-p0-review-fixes-plan.md": "4b62eee79a38f8c0",
"docs/plans/2026-06-07-015-feat-agentkit-phase8-chat-adaptive-plan.md": "d9730989b4e1056b",
"docs/plans/2026-06-08-016-feat-agentkit-layered-memory-plan.md": "0fbb6ba2badbd690",
"docs/plans/2026-06-09-017-feat-agentkit-multi-agent-marketplace-plan.md": "125227b9fab2f7cf",
"docs/plans/2026-06-10-018-fix-agentkit-p2-hardening-plan.md": "fb2b2de97bc5d108",
"docs/plans/2026-06-10-019-feat-agentkit-deferred-improvements-plan.md": "d59405e0d5660cbc",
"docs/plans/2026-06-12-020-feat-pipeline-adversarial-loop-plan.md": "8d857040371b14c9",
"docs/plans/2026-06-12-021-feat-chat-response-speed-optimization-plan.md": "9a5e58432a3b9bbd",
"docs/plans/2026-06-12-022-feat-agentkit-phase9-integrated-next-stage-plan.md": "8a18b30c7a7d4976",
"docs/plans/2026-06-12-023-feat-frontend-productization-plan.md": "c17f31ebce588103",
"docs/plans/2026-06-13-001-feat-gui-productization-plan.md": "924d3081d6a4c328",
"docs/plans/2026-06-13-001-refactor-gui-redesign-plan.md": "5ec64af7d790295e",
"docs/plans/2026-06-13-003-feat-platform-experience-upgrade-plan.md": "354b625271f06f5e",
"docs/plans/2026-06-13-004-feat-tauri-desktop-client-plan.md": "c9afebfb3dae8a90",
"docs/plans/2026-06-14-001-feat-p0-production-hardening-plan.md": "29737af17bfd9ade",
"docs/plans/2026-06-14-002-u1-llm-cache-architecture.md": "71feabd02ad95169",
"docs/plans/2026-06-14-003-u2-llm-cache-integration.md": "853bfdf22312dbae",
"docs/plans/2026-06-14-004-u3-semantic-router.md": "6c765cbc5140be53",
"src/agentkit/__init__.py": "8f3077a792be01bf",
"src/agentkit/__main__.py": "a79cda859a4ff1e4",
"src/agentkit/bus/__init__.py": "46dddb662dbc4455",
"src/agentkit/bus/interface.py": "4172d55fa0e96410",
"src/agentkit/bus/memory_bus.py": "993b110d1aa1bf8b",
"src/agentkit/bus/message.py": "21093d396668686e",
"src/agentkit/bus/protocol.py": "ceb9511a718981ec",
"src/agentkit/bus/redis_bus.py": "b18f9d14abb106a4",
"src/agentkit/chat/__init__.py": "d41d8cd98f00b204",
"src/agentkit/chat/semantic_router.py": "ce5293262dc5fc6d",
"src/agentkit/chat/skill_routing.py": "4edfe82920e62b29",
"src/agentkit/cli/__init__.py": "6be0f88bfb1933cd",
"src/agentkit/cli/chat.py": "555a58858d7ac531",
"src/agentkit/cli/init.py": "c941e7e24b524414",
"src/agentkit/cli/main.py": "2f67646566ddbba2",
"src/agentkit/cli/onboarding.py": "c62b4f1b5508b05f",
"src/agentkit/cli/pair.py": "b796381a116076a3",
"src/agentkit/cli/skill.py": "513a7848cf995b79",
"src/agentkit/cli/task.py": "19ba46de4dfe86ee",
"src/agentkit/cli/templates.py": "3e3ea04125ac45dd",
"src/agentkit/cli/usage.py": "988272ab7ffb34ff",
"src/agentkit/core/__init__.py": "6e1420ad43fe4f94",
"src/agentkit/core/agent_pool.py": "38c413cc11f0be6c",
"src/agentkit/core/base.py": "330fbf17f4dfa01b",
"src/agentkit/core/compressor.py": "eae7a723d1b55bc3",
"src/agentkit/core/config_driven.py": "7592e9094dcbfa8b",
"src/agentkit/core/dispatcher.py": "a032ac64cc6d88e2",
"src/agentkit/core/exceptions.py": "a2e3376e0b06c6df",
"src/agentkit/core/goal_planner.py": "85a82ad127be83df",
"src/agentkit/core/headroom_compressor.py": "79691d95f00a9f2c",
"src/agentkit/core/logging.py": "4ca908eac76f4487",
"src/agentkit/core/orchestrator.py": "0b73a7612bf0d5fb",
"src/agentkit/core/plan_checker.py": "da4b29c79546f1ef",
"src/agentkit/core/plan_exec_engine.py": "5e78dab734dbd7a5",
"src/agentkit/core/plan_executor.py": "4e145a4903d2c159",
"src/agentkit/core/plan_schema.py": "5516e66e2a5241a6",
"src/agentkit/core/protocol.py": "6cfa0bfb01ee29f3",
"src/agentkit/core/react.py": "77124fe27b73d5fa",
"src/agentkit/core/reflexion.py": "23f90a739bfdb96b",
"src/agentkit/core/registry.py": "537905f014c67bc9",
"src/agentkit/core/rewoo.py": "3ad60b5f4015b434",
"src/agentkit/core/shared_workspace.py": "a847f5a879c5c551",
"src/agentkit/core/standalone.py": "aa0bf44a1b1649a6",
"src/agentkit/core/trace.py": "b6a4f8cebca7d594",
"src/agentkit/evaluation/__init__.py": "a330342ce4ca36e5",
"src/agentkit/evaluation/ragas_evaluator.py": "8f8d3f013d02e8b4",
"src/agentkit/evolution/__init__.py": "723b5130fb48b695",
"src/agentkit/evolution/ab_tester.py": "7841dff79e521e81",
"src/agentkit/evolution/evolution_store.py": "af3444081d9daa98",
"src/agentkit/evolution/experience_schema.py": "d2c096f18e1699ef",
"src/agentkit/evolution/experience_store.py": "d58a3d12c5bc9b2e",
"src/agentkit/evolution/fitness.py": "cc3a200bc1a45f8c",
"src/agentkit/evolution/genetic.py": "f688039da416f456",
"src/agentkit/evolution/lifecycle.py": "ec1fe00da015f086",
"src/agentkit/evolution/llm_reflector.py": "11a57b403186159b",
"src/agentkit/evolution/models.py": "f4431b403fb631be",
"src/agentkit/evolution/path_optimizer.py": "e14acc297c384e1f",
"src/agentkit/evolution/pg_store.py": "7d73520b6aaf6520",
"src/agentkit/evolution/pitfall_detector.py": "b20426d9c8ad32f0",
"src/agentkit/evolution/prompt_optimizer.py": "55d6803ff662373f",
"src/agentkit/evolution/reflector.py": "3bd2e26da20cf83f",
"src/agentkit/evolution/strategy_tuner.py": "bda557828a67e072",
"src/agentkit/llm/__init__.py": "82c7e6de6d66bb43",
"src/agentkit/llm/cache.py": "12e3ca6b88970d1b",
"src/agentkit/llm/cache_key.py": "647aba5b414ee210",
"src/agentkit/llm/config.py": "c6248208496fa3aa",
"src/agentkit/llm/gateway.py": "7725c78f65c86b22",
"src/agentkit/llm/protocol.py": "e37a2f29cc468686",
"src/agentkit/llm/providers/__init__.py": "782e0bfe4705d0a9",
"src/agentkit/llm/providers/anthropic.py": "bb56d751393249f0",
"src/agentkit/llm/providers/doubao.py": "15a834e0808a9243",
"src/agentkit/llm/providers/gemini.py": "d8531fc268d702d2",
"src/agentkit/llm/providers/openai.py": "33a7e10d8fc09e47",
"src/agentkit/llm/providers/tracker.py": "949dab590baf4768",
"src/agentkit/llm/providers/usage_store.py": "d7e8bf9cd92e3f70",
"src/agentkit/llm/providers/wenxin.py": "3748b70a13c1463f",
"src/agentkit/llm/providers/yuanbao.py": "9fcc4b6ad8ac31b4",
"src/agentkit/llm/retry.py": "2aa47e585f77dd50",
"src/agentkit/marketplace/__init__.py": "7eba735388883e2d",
"src/agentkit/marketplace/auction.py": "593df1eb74b19149",
"src/agentkit/marketplace/wealth.py": "49b6d56a6dc938b9",
"src/agentkit/mcp/__init__.py": "c043d1a081979781",
"src/agentkit/mcp/client.py": "287662e5fa494e0f",
"src/agentkit/mcp/manager.py": "0652af492b6d0d02",
"src/agentkit/mcp/server.py": "b2689b6ff79e98ec",
"src/agentkit/mcp/transport.py": "6c509f28b30eeb93",
"src/agentkit/memory/__init__.py": "899e6e94f549d9ee",
"src/agentkit/memory/adapters/__init__.py": "d80a08cc9de5c4e5",
"src/agentkit/memory/adapters/base.py": "045ccec94443abb2",
"src/agentkit/memory/adapters/confluence.py": "13e76eb173bfaf3d",
"src/agentkit/memory/adapters/feishu.py": "011d187d101c942b",
"src/agentkit/memory/adapters/generic_http.py": "ecd08ccaf23fb6a9",
"src/agentkit/memory/base.py": "da502f486f860246",
"src/agentkit/memory/chunking.py": "7cdd1dcb43eaaecd",
"src/agentkit/memory/contextual_retrieval.py": "5c192a0c86b7fc45",
"src/agentkit/memory/document_loader.py": "b9af5438034e1450",
"src/agentkit/memory/embedder.py": "1cf207b86c87fb9f",
"src/agentkit/memory/episodic.py": "d9eadf7068d02985",
"src/agentkit/memory/http_rag.py": "c115b197f512896d",
"src/agentkit/memory/knowledge_base.py": "594e0601119ba926",
"src/agentkit/memory/local_rag.py": "35c473b15ce6ba0f",
"src/agentkit/memory/models.py": "1ed936d411b508b2",
"src/agentkit/memory/multi_source_retriever.py": "a528a4d316d9b1a7",
"src/agentkit/memory/profile.py": "8d1144e61dc41ab0",
"src/agentkit/memory/query_transformer.py": "4ed2930eba6ffa32",
"src/agentkit/memory/rag_loop.py": "9f22171fd1ad1e4c",
"src/agentkit/memory/relevance_scorer.py": "bd5bde3493c1d88f",
"src/agentkit/memory/retriever.py": "3d124234ea5f6bc2",
"src/agentkit/memory/semantic.py": "d8330a3242202690",
"src/agentkit/memory/working.py": "2c23cc7e7311fca8",
"src/agentkit/orchestrator/__init__.py": "c3fc598ee58f6f8b",
"src/agentkit/orchestrator/compensation.py": "439752d48824ac1a",
"src/agentkit/orchestrator/dynamic_pipeline.py": "a71d105fd793c873",
"src/agentkit/orchestrator/handoff.py": "088b49d79a7d8ce5",
"src/agentkit/orchestrator/pipeline_engine.py": "2aa54170ff33511f",
"src/agentkit/orchestrator/pipeline_loader.py": "367bdc2921b95136",
"src/agentkit/orchestrator/pipeline_models.py": "6a565353eca205aa",
"src/agentkit/orchestrator/pipeline_schema.py": "7002a77b7bd837b6",
"src/agentkit/orchestrator/pipeline_state.py": "61e7b05b8ff7d7c3",
"src/agentkit/orchestrator/reflection.py": "ffb779fa4ee52e4d",
"src/agentkit/orchestrator/retry.py": "d3664f5702da0891",
"src/agentkit/orchestrator/workflow_schema.py": "304c14f9107e18d3",
"src/agentkit/org/__init__.py": "cfa58426f7f486f6",
"src/agentkit/org/context.py": "2f2e35245c8d460d",
"src/agentkit/org/discovery.py": "ceb833baf51b62ff",
"src/agentkit/prompts/__init__.py": "3edd02e1768b0daa",
"src/agentkit/prompts/section.py": "f1d167a2f0abebf5",
"src/agentkit/prompts/template.py": "717673ca624bbe25",
"src/agentkit/quality/__init__.py": "1cd7368784872d72",
"src/agentkit/quality/alignment.py": "f252282b05baa04d",
"src/agentkit/quality/cascade_detector.py": "62ceb3c9a6ea94f5",
"src/agentkit/quality/cascade_state_store.py": "3e3afeabf789b676",
"src/agentkit/quality/gate.py": "8f615904f6ef877e",
"src/agentkit/quality/output.py": "9f596461e85908fd",
"src/agentkit/router/__init__.py": "6b1080a95c77611c",
"src/agentkit/router/intent.py": "0e8cb267be40071c",
"src/agentkit/server/__init__.py": "e6ba7d89409ef8bc",
"src/agentkit/server/app.py": "b300b1e0afdc3d3d",
"src/agentkit/server/client.py": "d18b1a046e59ca55",
"src/agentkit/server/client_config.py": "91e05d25245e90cf",
"src/agentkit/server/config.py": "b45512d0b1b53073",
"src/agentkit/server/frontend/components.d.ts": "5f476f813a2598b2",
"src/agentkit/server/frontend/env.d.ts": "7e784bd82b37b057",
"src/agentkit/server/frontend/package.json": "64e85b8f27c88709",
"src/agentkit/server/frontend/src/App.vue": "cef09b6f2dae1f21",
"src/agentkit/server/frontend/src/api/base.ts": "8f917711ede5bf8e",
"src/agentkit/server/frontend/src/api/client.ts": "faf1c13362837642",
"src/agentkit/server/frontend/src/api/evolution.ts": "a8369e1c5f2d53a1",
"src/agentkit/server/frontend/src/api/kb.ts": "2d3ea26a96f0666d",
"src/agentkit/server/frontend/src/api/settings.ts": "7c8523fcbd08e9cc",
"src/agentkit/server/frontend/src/api/skills.ts": "83fc22d3ab317669",
"src/agentkit/server/frontend/src/api/tauri.ts": "b4da451ccb551f44",
"src/agentkit/server/frontend/src/api/terminal.ts": "c3566c3ef0db60e8",
"src/agentkit/server/frontend/src/api/types.ts": "1b1ee4f23c38e580",
"src/agentkit/server/frontend/src/api/workflow.ts": "e96945651f30a5e9",
"src/agentkit/server/frontend/src/components/chat/ChatInput.vue": "afdebfb4570dcbee",
"src/agentkit/server/frontend/src/components/chat/ChatMessage.vue": "06a2d6cb671e68a2",
"src/agentkit/server/frontend/src/components/chat/ChatSidebar.vue": "cc6f7b8bbdcf449a",
"src/agentkit/server/frontend/src/components/chat/ContextPill.vue": "330ed22357ce7616",
"src/agentkit/server/frontend/src/components/chat/FilePreview.vue": "defd56ba76c588a0",
"src/agentkit/server/frontend/src/components/chat/MentionDropdown.vue": "4c7d84ff26d7d899",
"src/agentkit/server/frontend/src/components/chat/ToolCallCard.vue": "7081a83bd23f2414",
"src/agentkit/server/frontend/src/components/chat/ToolCallIndicator.vue": "9431bf6cc2e76df8",
"src/agentkit/server/frontend/src/components/code/CodeDiffViewer.vue": "6d668833fa45c2fd",
"src/agentkit/server/frontend/src/components/code/FileTree.vue": "7ccd0d7f03c1c8c4",
"src/agentkit/server/frontend/src/components/evolution/DashboardOverview.vue": "a1961bfd0b32228d",
"src/agentkit/server/frontend/src/components/evolution/ExperiencePanel.vue": "21fcfabd936c32f5",
"src/agentkit/server/frontend/src/components/evolution/ExperienceTimeline.vue": "d626e873a11e8c08",
"src/agentkit/server/frontend/src/components/evolution/MetricsChart.vue": "06767d5c02e37823",
"src/agentkit/server/frontend/src/components/evolution/MetricsPanel.vue": "66e873ddf0d0c542",
"src/agentkit/server/frontend/src/components/evolution/OptimizationPanel.vue": "c0e148ad6648aab3",
"src/agentkit/server/frontend/src/components/evolution/PathOptimizerPanel.vue": "bcc00116179846ba",
"src/agentkit/server/frontend/src/components/evolution/PitfallPanel.vue": "f7c32f866c78f081",
"src/agentkit/server/frontend/src/components/evolution/PitfallRoutePanel.vue": "141fa883a4f139f3",
"src/agentkit/server/frontend/src/components/evolution/UsagePanel.vue": "3e7ae825a23b550e",
"src/agentkit/server/frontend/src/components/kb/DocumentUpload.vue": "eaf08ca06861c8ee",
"src/agentkit/server/frontend/src/components/kb/SearchTest.vue": "e813640e950768a2",
"src/agentkit/server/frontend/src/components/kb/SourceConfig.vue": "9453a7d7466c190c",
"src/agentkit/server/frontend/src/components/layout/AgentLayout.vue": "8302bdbff9f407fe",
"src/agentkit/server/frontend/src/components/layout/AppLayout.vue": "0f3d4a85e031b87e",
"src/agentkit/server/frontend/src/components/layout/IconNav.vue": "be857d42b74ed508",
"src/agentkit/server/frontend/src/components/layout/QuadrantPanel.vue": "77329d3919b07f00",
"src/agentkit/server/frontend/src/components/layout/SideNav.vue": "323ce7d138162303",
"src/agentkit/server/frontend/src/components/layout/SplashScreen.vue": "ec6928eebd625c58",
"src/agentkit/server/frontend/src/components/layout/SplitPane.vue": "75fc8951b277767b",
"src/agentkit/server/frontend/src/components/layout/TitleBar.vue": "b6d128888ee306c8",
"src/agentkit/server/frontend/src/components/layout/TopNav.vue": "2808cedccab1d945",
"src/agentkit/server/frontend/src/components/skills/SkillCard.vue": "40c0fd217652f00d",
"src/agentkit/server/frontend/src/components/skills/SkillDetail.vue": "9d1d4601560084cf",
"src/agentkit/server/frontend/src/components/terminal/CommandHistory.vue": "faaf28480d749c4a",
"src/agentkit/server/frontend/src/components/terminal/TerminalEmulator.vue": "609caa6d694356d5",
"src/agentkit/server/frontend/src/components/workflow/ApprovalNode.vue": "f8f2461cec0a7315",
"src/agentkit/server/frontend/src/components/workflow/ConditionNode.vue": "407959ec9eb47ee3",
"src/agentkit/server/frontend/src/components/workflow/FlowCanvas.vue": "a8a03d2ee204d45e",
"src/agentkit/server/frontend/src/components/workflow/NodePalette.vue": "95fb0648ce648331",
"src/agentkit/server/frontend/src/components/workflow/ParallelNode.vue": "865dde4247960ec4",
"src/agentkit/server/frontend/src/components/workflow/PropertyPanel.vue": "d43f36e67014da03",
"src/agentkit/server/frontend/src/components/workflow/SkillNode.vue": "7ad0101f51d1c4aa",
"src/agentkit/server/frontend/src/main.ts": "639546b71c6c8e64",
"src/agentkit/server/frontend/src/router/index.ts": "29208a1d32a815ec",
"src/agentkit/server/frontend/src/stores/capabilities.ts": "0176d1b76782ebc3",
"src/agentkit/server/frontend/src/stores/chat.ts": "935a67e98204747b",
"src/agentkit/server/frontend/src/stores/evolution.ts": "069b9010ed60a48e",
"src/agentkit/server/frontend/src/stores/knowledge.ts": "99f61767dba1fe03",
"src/agentkit/server/frontend/src/stores/settings.ts": "56d078006752617c",
"src/agentkit/server/frontend/src/stores/skills.ts": "ae5050c1fb6ddd53",
"src/agentkit/server/frontend/src/stores/terminal.ts": "cbccc94e9cc62778",
"src/agentkit/server/frontend/src/stores/theme.ts": "58bd9259796d6c5b",
"src/agentkit/server/frontend/src/stores/workflow.ts": "d41bf6daa2725f1e",
"src/agentkit/server/frontend/src/styles/index.ts": "c5c64537137cdd6c",
"src/agentkit/server/frontend/src/styles/responsive.css": "5555100df036dfde",
"src/agentkit/server/frontend/src/styles/theme.ts": "9e10f0955fe24771",
"src/agentkit/server/frontend/src/styles/tokens.css": "856befefe5094b09",
"src/agentkit/server/frontend/src/styles/transitions.css": "5d761d0a4cc6fc7c",
"src/agentkit/server/frontend/src/utils/echarts.ts": "ae4cffe3c5d35db6",
"src/agentkit/server/frontend/src/utils/workflowSerializer.ts": "3b808fd32d78e280",
"src/agentkit/server/frontend/src/views/ChatView.vue": "d19086d9f744decc",
"src/agentkit/server/frontend/src/views/ComputerUseView.vue": "61c9868f1f7bf2aa",
"src/agentkit/server/frontend/src/views/EvolutionView.vue": "508f4b284dc6d96d",
"src/agentkit/server/frontend/src/views/KnowledgeBaseView.vue": "90a70c08c1ad2cb9",
"src/agentkit/server/frontend/src/views/SettingsView.vue": "a986f8bdb21cfe15",
"src/agentkit/server/frontend/src/views/SkillsView.vue": "617903074c9d2941",
"src/agentkit/server/frontend/src/views/TerminalView.vue": "63ac6fc0787aea9d",
"src/agentkit/server/frontend/src/views/WorkflowView.vue": "3b5d6c6e1f585f69",
"src/agentkit/server/frontend/tsconfig.json": "dd37dcb70fdb4d9a",
"src/agentkit/server/frontend/tsconfig.node.json": "80d15af8bdd98d4b",
"src/agentkit/server/frontend/vite.config.ts": "3680082296b63e43",
"src/agentkit/server/middleware.py": "96b05f6f8063241d",
"src/agentkit/server/routes/__init__.py": "1b79a3ccfc6a066b",
"src/agentkit/server/routes/agents.py": "21f2555a100d026a",
"src/agentkit/server/routes/chat.py": "104dbdffea2e444c",
"src/agentkit/server/routes/evolution.py": "fce7e8d39d81d71b",
"src/agentkit/server/routes/evolution_dashboard.py": "d8cdddb6f1f31a2f",
"src/agentkit/server/routes/health.py": "6b80279471d80b96",
"src/agentkit/server/routes/kb_management.py": "4e2805fe7e5cefcc",
"src/agentkit/server/routes/llm.py": "41528730c7e8fc23",
"src/agentkit/server/routes/memory.py": "1c80383f27ea9d06",
"src/agentkit/server/routes/metrics.py": "00e89aa44374d486",
"src/agentkit/server/routes/portal.py": "f84fd0fd3765a473",
"src/agentkit/server/routes/settings.py": "d4e099566030cdd3",
"src/agentkit/server/routes/skill_management.py": "18b59ecc2101a983",
"src/agentkit/server/routes/skills.py": "d8af03b75ee51a7d",
"src/agentkit/server/routes/tasks.py": "5da229c6eeee665d",
"src/agentkit/server/routes/terminal.py": "54a7be8b03545753",
"src/agentkit/server/routes/workflows.py": "36a26fff758c1d46",
"src/agentkit/server/routes/ws.py": "610ea335d8dc8029",
"src/agentkit/server/runner.py": "d9dcdc9dbcead3a2",
"src/agentkit/server/static/assets/AgentLayout-DRofOCle.css": "624fe10c57377b9f",
"src/agentkit/server/static/assets/AppLayout-D3vb9nEe.css": "da60a0d0bcd6f73b",
"src/agentkit/server/static/assets/ChatView-pABfekuB.css": "4c85ceda245e4296",
"src/agentkit/server/static/assets/ComputerUseView-DLnWxFj5.css": "634489f6de671317",
"src/agentkit/server/static/assets/EvolutionView-CYpO52XJ.css": "1093af8009b6d07e",
"src/agentkit/server/static/assets/KnowledgeBaseView-B7BP9eFg.css": "897bd5863b866a4b",
"src/agentkit/server/static/assets/SettingsView-Cux44Hx9.css": "5d814d3e6e2aad80",
"src/agentkit/server/static/assets/SkillsView-CD6l4lTk.css": "01d3f36c1c81d634",
"src/agentkit/server/static/assets/TerminalView-Dg1PpXnU.css": "f700710a00b17f72",
"src/agentkit/server/static/assets/WorkflowView-DRk6nEaR.css": "34e060faceedeaa8",
"src/agentkit/server/static/assets/index-De1g9qb4.css": "440d66ee03cf0385",
"src/agentkit/server/task_store.py": "48b0fa6b93eedd02",
"src/agentkit/session/__init__.py": "4c9b3ddcd033cdfd",
"src/agentkit/session/manager.py": "6970d7f8e84533c9",
"src/agentkit/session/models.py": "73142e2bd83acf39",
"src/agentkit/session/store.py": "afc3cb368d3eda2d",
"src/agentkit/skills/__init__.py": "da932fee48b46389",
"src/agentkit/skills/base.py": "21f6dd7a8ce8e743",
"src/agentkit/skills/geo_pipeline.py": "3d24462bf74772d6",
"src/agentkit/skills/loader.py": "894ad8633fbd6a71",
"src/agentkit/skills/pipeline.py": "5337561cd632fa86",
"src/agentkit/skills/registry.py": "8fb961ea4694a0c8",
"src/agentkit/skills/schema.py": "37b34d43e7d4c872",
"src/agentkit/skills/skill_md.py": "50115a8f8c7daf39",
"src/agentkit/telemetry/__init__.py": "0196a5ba60a48373",
"src/agentkit/telemetry/metrics.py": "2c137c5e2afc0219",
"src/agentkit/telemetry/setup.py": "d80b0163b2b40e4b",
"src/agentkit/telemetry/tracer.py": "2febd113b9aefaff",
"src/agentkit/telemetry/tracing.py": "0de031c9690f1084",
"src/agentkit/tools/__init__.py": "3f38137ac910b75e",
"src/agentkit/tools/agent_tool.py": "40dd8fc67609c1c7",
"src/agentkit/tools/ask_human.py": "0af1dce2de198057",
"src/agentkit/tools/baidu_search.py": "f1fc70895adf4c86",
"src/agentkit/tools/base.py": "f8051c91e7b2c870",
"src/agentkit/tools/composition.py": "1399400373a7bae1",
"src/agentkit/tools/computer_use.py": "07fd6142ba572caf",
"src/agentkit/tools/computer_use_recorder.py": "b128cf30a8194210",
"src/agentkit/tools/computer_use_session.py": "07e93efbf41e56bb",
"src/agentkit/tools/function_tool.py": "d965b795e7aa971a",
"src/agentkit/tools/headroom_retrieve.py": "78c9f452e2884b56",
"src/agentkit/tools/mcp_tool.py": "fdf613db4e05386a",
"src/agentkit/tools/memory_tool.py": "f0e43e260a066b41",
"src/agentkit/tools/output_parser.py": "b5979893bc8751a0",
"src/agentkit/tools/pty_session.py": "a59eb84476a1d233",
"src/agentkit/tools/registry.py": "d9f431fde32e23da",
"src/agentkit/tools/schema_tools.py": "d06b2ebb68137eae",
"src/agentkit/tools/shell.py": "d979a37c206abb75",
"src/agentkit/tools/skill_install.py": "7f6df2be83f0a974",
"src/agentkit/tools/terminal_session.py": "09a52ee902faadf8",
"src/agentkit/tools/web_crawl.py": "7d33adfa513583f9",
"src/agentkit/tools/web_search.py": "32c04419179a503b",
"src/agentkit/utils/__init__.py": "c4cde77152627568",
"src/agentkit/utils/security.py": "64a377f0f9af299b",
"src/agentkit/utils/vector_math.py": "95fbdc879f63c9f6",
"tests/__init__.py": "d41d8cd98f00b204",
"tests/conftest.py": "e25a6080360bb392",
"tests/integration/__init__.py": "d41d8cd98f00b204",
"tests/integration/conftest.py": "35362f98fbcf007d",
"tests/integration/test_agent_lifecycle.py": "45ca2361ae89d2dc",
"tests/integration/test_agent_v2_lifecycle.py": "813c0f11a440e191",
"tests/integration/test_chat_adaptive_e2e.py": "3c29dadefff87012",
"tests/integration/test_coding_harness_pipeline.py": "c07c587bdf135085",
"tests/integration/test_evolution_loop.py": "af13640b3c042c8f",
"tests/integration/test_gap_closure.py": "5445e0618f1570c7",
"tests/integration/test_geo_compression.py": "e97a1e18fec7de33",
"tests/integration/test_geo_e2e.py": "6ab31754e519e7e4",
"tests/integration/test_goal_driven_scenario.py": "d4ff1c1d1095712c",
"tests/integration/test_marketplace_e2e.py": "f9953b64d5fec881",
"tests/integration/test_mcp_roundtrip.py": "d7b61599fa38f9fb",
"tests/integration/test_merged_router.py": "4ebbb09628b4e5ec",
"tests/integration/test_p0_hardening.py": "a64e9748554c3af5",
"tests/integration/test_parallel_tools.py": "835e923a8ed8f0f6",
"tests/integration/test_react_loop.py": "6600439989c27015",
"tests/integration/test_reflexion_loop.py": "44cd7bd06b36311d",
"tests/integration/test_rewoo_configurable_fallback.py": "22d188945fcfcace",
"tests/integration/test_rewoo_fallback.py": "b0d90d02ad2e376a",
"tests/integration/test_router_engine_chain.py": "298d027b3fb536e0",
"tests/integration/test_server_e2e.py": "b18dd4834e6c9294",
"tests/integration/test_soul_evolution_trigger.py": "395fa9a1c00ccf84",
"tests/integration/test_tool_composition.py": "ca26b34ba7ac9fb4",
"tests/test_routing_chain.py": "dc4bbb716852dc24",
"tests/unit/__init__.py": "d41d8cd98f00b204",
"tests/unit/conftest.py": "d957c6d325cecf73",
"tests/unit/core/__init__.py": "d41d8cd98f00b204",
"tests/unit/core/test_plan_checker.py": "a19366b76820465f",
"tests/unit/core/test_plan_executor.py": "c91e6cf7aa90421d",
"tests/unit/evolution/__init__.py": "d41d8cd98f00b204",
"tests/unit/evolution/test_experience_store.py": "95ac34f0aa4eff18",
"tests/unit/evolution/test_path_optimizer.py": "061f66cc4818ca25",
"tests/unit/evolution/test_pitfall_detector.py": "456b21abec2630ed",
"tests/unit/llm/test_usage_store.py": "d35fdbc8ad8c8807",
"tests/unit/memory/__init__.py": "d41d8cd98f00b204",
"tests/unit/memory/test_adapters.py": "bf54ecf5caaba82a",
"tests/unit/memory/test_document_loader.py": "a85a73dacefded5b",
"tests/unit/memory/test_local_rag.py": "d1bb69b64a704ac6",
"tests/unit/memory/test_multi_source_rag.py": "2a90418f99d39a38",
"tests/unit/quality/test_cascade_state_store.py": "373a5015bc25f010",
"tests/unit/server/__init__.py": "d41d8cd98f00b204",
"tests/unit/server/test_evolution_dashboard.py": "8b1882eb0e28c0fe",
"tests/unit/server/test_kb_management.py": "ba66ac5d81a89675",
"tests/unit/server/test_portal_routes.py": "5096d6fb11f916e4",
"tests/unit/server/test_settings_routes.py": "c958e137d691bbdb",
"tests/unit/server/test_skill_management.py": "7c2ab7ca496c2238",
"tests/unit/server/test_terminal_routes.py": "473c2f0453f7bce5",
"tests/unit/server/test_workflow_routes.py": "43f994ebbf8c14a1",
"tests/unit/skills/__init__.py": "d41d8cd98f00b204",
"tests/unit/skills/test_skill_registry_v2.py": "09566589524e0eb7",
"tests/unit/test_ab_tester.py": "ddc218d7db08297a",
"tests/unit/test_agent_bus.py": "7b4ec0c38781e682",
"tests/unit/test_agent_pool.py": "6b0ed7625bfc0ca9",
"tests/unit/test_agent_tool.py": "1d90fc78af703235",
"tests/unit/test_alignment_guard.py": "e8dd184b11c82084",
"tests/unit/test_anthropic_provider.py": "d15f53edc11cb259",
"tests/unit/test_ask_human_tool.py": "a097f78a6d3161ea",
"tests/unit/test_async_tasks.py": "f67efcc208f8ebae",
"tests/unit/test_auction.py": "854f6a6ac5d810b6",
"tests/unit/test_base_agent.py": "527b0607af6b3d0b",
"tests/unit/test_base_agent_v2.py": "8b34903756625ed8",
"tests/unit/test_bus_protocol.py": "cc278c537b9ab55a",
"tests/unit/test_chat_memory_integration.py": "e04eedb7d6769bb9",
"tests/unit/test_chat_routes.py": "ac9fd896485fad70",
"tests/unit/test_chinese_providers.py": "8b7c6b1db3aed927",
"tests/unit/test_cli.py": "391386a1f4b59780",
"tests/unit/test_compression_config.py": "6e789f9e0b0e163d",
"tests/unit/test_compression_strategy.py": "a354bb2c7caf96da",
"tests/unit/test_config_driven.py": "c6110f498ad0685e",
"tests/unit/test_context_compressor.py": "99d074bd33277d71",
"tests/unit/test_contextual_retrieval.py": "c67f8f0c3cd088d1",
"tests/unit/test_cost_aware_router.py": "c46fd3ad7d16d9b2",
"tests/unit/test_dispatcher.py": "093c243e1fd88c53",
"tests/unit/test_embedding_cache.py": "63f6898cd4c08cf8",
"tests/unit/test_episodic_memory.py": "efd3fed7476b01fa",
"tests/unit/test_episodic_vector_search.py": "d512a403d915d17c",
"tests/unit/test_evolution.py": "267c0d997ac4b69b",
"tests/unit/test_evolution_api.py": "c8bc3257089f2f28",
"tests/unit/test_evolution_integration.py": "97dbf613d90cfade",
"tests/unit/test_evolution_lifecycle.py": "c54cba680de3c6cf",
"tests/unit/test_evolution_store.py": "aeede35adc6298bb",
"tests/unit/test_evolution_store_persistent.py": "0a4f2e0b68352bf4",
"tests/unit/test_execution_modes.py": "721e3dbd3bbe42b6",
"tests/unit/test_fitness.py": "a8574c829dd1e6b8",
"tests/unit/test_gateway_cache.py": "b90d19fe913acd6b",
"tests/unit/test_gemini_provider.py": "a11569acda793b28",
"tests/unit/test_genetic_evolution.py": "2298a952c78a1d57",
"tests/unit/test_geo_pipeline.py": "0fbea766ae4127ff",
"tests/unit/test_goal_planner.py": "41a75d8d954b9be2",
"tests/unit/test_handoff.py": "b62bd295b820e7f0",
"tests/unit/test_headroom_compressor.py": "690070348dedc54f",
"tests/unit/test_headroom_retrieve_tool.py": "9cd59c7ef33abdf8",
"tests/unit/test_http_rag_service.py": "c9553d7192028003",
"tests/unit/test_intent_router.py": "d6d25ef448de860b",
"tests/unit/test_llm_cache.py": "40d438e5249bffa3",
"tests/unit/test_llm_gateway.py": "bc60cbfd1cab0078",
"tests/unit/test_llm_protocol.py": "aa3e78b428e69995",
"tests/unit/test_llm_provider.py": "596744095843d1ff",
"tests/unit/test_llm_reflector.py": "703d9262357056ce",
"tests/unit/test_llm_retry.py": "24b9eeb9b333a1e8",
"tests/unit/test_mcp_client.py": "a41cb2b1f7a2ef9f",
"tests/unit/test_mcp_config.py": "81b92487b8d81f0b",
"tests/unit/test_mcp_manager.py": "8536847fdc4cd5ea",
"tests/unit/test_mcp_server.py": "5a27613e90d76aae",
"tests/unit/test_mcp_transport.py": "41d5b3080f524384",
"tests/unit/test_memory_api.py": "bdbc0f4ee07e6f3f",
"tests/unit/test_memory_integration.py": "c65d5c3312dfaf06",
"tests/unit/test_memory_profile.py": "536e91f76c288475",
"tests/unit/test_memory_retriever.py": "fe00ea44c651dd61",
"tests/unit/test_memory_system.py": "cfb1ed2fcf7b3d2b",
"tests/unit/test_memory_tool.py": "14b9fa7c03ca094d",
"tests/unit/test_observability.py": "bc2708134d2e52ac",
"tests/unit/test_onboarding.py": "b3fbdf5aa374ede9",
"tests/unit/test_orchestrator.py": "93feb0c97e569e8c",
"tests/unit/test_orchestrator_adaptive.py": "4b3204dd2e649de8",
"tests/unit/test_orchestrator_bus.py": "20392261e563eedc",
"tests/unit/test_orchestrator_integration.py": "5e5a16aac7a0f1d5",
"tests/unit/test_org_context.py": "d1bad982d17ef585",
"tests/unit/test_output_standardizer.py": "9305eddc864f0c3f",
"tests/unit/test_pipeline.py": "dd3d8845733456f6",
"tests/unit/test_pipeline_adversarial.py": "af99d36831acec26",
"tests/unit/test_pipeline_compensation.py": "2acc0c4bd0126cc8",
"tests/unit/test_pipeline_reflection.py": "d171b373e96620b4",
"tests/unit/test_pipeline_retry.py": "b50bba77dc357937",
"tests/unit/test_pipeline_state.py": "65df456e2d6054c5",
"tests/unit/test_plan_exec_engine.py": "18c76d5d4f010b7d",
"tests/unit/test_prompt_optimizer.py": "06325696628d2f69",
"tests/unit/test_prompt_section.py": "e764eca7b44cc6c8",
"tests/unit/test_prompt_template.py": "e57af046cdb3b4e4",
"tests/unit/test_protocol.py": "da661f7a951b6576",
"tests/unit/test_quality_gate.py": "a1b9a0ce009e3d8d",
"tests/unit/test_query_transformer.py": "7fde4b222ae1f145",
"tests/unit/test_rag_loop.py": "f1347eac9a5919a2",
"tests/unit/test_ragas_evaluator.py": "521786f58e3fbb2f",
"tests/unit/test_react_compression.py": "40a45ee367d84810",
"tests/unit/test_react_engine.py": "62a5df9422ae21f4",
"tests/unit/test_react_skill_mcp_integration.py": "6725ad4c70f8e4a9",
"tests/unit/test_react_token_streaming.py": "14aa43e723cef9c6",
"tests/unit/test_reflexion_engine.py": "689979f3c835aedb",
"tests/unit/test_registry.py": "8d31cc0eee9cd89d",
"tests/unit/test_retrieval_config.py": "a09fca0f1d8c44b2",
"tests/unit/test_retrieve_knowledge_tool.py": "d12b7414e129f593",
"tests/unit/test_rewoo_engine.py": "1fbd72b1923c3e24",
"tests/unit/test_schema_tools.py": "a600b44b4c2ec894",
"tests/unit/test_semantic_router.py": "a277ea3cb0bdd4d3",
"tests/unit/test_server_config.py": "e86663f135a8396c",
"tests/unit/test_server_middleware.py": "660766db5bdf4fb0",
"tests/unit/test_server_routes.py": "3370e164dbac52ea",
"tests/unit/test_session_manager.py": "90ef9929f4910c4b",
"tests/unit/test_session_models.py": "2297ebde41ac1961",
"tests/unit/test_session_store.py": "124898baeef2c549",
"tests/unit/test_shell_tool.py": "fc1237230c684e25",
"tests/unit/test_skill_config.py": "f3aef1188e101bac",
"tests/unit/test_skill_loader.py": "21b83961057e4fcb",
"tests/unit/test_skill_md.py": "a78f997dbe75695b",
"tests/unit/test_skill_pipeline.py": "b51de8bf81f193d4",
"tests/unit/test_skill_registry.py": "30679e6242902a3e",
"tests/unit/test_soul_evolution.py": "07d8b0b4550142a6",
"tests/unit/test_stdio_transport.py": "ad7d51c748b3580f",
"tests/unit/test_streaming.py": "9430a86a4cae4435",
"tests/unit/test_task_store_redis.py": "31e1ed3cb10dab5b",
"tests/unit/test_telemetry.py": "2f957631f9d87522",
"tests/unit/test_tool_composition.py": "88c496199e43eab1",
"tests/unit/test_tool_registry.py": "cb930ed167fe2b23",
"tests/unit/test_trace_recorder.py": "0f7809fe85094c08",
"tests/unit/test_u8_geo_integration.py": "c0eb3468de53fe1e",
"tests/unit/test_unified_evolution_store.py": "8419ece33016b902",
"tests/unit/test_usage_tracker.py": "eed60bc0fcf6a4a3",
"tests/unit/test_web_crawl_tool.py": "13d5bb6f6b098410",
"tests/unit/test_web_search_tool.py": "7ee8a8b20b793e03",
"tests/unit/test_websocket.py": "53e9ecd8c70f5bfe",
"tests/unit/test_working_memory.py": "49016e18a7998c4a",
"tests/unit/tools/__init__.py": "d41d8cd98f00b204",
"tests/unit/tools/test_computer_use.py": "1f851f50f5eb5e44",
"tests/unit/tools/test_pty_session.py": "8a512aabb314036e",
"tests/unit/tools/test_terminal_session.py": "1e979327e0d14753"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
{
"lastAnalyzedAt": "2026-06-17T05:30:00.000000+00:00",
"gitCommitHash": "840d1af4f7a3c1b5e8d2c6a9f0e3b7d5h6i8j0k2",
"version": "1.0.0",
"analyzedFiles": 2418,
"lastUpdateSummary": "fix: resolve benchmark failures from root cause (LLM timeout, WebSocket, latency stats)"
}

View File

@ -16,7 +16,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "A" avatar: "A"
color: "#07C160" color: "#1e40af"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -15,7 +15,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "B" avatar: "B"
color: "#fa8c16" color: "#155e75"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -16,7 +16,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "C" avatar: "C"
color: "#2C3E50" color: "#166534"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -15,7 +15,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "C" avatar: "C"
color: "#722ed1" color: "#1f2937"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -18,7 +18,7 @@ config:
- qa_engineer - qa_engineer
- code_reviewer - code_reviewer
avatar: "D" avatar: "D"
color: "#1890ff" color: "#6b7280"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -15,7 +15,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "E" avatar: "E"
color: "#E31937" color: "#9ca3af"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -15,7 +15,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "F" avatar: "F"
color: "#52c41a" color: "#1e40af"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -16,7 +16,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "J" avatar: "J"
color: "#FF9900" color: "#92400e"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -17,7 +17,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "P" avatar: "P"
color: "#FF6600" color: "#92400e"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -18,7 +18,7 @@ config:
- charlie_munger - charlie_munger
- paul_graham - paul_graham
avatar: "P" avatar: "P"
color: "#8E44AD" color: "#7c2d12"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -15,7 +15,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "Q" avatar: "Q"
color: "#eb2f96" color: "#374151"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -17,7 +17,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "R" avatar: "R"
color: "#1A5276" color: "#6b7280"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -17,7 +17,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "S" avatar: "S"
color: "#555555" color: "#1e40af"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -15,7 +15,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "T" avatar: "T"
color: "#1890ff" color: "#92400e"
is_lead: true is_lead: true
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

View File

@ -17,7 +17,7 @@ config:
collaboration_strategy: "cooperative" collaboration_strategy: "cooperative"
bound_skills: [] bound_skills: []
avatar: "W" avatar: "W"
color: "#1E8449" color: "#78716c"
is_lead: false is_lead: false
task_mode: llm_generate task_mode: llm_generate
prompt: prompt:

27
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,27 @@
version: "3.8"
# =============================================================================
# 开发环境 override
# =============================================================================
# 用法docker compose -f docker-compose.yaml -f docker-compose.dev.yml up -d redis postgres
#
# 仅启动 redis + postgresagentkit 在宿主机运行),映射端口到 6381/5435
# 避免与 pms-redis(6379) / geo_redis(6380) / geo_db(5433) 端口冲突
#
# .env.dev 应包含:
# REDIS_URL=redis://127.0.0.1:6381/0
# DATABASE_URL=postgresql+asyncpg://agentkit:agentkit@127.0.0.1:5435/agentkit
# =============================================================================
services:
# 开发模式不启动 agentkit 容器(在宿主机运行)
agentkit:
profiles: ["never"]
redis:
ports:
- "6381:6379"
postgres:
ports:
- "5435:5432"

View File

@ -1,5 +1,12 @@
version: "3.8" version: "3.8"
# =============================================================================
# 生产部署配置
# =============================================================================
# 启动docker compose up -d
# agentkit 容器内通过 service name (redis/postgres) 连接,不暴露中间件端口
# =============================================================================
services: services:
agentkit: agentkit:
build: . build: .
@ -8,8 +15,12 @@ services:
- "8001:8001" - "8001:8001"
env_file: .env env_file: .env
environment: environment:
# 容器间通信:使用 service name不依赖宿主机端口
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- DATABASE_URL=postgresql+asyncpg://agentkit:agentkit@postgres:5432/agentkit - DATABASE_URL=postgresql+asyncpg://agentkit:agentkit@postgres:5432/agentkit
- AGENTKIT_BUS_BACKEND=redis
- AGENTKIT_SESSION_BACKEND=redis
- AGENTKIT_TASK_STORE_BACKEND=redis
depends_on: depends_on:
redis: redis:
condition: service_healthy condition: service_healthy
@ -25,8 +36,9 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
ports: # 生产模式不暴露端口到宿主机,仅容器间可达
- "6379:6379" expose:
- "6379"
volumes: volumes:
- redisdata:/data - redisdata:/data
healthcheck: healthcheck:
@ -38,8 +50,9 @@ services:
postgres: postgres:
image: pgvector/pgvector:pg15 image: pgvector/pgvector:pg15
ports: # 生产模式不暴露端口到宿主机,仅容器间可达
- "5432:5432" expose:
- "5432"
environment: environment:
POSTGRES_USER: agentkit POSTGRES_USER: agentkit
POSTGRES_PASSWORD: agentkit POSTGRES_PASSWORD: agentkit

View File

@ -0,0 +1,78 @@
---
date: 2026-07-02
topic: private-board-restrictions-and-scheme-b-bubbles
---
## Summary
收尾私董会(@board模块的 UI 细节与单会话状态约束。三处改动:(1) 限制一个对话只能存在一个私董会,新建私董会必须从新会话发起;(2) 将 `BoardBannerCard`(私董会开始卡片)从带边框 / 紫条 / 进度条 / 专家 chip 的重样式简化为单行标题 + 副标题;(3) 给所有 `AssistantText` 渲染添加方案 B 风格的浅灰圆角矩形气泡,与方案 B 截图保持一致。
## Problem Frame
私董会功能自 2026-06 上线以来已能正确触发多轮讨论,但 UI 上仍残留三处粗糙点:
1. **单会话多私董会无约束**。`ChatInput.vue:75` 的"私董会"按钮 `@click` 直接 `showBoardModal = true`,对当前会话是否已经存在私董会无任何判断。后端在 `board_started` 事件中没有按会话做去重,连续两次 `SendMessage("@board:...")` 会在同一会话里创建第二个私董会,叠加在第一个未结束的私董会之上,`boardState.experts` 被覆盖、轮次错乱、`StickyModeHeader` 头像数与实际不符。
2. **`BoardBannerCard` 样式过重**。`BoardBannerCard.vue:55-137` 使用了 `background / border / border-radius / box-shadow` 四件套 + 4px 紫条 + 进度条 + 专家 chip pill与方案 B 整体"克制、不重样式"的取向冲突。方案 B 截图(参考 `docs/.../2026-06-18-chat-area-vi-redesign-requirements.md` 中的方案 B 示意)的"开始"标题区域是单行文本,不带装饰。
3. **方案 B 气泡未落地**。方案 B 截图中的"专家发言"区域是**有**浅灰圆角矩形气泡包裹内容(不是无气泡),与 ChatGPT / Notion AI 风格一致。当前 `AssistantText.vue:1-30` 的内容区 `.assistant-text` 没有背景 / 边框 / 圆角,气泡效果完全缺失。
## Key Decisions
- **私董会限制的"已存在"判断以 `boardState.status` 为准**`status === 'discussing' | 'concluding'` 时禁止在当前会话再次发起;`status === 'completed' | 'dissolved' | null` 时允许(已完成 / 已解散的旧私董会不阻塞新私董会)。判断点放在 `ChatInput.vue` "私董会"按钮 `@click` 处(最自然的 UX 拦截点),不放在 `BoardMeetingModal` 内部(避免用户填表后才发现不能发起)。
- **私董会限制的反馈方式是 a-modal 弹窗 + 快捷新建按钮**。点击"私董会"按钮时若检测到已有私董会,弹出 `a-modal`,标题"当前会话已存在私董会",副文"请新建会话来创建新的私董会",按钮"我知道了" + "新建会话"。"新建会话"按钮直接调用 `chatStore.createConversation()``chatStore.selectConversation(newId)`,让用户立即在新会话里继续操作。
- **BoardBannerCard 简化为单行标题 + 副标题**。完全去掉 `BankOutlined` 图标、专家 chip 列表、4px 紫条、进度条、卡片背景 / 边框 / 圆角 / 阴影。最终输出形如:
```
私董会 — 利用 agent 实现私董会的功能,应该用什么功能来打动客户
轮次:第 1 / 5 轮
```
标题字号 = `var(--font-base)` 加粗 + 主题文本;副标题字号 = `var(--font-xs)` + `var(--text-tertiary)`。`StickyModeHeader` 顶部的紫色"私董会"徽章 + 主题 + 专家头像组保持不变,承担需要"重样式"的展示职责。
- **方案 B 浅灰气泡应用到所有 assistant 消息**。具体范围:`role === 'assistant'` 的所有 `MessageShell` 内的内容(普通 chat、@team 阶段、@board 发言、Debate、Plan exec、Tool result 等)都加同款浅灰圆角矩形气泡;`role === 'user'` 的用户消息气泡保留现有 `UserBubble.vue` 的右对齐独立样式,不加 AssistantText 风格的浅灰块。气泡使用 token 颜色(`var(--bg-secondary)` 或 `var(--bg-elevated)` 系)+ 圆角 `var(--radius-md)` + 内边距 `var(--space-3) var(--space-4)`,确保与方案 B 截图视觉一致;颜色与边框用 CSS 变量绑定,**禁止硬编码** `#f3f4f6` / `#fbfbfa` / `#ededec` 等值。
- **气泡内的代码块、表格、行内代码样式保持不变**。`AssistantText.vue:257-401` 的 `pre / hljs / code / table` 样式已经在 dark-on-light 配色上做了适配(`--code-bg` / `--code-fg` / `--code-keyword` 等 token气泡背景换浅灰后这些 token 自动适配,不需要单独再改。
- **不触碰 StickyModeHeader 顶部条**。顶部条的紫色边框、徽章、4 个专家头像等不属于本次改动范围。
## Requirements
### 单会话私董会限制
- R1. `ChatInput.vue` 的"私董会"按钮 `@click` 处理函数在打开 `BoardMeetingModal` 前,先检查 `chatStore.boardState.value`:若非 null 且 `status === 'discussing' | 'concluding'`,触发 a-modal 弹窗,**不**打开 `BoardMeetingModal`
- R2. a-modal 弹窗内容:标题"当前会话已存在私董会",副文案"请新建会话来创建新的私董会",按钮"我知道了"(关闭弹窗)+ "新建会话"(主操作)。"新建会话"按钮点击后:(a) 关闭弹窗;(b) 调用 `chatStore.createConversation()`(c) `await chatStore.selectConversation(newId)`(d) 不自动打开 `BoardMeetingModal`,由用户在新会话中再次点击"私董会"按钮继续。
- R3. 若 `boardState.value === null``status === 'completed' | 'dissolved'`,保持当前行为:`showBoardModal = true` 直接打开 `BoardMeetingModal`,不弹提示。
- R4. 判定不依赖后端 / `is_board` 标记 / `conv.is_board`——以前端 `chatStore.boardState.value` 实时状态为权威源,避免 reload 后误判。
### 私董会开始卡片简化
- R5. `BoardBannerCard.vue` 重构为单行标题 + 副标题:保留 `topic / maxRounds / currentRound` props向后兼容调用方不再使用 `experts` props删除 prop
- R6. 模板输出仅两行:第一行 `私董会 — {topic}`(无 BankOutlined、无边框、无背景、无圆角、无 4px 紫条);第二行 `轮次:第 {currentRound} / {maxRounds} 轮`(小灰字)。
- R7. `<style scoped>` 中删除 `.board-banner-card` / `.board-banner-card__bar` / `.board-banner-card__chip` 等所有重样式相关类;保留 `.board-banner-card__title` / `.board-banner-card__meta` 两条最小样式。
- R8. `useMessageRenderer.ts``board_started` 的渲染路径不变(仍然渲染 `BoardBannerCard` 组件),确保 streaming 期间与 reload 后的视觉一致。
### AssistantText 浅灰气泡
- R9. `MessageShell.vue``.message-shell__content` 增加浅灰气泡样式:仅当 `role === 'assistant'` 时生效(`user` 角色不触发);`background: var(--bg-secondary)` + `border-radius: var(--radius-md)` + `padding: var(--space-3) var(--space-4)` + `border: 1px solid var(--border-color)` + `color: var(--text-primary)`
- R10. 气泡样式**不**使用 `!important`,不覆盖组件内已有的代码块 / 表格 / 路由 tag 样式(这些样式已经在 R-9 之外独立维护)。
- R11. 浅灰气泡不影响 `BoardRoundCard``AssistantText` 的渲染(已统一走 `MessageShell` 槽位)。
- R12. 私董会专家发言(`board_speech` / `board_summary`)、普通 chat assistant 消息、@team 阶段消息、Debate 消息都通过 `MessageShell` + `AssistantText` 渲染,因此统一获得 R-9 浅灰气泡,无需逐个组件加样式。
- R13. 气泡宽度继承 `.message-shell__content` 的现有 `width: 100%; max-width: 100%`——气泡跟随消息列宽自适应,不强制固定最大宽度。
## Scope Boundaries
### Deferred for later
- **后端按会话去重 `board_started`**:本次只做前端拦截,**不**改后端逻辑。后端允许同一会话收到多次 `board_started` 是当前事实,本次不做接口变更;前端的"未在新会话发起"拦截在功能上等价于"阻止重复发起"。如果未来需要在多端 / 多浏览器同步场景下做强一致,再考虑后端校验。
- **StickyModeHeader 顶部条的视觉细化**(徽章大小、专家头像间距、紫色边框粗细)不在本次范围。
- **BoardConclusionCard / DebateBannerCard / TeamPlanCard 等其他 board 模式组件的样式统一**:本次只改 `BoardBannerCard`,其他卡片样式留待后续迭代。
### Outside this product's identity
- 不调整方案 B 调色板(`expertIdentity.ts` 的 12 色 PALETTE和专家头像首字符规则。
- 不调整私董会后端流程(`BoardOrchestrator` / `chatStream` 事件顺序)。
- 不调整 AssistantText 的 markdown 渲染逻辑、代码高亮、表格样式。
## Dependencies / Assumptions
- 假设:`chatStore.createConversation()` 存在并返回新会话 id`chatStore.ts:310` 已确认)。
- 假设:`chatStore.selectConversation(id)` 可异步调用(`chatStore.ts:218` 已确认)。
- 假设:`BoardState.status` 类型为 `"discussing" | "concluding" | "completed" | "dissolved"``chatStream.ts:65` 已确认),"已存在私董会"判断取 `discussing | concluding`
- 假设:`--bg-secondary` / `--radius-md` / `--space-3` / `--space-4` / `--border-color` / `--text-primary` 已在 `tokens.css` 中定义并被前端使用(`styles/tokens.css` 已确认存在)。
- 不确定性:私董会可能存在的中间状态(`forming` / `executing` / `synthesizing` 等)目前不在 `BoardState.status` 联合中;如果未来增加新 status本次"已存在"判断需扩展。

View File

@ -0,0 +1,393 @@
---
date: 2026-07-02
type: feat
title: 私董会单会话限制 + 方案B 气泡 + 简化开始卡片
origin: docs/brainstorms/2026-07-02-private-board-restrictions-and-scheme-b-bubbles-requirements.md
status: ready
---
## Summary
收尾私董会(@board模块的 UI 细节与单会话状态约束,四处协同改动:(1) 在 `ChatInput.vue` 的"私董会"按钮 click 处拦截"当前会话已存在进行中的私董会"场景,弹 a-modal 提示并提供快捷新建会话按钮;(2) 将 `BoardBannerCard.vue` 从带边框/紫条/进度条/专家 chip 的重样式简化为单行标题+副标题;(3) 给 `MessageShell.vue` 中所有 `role === 'assistant'` 的消息内容添加方案 B 风格的浅灰圆角矩形气泡F1-A 独立 token / F4-A 排除 conclusion / D4-方案1 `:empty` 隐藏);(4) 将 `UserBubble.vue` 普通文本消息改为 demo 中的深色右对齐气泡(`--color-primary` + `--text-inverse`@board/@team 命令卡片保持现有浅色背景。改动范围仅限前端 Vue 组件与少量 store/type/token 文件,不动后端、不动方案 B 调色板、不动 StickyModeHeader 顶部条。
## Problem Frame
私董会功能上线后存在三处 UI 粗糙点(详见 origin: docs/brainstorms/2026-07-02-private-board-restrictions-and-scheme-b-bubbles-requirements.md
1. **单会话多私董会无约束**`ChatInput.vue:75` 的"私董会"按钮 `@click` 直接 `showBoardModal = true`,对当前会话是否已存在私董会无任何判断。连续两次 `SendMessage("@board:...")` 会创建第二个私董会,叠加在第一个未结束的私董会之上,`boardState.experts` 被覆盖、轮次错乱、`StickyModeHeader` 头像数与实际不符。
2. **BoardBannerCard 样式过重**`BoardBannerCard.vue` 使用了 card+border+shadow+4px 紫条+进度条+专家 chip pill 等装饰,与方案 B 整体"克制、不重样式"取向冲突。方案 B 截图中的"开始"区域是单行文本,不带装饰。
3. **方案 B 气泡未落地**:方案 B 截图中专家发言区域**有**浅灰圆角矩形气泡包裹内容,与 ChatGPT / Notion AI 风格一致。当前 `MessageShell.vue:178-184``.message-shell__content` 没有背景/边框/圆角,气泡效果完全缺失。
## Requirements
完整继承 origin 文档的 13 条 R-IDsR1-R13并新增 R14-R19F4-A / D4-方案1 / U4 决策固化),共 19 条,分组如下:
**单会话私董会限制R1-R4**
- R1`ChatInput.vue` "私董会"按钮 `@click` 在打开 `BoardMeetingModal` 前检查 `chatStore.boardState``status === 'discussing' | 'concluding'` 时弹 a-modal**不**打开 modal
- R2a-modal 标题"当前会话已存在私董会",副文"请新建会话来创建新的私董会",按钮"我知道了"+ "新建会话"(主操作);"新建会话"流程:关 modal → `chatStore.createConversation()``chatStore.selectConversation(newId, true)` → 不自动打开 modal
- R3`boardState === null` 或 `status === 'completed' | 'dissolved'` 时保持当前行为
- R4以前端 `chatStore.boardState` 为权威源,不依赖后端 / `is_board` 标记
**BoardBannerCard 简化R5-R8**
- R5重构为单行标题+副标题,保留 `topic / maxRounds / currentRound` props 向后兼容,删除 `experts` prop
- R6模板输出两行`私董会 — {topic}` + `轮次:第 {currentRound} / {maxRounds} 轮`
- R7删除 `.board-banner-card` 的重样式background/border/border-radius/box-shadow`__bar / __chip` 等重样式类,保留 `.board-banner-card` 容器(仅 margin/padding+ `__title / __meta` 最小样式
- R8`useMessageRenderer.ts` 中 `board_started` 渲染路径不变
**AssistantText 浅灰气泡R9-R15**
- R9`MessageShell.vue` 的 `.message-shell__content``background: var(--bg-message-bubble)` + `border-radius: var(--radius-md)` + `padding: var(--space-3) var(--space-4)` + `border: 1px solid var(--border-color)` + `color: var(--text-primary)`,仅 `role === 'assistant'` 时生效。**F1-A 决策**:引入独立 token `--bg-message-bubble`light `#ffffff` / dark `#1f1f1f`),与 `--bg-secondary`inline code/table 背景)解耦,避免气泡背景与代码/表格背景视觉冲突
- R10不使用 `!important`,不覆盖代码块/表格/路由 tag 样式
- R11不影响 `BoardRoundCard``AssistantText` 渲染
- R12私董会专家发言、普通 chat、@team 阶段、Debate 等都通过 `MessageShell + AssistantText` 统一获得气泡
- R13气泡宽度继承现有 `width: 100%; max-width: 100%`,不强制固定最大宽度
- R14**F4-A 决策**:气泡选择器排除 `board_conclusion` 类型——`BoardConclusionCard` 自带 card chromebackground/border/border-radius/shadow保留其自身样式不再被气泡包裹。实现方式`MessageShell.vue` 通过 `messageType` prop 或 `:not(:has(.board-conclusion-card))` 选择器排除
- R15**D4-方案1 决策**:空 slot 内容pre-stream thinking / tool-call-only时气泡不渲染背景——通过 `:empty` 选择器隐藏 `background / border / padding`,仅显示 thinking dots。有内容流入后自动恢复气泡样式
**UserBubble 普通文本深色气泡R16-R19**
- R16`UserBubble.vue` 的普通文本消息(`<span class="user-bubble__text">`)样式改为 demo 中的深色右对齐气泡:`background: var(--color-primary)` + `color: var(--text-inverse)` + `padding: var(--space-3) var(--space-4)` + `max-width: 70%`
- R17@board/@team 命令卡片(`.user-bubble__command`)和文件附件保持现有 `--bg-tertiary` 浅色背景,不应用深色气泡样式
- R18dark mode 自动反转——`--color-primary` 在 dark mode 下为 `#fbfbfa`(浅),`--text-inverse` 为 `#1a1a1a`user 气泡自动变为浅色背景 + 深色文字
- R19通过 `isPlainText` computed`!fileAttachment && !commandBubble`+ `.user-bubble--text` modifier class 区分普通文本与命令卡片,不修改 `.user-bubble` 默认样式
## Key Technical Decisions
- **拦截点选择 ChatInput 按钮 click不放在 BoardMeetingModal 内部** — 最自然的 UX 拦截点,避免用户填表后才发现不能发起。判断依据 `chatStore.boardState` 实时状态,不依赖后端 / `is_board` 标记,避免 reload 后误判(见 origin R4
- **私董会状态判定取 `discussing | concluding`**`BoardState.status` 类型为 `"discussing" | "concluding" | "completed" | "dissolved"`chatStream.ts:65 确认)。`completed | dissolved` 的旧私董会不阻塞新私董会发起,符合 origin R3 要求。
- **"新建会话"按钮流程:`createConversation()` → `selectConversation(newId, true)`** — `createConversation()` 是同步函数chatStore.ts:333 确认),自动设置 `currentConversationId` 但不加载服务端会话;后续 `selectConversation(newId, true)` 强制 reload 确保状态干净。不自动打开 `BoardMeetingModal`,由用户在新会话中再次点击"私董会"按钮继续(见 origin R2
- **BoardBannerCard 简化为单行标题+副标题,不删除组件本身** — 保留 `BoardBannerCard.vue` 文件,仅重构 template + style。`useMessageRenderer.ts` 的 `board_started` 渲染路径不变origin R8保持 streaming 期间与 reload 后视觉一致。
- **气泡样式挂在 `MessageShell.vue` 的 `.message-shell__content` 上,不挂在 `AssistantText.vue`**`MessageShell` 是所有 assistant 消息的统一外壳,挂在这里可一次性覆盖 board_speech / board_summary / 普通聊天 / @team / Debate 等场景origin R12且不影响 `UserBubble.vue` 的 user 消息样式。**board_conclusion 例外**F4-A`BoardConclusionCard` 自带 card chrome不被气泡包裹。
- **气泡使用 CSS 变量,禁止硬编码** — 新增 `--bg-message-bubble: #ffffff` (light) / `#1f1f1f` (dark) 到 `tokens.css`,与 `--bg-secondary`inline code/table 背景 `#fbfbfa`解耦F1-A 决策。dark mode 自动切换。禁止硬编码 `#f3f4f6` / `#fbfbfa` / `#ffffff` 等值project rules 硬约束)。
- **`role === 'assistant'` 时启用气泡,`role === 'user'` 不启用** — 通过 `.message-shell--assistant .message-shell__content` 选择器限定,避免污染 user 消息的 `UserBubble` 样式。
- **F4-A 排除 conclusion 类型**`board_conclusion` 消息渲染的 `BoardConclusionCard` 自带 `background: var(--bg-primary)` + `border` + `border-radius: var(--radius-card)` + `box-shadow`,气泡选择器需排除该类型,避免"白卡片嵌套在白气泡里"的视觉冲突。实现方式:在 `MessageShell.vue` 增加 `messageType` prop`:not(:has(.board-conclusion-card))` 选择器conclusion 类型不加气泡样式。
- **D4-方案1 `:empty` 隐藏空气泡** — 空 slot 内容pre-stream thinking / tool-call-only时通过 `:empty` 选择器隐藏 `background / border / padding`,仅显示 thinking dots 动画。有内容流入后自动恢复气泡样式,无需 JS 判断。
- **U4 仅普通文本消息深色气泡,命令卡片保持浅色**`UserBubble.vue` 同时渲染三种内容(普通文本 / @board|@team 命令卡片 / 文件附件命令卡片含结构化信息header + experts list深色背景会降低可读性。通过 `isPlainText` computed + `.user-bubble--text` modifier 精确限定深色样式仅作用于普通文本,命令卡片和文件附件继承 `.user-bubble` 默认 `--bg-tertiary` 浅色背景。
- **U4 用 `--color-primary` + `--text-inverse` 自动适配 dark mode**`--color-primary` 在 light mode 为 `#1a1a1a`dark mode 为 `#fbfbfa`(浅);`--text-inverse` 反之。user 气泡在 dark mode 下自动反转为浅色背景 + 深色文字,无需额外 dark mode 覆盖。
## Implementation Units
### U1. ChatInput 拦截 + a-modal 弹窗 + 快捷新建会话
**Goal:** 在 `ChatInput.vue` 的"私董会"按钮 click 处增加拦截逻辑:当前会话已存在进行中的私董会时弹 a-modal 提示,提供"我知道了"和"新建会话"两个按钮;"新建会话"按钮调用 `chatStore.createConversation()` + `selectConversation(newId, true)` 跳转到新空会话。
**Requirements:** R1, R2, R3, R4
**Dependencies:** 无
**Files:**
- src/agentkit/server/frontend/src/components/chat/ChatInput.vue修改
- src/agentkit/server/frontend/src/components/chat/\_\_tests\_\_/ChatInput.test.ts新建
**Approach:**
- 在 `<template>` 中新增一个 `<a-modal v-model:open="showBoardBlockModal">`,标题"当前会话已存在私董会",内容"请新建会话来创建新的私董会"footer 两个按钮:`<a-button @click="showBoardBlockModal = false">我知道了</a-button>` + `<a-button type="primary" @click="handleCreateNewConversationForBoard">新建会话</a-button>`
- 新增 `showBoardBlockModal = ref(false)` state
- 新增 `handleBoardClick()` 函数替代 inline `@click="showBoardModal = true"`
- 检查 `chatStore.boardState?.status`:若为 `'discussing' | 'concluding'``showBoardBlockModal.value = true`return
- 否则 `showBoardModal.value = true`
- 新增 `handleCreateNewConversationForBoard()` 函数:
- `showBoardBlockModal.value = false`
- `chatStore.createConversation()`
- `await chatStore.selectConversation(chatStore.currentConversationId!, true)`
- 不设置 `showBoardModal.value = true`(让用户在新会话中再次点击)
- "私董会"按钮 `@click` 改为 `handleBoardClick`
**Patterns to follow:**
- `ChatInput.vue:63-71``showTeamModal` 模式v-model:open + ref + handleXxxSubmit
- `chatStore.ts:333-345``createConversation` 同步签名
- `chatStore.ts:219``selectConversation(id, force = false)` 异步签名
**Test scenarios:**
- Happy path`chatStore.boardState === null` → 点击"私董会"按钮 → `showBoardModal === true``showBoardBlockModal === false`
- Happy path`chatStore.boardState?.status === 'completed'` → 点击"私董会"按钮 → `showBoardModal === true`(已完成私董会不阻塞)
- Happy path`chatStore.boardState?.status === 'dissolved'` → 同上
- 拦截 path`chatStore.boardState?.status === 'discussing'` → 点击"私董会"按钮 → `showBoardModal === false``showBoardBlockModal === true`
- 拦截 path`chatStore.boardState?.status === 'concluding'` → 同上
- "新建会话"按钮:点击后 `showBoardBlockModal === false``chatStore.currentConversationId` 已变化,`chatStore.boardState === null`(新会话清空 boardState
- "我知道了"按钮:点击后 `showBoardBlockModal === false``chatStore.currentConversationId` 不变,`showBoardModal === false`
> **注**Pinia setup store 的 ref 自动解包,`chatStore.boardState` 已是 `BoardState | null`,不能写 `chatStore.boardState.value`。如需 ref 访问用 `storeToRefs(chatStore).boardState.value`
**Verification:**
- 启动 Tauri 客户端,在已有私董会的会话中点击"私董会"按钮,应弹出提示而非 modal
- 点击"新建会话"后自动跳转到新会话boardState 清空
- 在新会话中点击"私董会"按钮可正常打开 BoardMeetingModal
---
### U2. BoardBannerCard 简化为单行标题+副标题
**Goal:** 将 `BoardBannerCard.vue` 从带边框/紫条/进度条/专家 chip 的重样式简化为纯文本两行:`私董会 — {topic}` + `轮次:第 N/M 轮`,删除 BankOutlined 图标、4px 紫条、专家 chip、卡片背景/边框/圆角/阴影。
**Requirements:** R5, R6, R7, R8
**Dependencies:** 无(与 U1/U3 互不依赖,可并行)
**Files:**
- src/agentkit/server/frontend/src/components/chat/messages/BoardBannerCard.vue修改
- src/agentkit/server/frontend/src/components/chat/helpers/useMessageRenderer.ts修改**F3-A 决策**:删除 board_banner case 中 experts prop 传参,避免 Vue 3 attribute fallthrough 把 `[object Object]` 渲染到根 div
- src/agentkit/server/frontend/src/components/preview/scenes/Scene4BoardDiscussion.vue修改适配新 props
- src/agentkit/server/frontend/src/components/chat/messages/\_\_tests\_\_/BoardBannerCard.test.ts新建
**Approach:**
- `<template>` 简化为:
```vue
<div class="board-banner-card">
<div class="board-banner-card__title">私董会 — {{ topic }}</div>
<div class="board-banner-card__meta">轮次:第 {{ currentRound }} / {{ maxRounds }} 轮</div>
</div>
```
- 删除 `<script setup>` 中的 `BankOutlined` import 和 `progressPercent` computed
- `interface Props` 删除 `experts` 字段,保留 `topic / maxRounds / currentRound`
- `<style scoped>` 删除 `.board-banner-card__bar / __body / __icon / __experts / __chip / __chip--moderator / __chip-avatar / __progress / __progress-fill` 共 9 个类,仅保留:
- `.board-banner-card` 容器(`margin-bottom: var(--space-3)` + `padding: var(--space-2) 0`,无 background/border/shadow
- `.board-banner-card__title``font-size: var(--font-md); font-weight: var(--font-weight-semibold); color: var(--text-primary); margin-bottom: var(--space-1)`
- `.board-banner-card__meta``font-size: var(--font-xs); color: var(--text-tertiary)`
- `Scene4BoardDiscussion.vue` 中调用 `<BoardBannerCard>` 的地方删除 `:experts` prop 传参
- `useMessageRenderer.ts``board_banner` case 删除 `experts` prop 传参(**F3-A 决策**
**Patterns to follow:**
- `MessageShell.vue:150-163``.message-shell__name / __meta` 简洁文本样式
**Test scenarios:**
- Happy path传入 `topic="测试主题" maxRounds=5 currentRound=2` → 渲染两行文本,第一行"私董会 — 测试主题",第二行"轮次:第 2 / 5 轮"
- 边界:`currentRound=0` → 第二行"轮次:第 0 / 5 轮"(不报错)
- 边界:`maxRounds=0` → 第二行"轮次:第 1 / 0 轮"(不报错,不做除零校验)
- 渲染契约:组件根 div **不**含 `background / border / border-radius / box-shadow` 任一 CSS 属性
- 渲染契约:组件**不**渲染 `<BankOutlined />` / `.board-banner-card__bar` / `.board-banner-card__chip` 任一元素
- Props 契约:传入 `experts` 数组时组件不报错向后兼容prop 已删除会被 Vue 忽略为 fallthrough attr但需确认不会作为 attribute 渲染到根 div
- **F3-A fallthrough 契约**`useMessageRenderer.ts` 的 `board_banner` case 不再传 `experts` prop已删除根 div **不**含 `experts="[object Object]"` 属性
**Verification:**
- 启动 Tauri 客户端,发起私董会,第一条消息应显示为纯文本两行,无卡片背景/图标/进度条
- 预览场景 `Scene4BoardDiscussion.vue` 仍然能正常渲染
---
### U3. MessageShell 浅灰气泡方案B
**Goal:** 在 `MessageShell.vue``.message-shell__content` 上增加方案 B 风格的浅灰圆角矩形气泡样式,仅 `role === 'assistant'` 时生效,使用 CSS 变量绑定dark mode 自动切换。**F1-A 决策**:气泡用独立 token `--bg-message-bubble`(与 inline code/table 的 `--bg-secondary` 解耦);**F4-A 决策**:排除 `board_conclusion` 类型;**D4-方案1 决策**:空内容通过 `:empty` 隐藏气泡。
**Requirements:** R9, R10, R11, R12, R13, R14, R15
**Dependencies:** 无(与 U1/U2 互不依赖,可并行)
**Files:**
- src/agentkit/server/frontend/src/styles/tokens.css修改**F1-A 决策**:新增 `--bg-message-bubble` tokenlight `#ffffff` / dark `#1f1f1f`
- src/agentkit/server/frontend/src/components/chat/messages/MessageShell.vue修改
- src/agentkit/server/frontend/src/components/chat/messages/\_\_tests\_\_/MessageShell.test.ts新建或扩展
**Approach:**
- **第一步F1-A**:在 `tokens.css``:root`light`[data-theme="dark"]`(或等效 dark 选择器)下新增 `--bg-message-bubble: #ffffff`light/ `#1f1f1f`dark。位置紧邻 `--bg-secondary` 定义,注释说明"消息气泡背景,与 inline code/table 背景解耦"
- **第二步**:在 `MessageShell.vue``<style scoped>` 中新增规则:
```css
.message-shell--assistant .message-shell__content {
background: var(--bg-message-bubble);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
color: var(--text-primary);
}
/* F4-A: board_conclusion 类型不加气泡,保留 BoardConclusionCard 自身 card chrome */
.message-shell--assistant.message-shell--conclusion .message-shell__content,
.message-shell--assistant:has(.board-conclusion-card) .message-shell__content {
background: transparent;
border: none;
border-radius: 0;
padding: 0;
}
/* D4-方案1: 空 slot 内容隐藏气泡背景,仅显示 thinking dots */
.message-shell--assistant .message-shell__content:empty {
background: transparent;
border: none;
padding: 0;
}
```
- **第三步F4-A 配套)**`MessageShell.vue` 的 `interface Props` 新增 `messageType?: string` prop可选template 根 div 根据 `messageType === 'board_conclusion'` 添加 `.message-shell--conclusion` class。或使用 `:has(.board-conclusion-card)` 选择器(更简洁但需确认浏览器支持——现代浏览器已支持 `:has()`Tauri WebView 基于 Chromium 111+ 支持)
- 选择器限定 `.message-shell--assistant` 前缀,确保 `role === 'user'``UserBubble` 不受影响
- 不使用 `!important`,让 `AssistantText.vue:257-401` 内部的 `pre / hljs / code / table` 样式自然继承(这些样式已用 `--code-bg` 等 token浅灰气泡背景下自动适配
- 不修改 `.message-shell__content``display: flex / flex-direction: column / gap: var(--space-2) / width: 100% / max-width: 100%`R13 要求)
- 不修改 `.message-shell--user .message-shell__content``align-items: flex-end`user 消息保持右对齐,不加气泡)
**Patterns to follow:**
- `tokens.css:65,223``--bg-secondary` 定义light `#fbfbfa`dark `#1f1f1f`)—— `--bg-message-bubble` 紧邻此定义
- `tokens.css:79,237``--border-color` 定义
- `tokens.css:97``--radius-md: 6px`
- `LoginView.vue:184-196` 的"显式绑定 token禁硬编码"模式
**Test scenarios:**
- Happy pathassistant渲染 `<MessageShell role="assistant">` + 默认 slot 内容 → `.message-shell__content` 计算样式含 `background-color: rgb(255, 255, 255)`light mode 下 `--bg-message-bubble: #ffffff`
- Happy pathuser渲染 `<MessageShell role="user">` + 默认 slot 内容 → `.message-shell__content` 计算样式 `background-color` 为空或 `rgba(0,0,0,0)`(不加气泡)
- **F1-A 区分性**slot 内含 `<code>async</code>` inline code → inline code 背景为 `--bg-secondary` (`#fbfbfa`),气泡背景为 `--bg-message-bubble` (`#ffffff`),二者视觉可区分
- 气泡内代码块slot 内含 `<pre><code>` → 代码块背景 `--code-bg` 不被气泡背景覆盖(视觉可区分)
- 气泡宽度:`.message-shell__content` 计算样式 `width: 100%``max-width: 100%`(跟随消息列宽)
- Dark mode切换到 dark mode → `.message-shell__content` 计算样式 `background-color: rgb(31, 31, 31)``--bg-message-bubble: #1f1f1f`
- **F4-A 排除 conclusion**:渲染 `<MessageShell role="assistant" messageType="board_conclusion">` + `BoardConclusionCard` slot → `.message-shell__content` 计算样式 `background-color: transparent``border: none`(不加气泡,`BoardConclusionCard` 自身 card chrome 保留)
- **D4-方案1 空内容隐藏**:渲染 `<MessageShell role="assistant">` + 空 slot → `.message-shell__content` 计算样式 `background-color: transparent``border: none``padding: 0``:empty` 选择器生效)
- **D4-方案1 有内容恢复**:渲染 `<MessageShell role="assistant">` + 非空 slot → `:empty` 不匹配,气泡样式正常应用
- 渲染契约:样式**不**使用 `!important`
- 渲染契约:样式**不**硬编码 `#f3f4f6 / #fbfbfa / #ededec / #ffffff` 等值(必须用 CSS 变量)
**Verification:**
- 启动 Tauri 客户端,发起普通 chatassistant 消息应有浅灰(实际为 `#ffffff` 纯白,与 `#fbfbfa` 的 inline code 区分)圆角矩形气泡
- 发起私董会专家发言board_speech应有同款气泡
- 发起私董会结论消息board_conclusion应保留 `BoardConclusionCard` 自身 card 样式,**不**被气泡包裹
- 切换 dark mode气泡背景应自动变为深色`#1f1f1f`
- user 消息(右侧)应显示为 demo 中的深色气泡(`--color-primary` 背景 + `--text-inverse` 白字),@board/@team 命令卡片保持现有浅色背景(见 U4
- assistant 消息在 thinking 阶段(空内容)不应显示空气泡矩形,仅显示三点动画
---
### U4. UserBubble 普通文本深色气泡(参考 demo
**Goal:** 将 `UserBubble.vue` 的普通文本消息(`<span class="user-bubble__text">`)样式改为 demo 中的深色右对齐气泡:`--color-primary` 背景 + `--text-inverse` 白字 + `padding: var(--space-3) var(--space-4)` + `max-width: 70%`。@board/@team 命令卡片和文件附件保持现有 `--bg-tertiary` 浅色背景,不变。
**Requirements:** R16, R17, R18, R19
**Dependencies:** 无(与 U1/U2/U3 互不依赖,可并行)
**Files:**
- src/agentkit/server/frontend/src/components/chat/messages/UserBubble.vue修改
- src/agentkit/server/frontend/src/components/chat/messages/\_\_tests\_\_/UserBubble.test.ts新建或扩展
**Approach:**
- `<script setup>` 新增 `isPlainText` computed`!fileAttachment.value && !commandBubble.value`
- `<template>` 根 div 的 `:class` 新增 `'user-bubble--text': isPlainText` modifier
```vue
<div
class="user-bubble"
:class="{
'user-bubble--focusable': msgId,
'user-bubble--text': isPlainText,
}"
...
>
```
- `<style scoped>` 新增 `.user-bubble--text` 规则(仅普通文本消息时覆盖 `.user-bubble` 默认背景):
```css
/* U4: 普通文本消息参考 demo 深色气泡command card / file attachment 保持浅色 */
.user-bubble--text {
background: var(--color-primary);
color: var(--text-inverse);
padding: var(--space-3) var(--space-4);
max-width: 70%;
}
```
- 不修改 `.user-bubble` 默认样式(`--bg-tertiary` 背景 + `--text-primary` 文字),让 command card 和 file attachment 继承现有浅色背景
- 不修改 `.user-bubble__command` / `.user-bubble__command-*` 内部样式(保持现有结构化卡片可读性)
- 不修改 `.user-bubble__actions` 操作按钮工具栏样式(当前 `--bg-secondary` 背景 + `--border-color` 边框,在深色气泡旁可见性需验证)
- `--color-primary``--text-inverse` 在 light/dark mode 下自动反转tokens.css:18,176 + 75,233 确认dark mode 下 user 气泡自动变为浅色背景 + 深色文字
**Patterns to follow:**
- demo `tmp_scheme_b_options_demo.html:230-240``.user-msg__bubble` 样式(`--color-primary` + `--text-inverse` + `--space-3 var(--space-4)` + `--radius-lg` + `max-width: 70%`
- `tokens.css:18,176``--color-primary` 定义light `#1a1a1a` / dark `#fbfbfa`
- `tokens.css:75,233``--text-inverse` 定义light `#ffffff` / dark `#1a1a1a`
**Test scenarios:**
- Happy path普通文本渲染 `<UserBubble content="Hello">` → 根 div 含 `.user-bubble--text` class计算样式 `background-color: rgb(26, 26, 26)`light mode 下 `--color-primary: #1a1a1a``color: rgb(255, 255, 255)``--text-inverse: #ffffff`
- Happy path@board 命令):渲染 `<UserBubble content="@board 测试主题">` → 根 div **不**含 `.user-bubble--text` class背景保持 `--bg-tertiary`浅色command card 内部样式不变
- Happy path@team 命令):渲染 `<UserBubble content="@team 测试">` → 同上,保持浅色背景
- Happy path文件附件渲染 `<UserBubble content="[文件][x.pdf](url)">` → 根 div **不**含 `.user-bubble--text` class背景保持 `--bg-tertiary`
- Dark mode切换到 dark mode → 普通文本消息 `.user-bubble--text` 计算样式 `background-color: rgb(251, 251, 250)``--color-primary: #fbfbfa``color: rgb(26, 26, 26)``--text-inverse: #1a1a1a`),自动反转
- 操作按钮工具栏:普通文本消息 hover → `.user-bubble__actions` 可见,在深色气泡左侧(`right: calc(100% + var(--space-2))`)显示,`--bg-secondary` 背景与深色气泡有视觉区分
- max-width 契约:普通文本消息 `max-width: 70%`command card 保持 `max-width: 100%``.user-bubble__command` 自身设置)
- 渲染契约:样式**不**使用 `!important`
- 渲染契约:样式**不**硬编码 `#1a1a1a / #ffffff` 等值(必须用 CSS 变量)
**Verification:**
- 启动 Tauri 客户端,发送普通文本消息 → 右侧显示深色圆角气泡,白字
- 发送 @board 命令 → 右侧显示浅色结构化命令卡片(保持现有样式)
- 发送 @team 命令 → 同上,保持浅色
- 切换 dark mode → 普通文本消息自动反转为浅色气泡 + 深色文字command card 不变
- 普通文本消息 hover → 操作按钮工具栏在气泡左侧可见
---
## Scope Boundaries
### Deferred to Follow-Up Work
- **后端按会话去重 `board_started`**:本次只做前端拦截,不改后端。如果未来需要在多端/多浏览器同步场景下做强一致再考虑后端校验origin 文档已声明 deferred
- **StickyModeHeader 顶部条的视觉细化**(徽章大小、专家头像间距、紫色边框粗细)不在本次范围。
- **BoardConclusionCard / DebateBannerCard / TeamPlanCard 等其他 board 模式组件的样式统一**:本次只改 `BoardBannerCard`,其他卡片样式留待后续迭代。
### Outside this product's identity
- 不调整方案 B 调色板(`expertIdentity.ts` 的 12 色 PALETTE和专家头像首字符规则。
- 不调整私董会后端流程(`BoardOrchestrator` / `chatStream` 事件顺序)。
- 不调整 AssistantText 的 markdown 渲染逻辑、代码高亮、表格样式。
## Risks & Dependencies
- **风险 1U3 气泡样式可能影响 `AssistantText` 内部代码块/表格的视觉对比度** — 缓解:测试场景明确要求代码块背景 `--code-bg` 不被覆盖;`AssistantText.vue:257-401` 的 `pre/hljs/code/table` 样式已用独立 token理论上不受影响。如出现对比度问题可在 `AssistantText.vue` 内微调 `--code-bg` 但**不**回滚 U3 的气泡。
- **风险 2U1 的 `selectConversation(newId, true)` 强制 reload 可能导致用户感知"卡顿"** — 缓解:`force=true` 仅在用户主动点击"新建会话"时触发,是用户预期行为;如确实卡顿,可考虑后续优化(不在本次范围)。
- **依赖 1`chatStore.createConversation()` 是同步函数(无返回值),但内部已自动设置 `currentConversationId`** — 实现时需注意:先 `createConversation()` 再读 `chatStore.currentConversationId` 拿到新 id不能直接 `const newId = await chatStore.createConversation()`
- **依赖 2`BoardState.status` 当前联合类型为 4 个值,未来若新增 `forming / executing / synthesizing` 等中间状态需扩展"已存在"判断** — 在 `handleBoardClick` 中用 `if (status === 'discussing' || status === 'concluding')` 显式判断,未来扩展时只需补 `||`
## Open Questions
以下问题在 ce-doc-review 中由 reviewer 提出。**Round 12026-07-02 demo 确认)** 已决策 F1/F3/F4/C2/D4**Round 22026-07-02 复审)** 新增 7 个 findingsG1-G5 + D2-R2/D3-R2/D4-R2全部 append 到本 section 留待实现期处理。其中 **G1 挑战 Round 1 D4-方案1 决策**P0 load-bearing3 reviewer 一致 confidence 100 认为不可实现),**G5 挑战 Round 1 F1-A 决策**`#ffffff` 与 `#fbfbfa` 视觉不可区分)—— 实现期需优先处理这两个挑战。
### 已决策Round 1 — 2026-07-02 demo 确认)
- **F1feasibilityP1— 已决策 F1-A**U3 气泡背景与 inline code/table 背景冲突。**决策**:引入独立 token `--bg-message-bubble`light `#ffffff` / dark `#1f1f1f`)区分气泡背景与 inline code/table 背景(保持 `--bg-secondary: #fbfbfa`)。已固化到 R9、U3 Approach、Key Technical Decisions。**⚠️ Round 2 G5 挑战**`#ffffff` 与 `#fbfbfa` 色差 <1% 低于 JND `#ffffff` `--bg-primary` 相同导致气泡仅靠边框可见——见下方 G5
- **F3feasibilityP2— 已决策 F3-A**U2 删除 `BoardBannerCard``experts` prop 后 `useMessageRenderer.ts:145` 的 fallthrough 风险。**决策**:在 U2 Files 列表中加 `useMessageRenderer.ts`,删除 `board_banner` case 中 `experts` prop 传参。已固化到 U2 Files、U2 Approach、U2 Test scenarios。
- **F4feasibilityP2— 已决策 F4-A**U3 气泡包裹 `BoardConclusionCard` 的嵌套冲突。**决策**:气泡选择器排除 `board_conclusion` 类型,`BoardConclusionCard` 保留自身 card chrome 不被气泡包裹。已固化到 R14、U3 Approach、U3 Test scenarios、Key Technical Decisions。**⚠️ Round 2 G2/G3 挑战**`:has()` 事实错误 + `messageType` prop 选项需 ChatMessage.vue不在 Files——见下方 G2/G3。
- **C2coherenceP2— 已应用 safe_auto**R7 表述已修订为"删除 `.board-banner-card` 的重样式background/border/border-radius/box-shadow`__bar / __chip` 等重样式类,保留 `.board-banner-card` 容器(仅 margin/padding+ `__title / __meta` 最小样式"。
- **D4design-lensP2— 已决策 D4-方案1**U3 空内容气泡表现。**决策**:通过 `:empty` 选择器隐藏 `background / border / padding`,仅显示 thinking dots。已固化到 R15、U3 Approach、U3 Test scenarios、Key Technical Decisions。**⚠️ Round 2 G1 推翻**`:empty` 永不匹配AssistantText 总渲染根 div——见下方 G1实现期必须替换方案。
### 仍待实现期决定
#### Round 1 遗留
- **D1design-lensP1**U1 `selectConversation(newId, true)` 失败时无错误处理——modal 已关闭、currentConversationId 已变更、用户无反馈。待选方案try/catch + a-message error toast + 回滚 currentConversationId。
- **D2design-lensP2**U1 modal 关闭与 selectConversation resolve 之间的过渡期无 loading 反馈。待选方案modal 内"新建会话"按钮 loading spinner / 全局 loading overlay。
- **D3design-lensP2**U2 长 topic 文本无 overflow/wrap/truncation 策略。待选方案:`text-overflow: ellipsis` + `white-space: nowrap` / `word-break: break-word` / 多行 line-clamp。
#### Round 2 复审新增2026-07-02
**P0/P1 load-bearing挑战 Round 1 决策,实现期优先处理):**
- **G1feasibility + design-lens + adversarial 三方共识P0conf 100×3— 推翻 Round 1 D4-方案1**`:empty` 选择器永不匹配。`AssistantText.vue:1-6` 总是渲染根 `<div class="assistant-text">`(含 loading spinner / tool cards / routing div`useMessageRenderer.ts:361-362` 总是将 AssistantText 作为 slot 子组件挂载到 `.message-shell__content`,所以 `.message-shell__content` 在生产环境永远非空。D4-方案1 的 `:empty` CSS 规则不会生效thinking 阶段会显示空气泡矩形(正是 D4-方案1 要防止的 UX 缺陷。plan 的测试场景"空 slot → `:empty` 生效"用真空 slot 不反映生产。**待选方案**
- (a) MessageShell 加 `isEmpty` computed`!message.content && !message.thinking && toolCalls.length === 0`+ `.message-shell--empty` class 覆盖气泡样式 —— 推荐,最简单
- (b) 用 `:has(.assistant-text__loading:only-child)` 检测纯 loading 状态 —— 脆弱,不推荐
- (c) AssistantText 真空时不渲染根 div —— 高风险,可能破坏 ThinkingBlock / ToolCallCard
- 实现期需选择 (a)/(b)/(c) 并更新 R15、U3 Approach 第二步 CSS、U3 Test scenarios、Key Technical Decisions。
- **G5adversarial + feasibility residualP2conf 75— 挑战 Round 1 F1-A**`#ffffff`纯白vs `#fbfbfa`off-white色差 <1% 低于人眼 JND~2.3 `#ffffff` `--bg-primary: #ffffff`页面背景相同导致气泡仅靠 1px `--border-color: #ededec` 边框可见AE6"视觉可区分"断言可能不成立origin 文档描述为"浅灰圆角矩形气泡" `#ffffff` 实际是白色。**待选方案**
- (a) `--bg-message-bubble` light 改为 `#f7f7f5`(实际浅灰,与 `--bg-tertiary` 一致)—— 视觉符合 origin 描述
- (b) 保持 `#ffffff` 但修订 AE6 + U3 Test scenarios "视觉可区分" 为"token 级解耦(未来兼容),视觉差异 sub-JND"
- (c) 改用 `#f3f4f6` 等更明显灰值
- 实现期需选择 (a)/(b)/(c) 并更新 R9、U3 Approach、AE6、tokens.css 值。
**P2 gated_auto事实修正 + 决策落地):**
- **G2adversarial + feasibility residualP2conf 75— Tauri `:has()` 事实错误**plan U3 Approach 第 202 行称"Tauri WebView 基于 Chromium 111+ 支持",实际 Tauri 2.x macOS 用 WKWebViewSafari 引擎Linux 用 WebKitGTK仅 Windows 用 WebView2Chromium。`:has()` 在 Safari 15.4+2022-03/ WebKitGTK 2.35+ / Chromium 105+ 支持,实际可用但 plan 推理错误。**Fix**:修正 U3 Approach 第 202 行 + Key Technical Decisions F4-A 段为"Tauri 2.x macOS 用 WKWebViewSafari 15.4+ 支持 `:has()`Linux WebKitGTK 2.35+Windows WebView2 Chromium 105+。若需支持更老 macOS优先用 `messageType` prop 方案"。
- **G3coherence + adversarialP2conf 75— F4-A `:has()` vs `messageType` prop 决策悬而未决 + ChatMessage.vue 缺失**plan 对 F4-A 结论排除提供两个选项(`:has(.board-conclusion-card)` 选择器 OR `messageType` prop + `.message-shell--conclusion` class但不决策且三处描述不一致Key Technical Decisions 用 `:not(:has())`U3 Approach 用"或"U3 CSS 用两者 comma-joined。若选 `messageType` prop需修改 ChatMessage.vue 传递 `:message-type="spec.type"`,但 ChatMessage.vue 不在 U3 Files 列表。**Fix**:实现期决策用 `messageType` prop更稳健无 CSS 兼容风险),添加 `src/agentkit/server/frontend/src/components/chat/ChatMessage.vue` 到 U3 Files更新 Key Technical Decisions + U3 Approach + U3 CSS 三处统一为 `messageType` prop 方案。
**P2 manual设计决策**
- **D2-R2design-lensP2conf 75— U4 focus ring 在深色气泡上不可见**`UserBubble.vue:304` 用 `var(--accent-primary, #1a1a1a)`,但 `--accent-primary` 在 tokens.css 未定义fallback `#1a1a1a``--color-primary` 相同。U4 深色气泡(`--color-primary: #1a1a1a` 背景)上 focus ring 不可见WCAG 1.4.11 违规。**待选方案**(a) 定义 `--accent-primary` token`#3b82f6` light / `#60a5fa` dark(b) 改用 `--color-info` / `--accent-team`(c) 不在本次处理pre-existing bugU4 加剧但不引入)。
- **D3-R2design-lensP2conf 75— U3 dark mode 代码块对比度不足**dark mode 下 `--code-bg: #11111b`tokens.css:255vs `--bg-message-bubble: #1f1f1f` 仅差 ~14 亮度级代码块可能融入气泡。light mode 对比度足够(`#1e1e2e` on `#ffffff`)。**待选方案**(a) 加 dark mode 专属测试场景验证对比度;(b) 调整 `--bg-message-bubble` dark 值为 `#252525`(接近 `--bg-elevated`(c) 给 `pre``border: 1px solid var(--border-color)`
- **D4-R2design-lensP2conf 75— U2 简化 banner 缺乏视觉分隔**:删除所有 chrome 后,两行纯文本 bannerfont-md semibold + font-xs tertiary可能融入消息流失去"section divider"作用。BankOutlined 图标删除后无视觉锚点。**待选方案**(a) 保留 2px 左边框 `--accent-board`(匹配私董会身份,克制);(b) 加小色点前缀;(c) 纯排版足够("私董会 —" 文本前缀即锚点)。
## System-Wide Impact
- **前端用户**:所有 Tauri 客户端和 Web GUI 用户将看到 (a) 私董会按钮在已有私董会的会话中弹提示而非 modal(b) 私董会开始消息显示为简洁两行文本;(c) 所有 assistant 消息有浅灰圆角气泡(`--bg-message-bubble: #ffffff`board_conclusion 例外(保留自身 card(d) thinking 阶段不显示空气泡。视觉变化明显但符合方案 B 整体取向。
- **后端/运维**:无影响(不动后端、不动部署、不动配置)。
- **其他团队**:无影响(仅前端 Vue 组件)。
## Acceptance Examples
- AE1用户在已存在 `status='discussing'` 私董会的会话中点击"私董会"按钮 → 弹出 a-modal 提示"当前会话已存在私董会",点击"新建会话"后自动跳转到新空会话boardState 清空。
- AE2用户在新会话中发起私董会 → 第一条消息显示为两行纯文本"私董会 — {topic}" + "轮次:第 1/5 轮",无卡片背景/图标/进度条。
- AE3用户发起私董会专家依次发言 → 每条专家发言board_speech显示为彩色圆形头像 + 粗体名字 + "第 N 轮 · 专家" + 浅灰圆角矩形气泡包裹内容。
- AE4用户发起普通 chat不涉及私董会→ assistant 消息有浅灰圆角气泡user 普通文本消息有深色右对齐气泡U4
- AE5用户切换到 dark mode → 浅灰气泡自动变为深色(`--bg-message-bubble: #1f1f1f`),代码块/表格对比度保持可读。
- AE6F1-Aassistant 消息气泡内含 inline code → inline code 背景 `--bg-secondary: #fbfbfa` 与气泡背景 `--bg-message-bubble: #ffffff` 视觉可区分。
- AE7F4-A私董会结论消息board_conclusion`BoardConclusionCard` 保留自身 card chromebackground/border/shadow**不**被气泡包裹,无嵌套冲突。
- AE8D4-方案1assistant 消息在 thinking 阶段(空内容)→ 不显示空气泡矩形,仅显示三点动画;内容流入后自动出现气泡。
- AE9U4用户发送普通文本消息 → 右侧显示深色圆角气泡(`--color-primary` 背景 + 白字max-width 70%
- AE10U4用户发送 @board 命令 → 右侧显示浅色结构化命令卡片(保持现有 `--bg-tertiary` 背景),不受深色气泡影响
- AE11U4切换 dark mode → 普通文本消息自动反转为浅色气泡 + 深色文字,@board 命令卡片样式不变

View File

@ -0,0 +1,361 @@
---
date: 2026-07-02
type: fix
title: 修复私董会 transient state 残留 + ReAct 工具调用引导不足
status: in-progress
---
## Summary
收尾两个独立 bug(1) 前端 store-level transient state`boardState` / `debateState` / `collaborationState`)在 `createConversation` / `selectConversation` / `deleteConversation` 三个动作下的重置口径不一致,导致新建对话后私董会顶部标题残留、跨会话状态泄漏;(2) ReAct 引擎 `_build_tool_use_prompt` 规则 3 "如果不需要工具就能回答,直接回答即可" 给 LLM 留出偷懒窗口,且工具调用提示被后置的 tool section 覆盖,导致复杂需求(涉及外部数据 / 多步分析)下 LLM 倾向于直接回答而非调用 `web_search` / `baidu_search`。Bug 1 覆盖前端 3 个 action 路径对称重置Bug 2 仅做 L0提示规则调整L1工具描述扩展与 L2PLAN_EXEC 启用)按用户决策拆为独立 plan。
## Progress
| Unit | 状态 | 验证 | 提交 |
|------|------|------|------|
| U1 createConversation 重置 | done | 5 前端单测 pass | 7376005 |
| U2 selectConversation 条件重置 | done | 5 前端单测 pass | 7376005 |
| U3 deleteConversation 补全 | done | 5 前端单测 pass | 7376005 |
| U4 ReAct prompt 规则重排 | done | 6 后端单测 pass | 7376005 |
| U5 端到端验证测试 | done | 11 单测全 pass | 7376005 |
| U6 Bug 2 L4 真实 LLM smoke test | done | 3/4 tool-call + 1/1 direct pass | (本 commit) |
| U7 工作树未提交变更清理 | done | git status 干净 + vitest 138/139 pass | 9e2ccf5..44f4f1c |
Bug 2 状态L4 verifiedL0 规则重排在真实 LLM 调用下生效)
L4 smoke test 结果2026-07-02bailian-coding/qwen3.7-plus
- Probe #1 external_info: PASS8 次 web_search 调用99.9s
- Probe #2 realtime_data: ERROR120s 超时,非 LLM 不调用工具)
- Probe #3 multi_step: PASS8 次 web_search 调用62.6s
- Probe #4 realtime_data_simple: PASS3 次 web_search 调用23.8s
- Probe #5 no_tool_escape_hatch: PASS0 次工具调用直接回答4.2s
- 判定3/4 tool-call pass达阈值 ≥3/4+ 1/1 direct pass → L4 verified
PR: http://8.153.107.96/gitea/fischer/fischer-agentkit/pulls/17
ce-code-review: mode:agent, 无 actionable findings
ce-test-browser: agent-browser 已安装U6 用脚本直接验证,未走前端)
## Problem Frame
**Bug 1私董会顶部标题在新对话后残留**
根因([chatStore.ts:333-345](src/agentkit/server/frontend/src/stores/chatStore.ts#L333-L345)`createConversation()` 仅清空 `streamingSteps`**未重置** `stream.boardState.value` / `debateState.value` / `collaborationState.value`。`StickyModeHeader.vue:113-117` 的 `mode` computed 依赖 `chatStore.boardState` 渲染"私董会"模式(带旧专家头像行),新对话切换时状态未清空 → 旧私董会标题持续显示。
同源问题散落三处:
- `createConversation` (chatStore.ts:333-345) — 三个 state 全漏
- `selectConversation` (chatStore.ts:219-330) — 仅在 404 分支line 266-267重置 board/debate主流程 line 222 仅重置 collaboration正常切换不重置 board/debate
- `deleteConversation` (chatStore.ts:348-372) — line 364-365 重置 board/debate但**漏**了 `collaborationState`
**Bug 2Agent 面对复杂需求时倾向直接回答**
根因([react.py:1605-1616](src/agentkit/core/react.py#L1605-L1616)`_build_tool_use_prompt` 拼接的规则 3
```
3. 如果不需要工具就能回答,直接回答即可
```
此规则是 LLM 偷懒的合法依据。`base_prompt`[server/app.py:200-206](src/agentkit/server/app.py#L200-L206))已有正向引导"当你不确定事实信息、时事新闻或任何你不确信的话题时,你必须先使用搜索工具",但位置在 system prompt **前部**。**假设**:后注入的 tool section 可能在注意力分配上弱化了 base_prompt 的正向引导(此假设未经 ablation 验证,但规则重排本身的风险可控 — 见 KTD。`web_search` / `baidu_search` 工具描述也无"何时使用"的触发条件。
**预测Bug 1**:若在 `createConversation` 末尾加 `stream.boardState.value = null; stream.debateState.value = null; stream.collaborationState.value = null;``StickyModeHeader` 的 `mode` 返回 `null``v-if="mode"` 不渲染 → 标题消失;同源问题在 `selectConversation` / `deleteConversation` 也应统一口径。
**预测Bug 2**:若规则 3 措辞改为"涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具;仅在确实无需工具时可直接回答",且**重排为规则 1**(先说"何时必须",再列"何时不必"LLM 面对"GitHub Trending + 商业价值分析"类需求时调用 `web_search` 的概率显著提升(实证需 L4 真实 LLM smoke test 验证)。
## Requirements
**R1**Bug 1`createConversation` 末尾重置 `stream.boardState.value = null; stream.debateState.value = null; stream.collaborationState.value = null;`(与现有 `stream.clearConvSteps` 顺序一致,先 stream-owned transient 后 streaming 步骤清理,避免响应式 watcher 误触发)
**R2**Bug 1`selectConversation` 在切换到不同 conversation 时(即非 404 分支、亦非 `selectConversation(sameId)`),替换 line 222 的无条件 `collaborationState` 重置为顶部条件性三 state 重置(仅当 `prevConvId !== id` 时触发 `boardState` / `debateState` / `collaborationState` 置 null避免 force-reload 同一会话时误清空状态
**R3**Bug 1`deleteConversation` 删除后切换到下一个会话时,若当前会话非 `currentConversationId`,三个 state 不动;若当前会话被删,三个 state 全部置 null包括 `collaborationState`),与 `selectConversation` 口径一致
**R4**Bug 2 L0`_build_tool_use_prompt` 规则重排为:
```
1. 涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具
2. 每次只调用一个工具
3. 等待工具返回结果后再决定下一步
4. 仅在确实无需工具时可直接回答
5. 不要在回答中重复工具的输出,而是基于结果给出有用的总结
```
**R5**Bug 2 L0保持 base_prompt 不动(按用户决策 L1/L2 拆为独立 plan
**R6**Bug 2 L0不在本次改 web_search / baidu_search 工具 description
## Key Technical Decisions
- **三个 transient state 的重置时机选择**:选在 `createConversation` / `selectConversation` / `deleteConversation` action 内统一重置(而非在 `useChatStream` 初始化时按 conversationId 拆分 ref理由是当前 `useChatStream` 是 store-level 单例stream-owned state 全部是单一 ref。重构为"按 conversationId 拆分的 reactive map"是更大的架构变更(影响所有读取 `stream.boardState` 的组件),不在本 plan 范围内
- **selectConversation 切换检测**:以 `currentConversationId` 是否变化为重置条件(即 `id !== currentConversationId.value` 才重置),避免 `selectConversation(sameId, true)` 这种 force-reload 误清空
- **createConversation 中重置顺序**:先 transient stateboard/debate/collaboration`clearConvSteps`。理由transient state 派生渲染StickyModeHeader、CollaborationGraph清空它会触发对应组件卸载`clearConvSteps` 删除 streaming 步骤,触发 streaming UI 收尾。先 transient 后 streaming 保证 UI 状态先稳定再清理流
- **R4 规则重排而非删除**:保留原规则 3 的语义("无需工具时可直接回答")作为规则 4仅位置后移 + 措辞收敛。**理论选择**:本 plan 的修复理论是"位置优先" — 在同一 prompt block 内,靠前的规则获得更多注意力权重。此理论未经 ablation 验证但规则重排的回归风险可控trivial 输入走 DIRECT_CHAT 不进 ReAct。保留 rule 4 的真实理由:非 trivial 但无需外部工具的输入(如"总结这段文字"、"改写这段话")会进入 ReAct 循环,此类输入不应被强制工具化,需要一个 escape hatch。"你好/介绍下自己"类 L1 trivial 输入已走 DIRECT_CHAT不构成保留理由
- **不动 web_search / baidu_search description**:按用户决策推迟到独立 plan。L0 调整后若真实 LLM 行为改善有限再决定 L1
- **不在 L0 引入 PhasePolicy / PLAN_EXEC**L2启用 PLAN_EXEC 让复杂需求先输出计划再执行)按用户决策拆为独立 plan
- **测试策略**Bug 1 用 vitest 单元测试覆盖三个 action 的状态重置矩阵Bug 2 L0 用 pytest 单元测试断言 `_build_tool_use_prompt` 输出文本包含新规则 + 不包含旧规则 3 措辞;不强制跑真实 LLM依赖 API key 与网络,且不稳定),但写一个 mock-based test 验证 web_search 描述出现在 prompt 中
- **Bug 2 验收门槛**L0 文本断言通过后Bug 2 状态标记为"hypothesis applied, pending L4 verification"(非"fixed")。在 L1/L2 独立 plan 中包含真实 LLM smoke test用 5 个 probe query如"GitHub Trending 分析"、"最新 AI 新闻"等),对比 fix 前后 web_search 调用率,目标 ≥4/5 触发工具调用。L0 plan 不包含此 smoke test 但在 Verification 中显式记录此降级状态
## Implementation Units
### U1. createConversation 补全 transient state 重置
**Goal:** 修复 Bug 1 的第一处泄漏点 — `createConversation` 创建新会话时同步重置 `boardState` / `debateState` / `collaborationState` 三个 stream-owned ref。
**Requirements:** R1
**Dependencies:** 无
**Files:**
- src/agentkit/server/frontend/src/stores/chatStore.ts修改 line 333-345 `createConversation`
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts追加测试
**Approach:**
- 在 `createConversation``stream.clearConvSteps(newConversation.id)` 之前插入三行:
```ts
stream.boardState.value = null;
stream.debateState.value = null;
stream.collaborationState.value = null;
```
- 不动 `stream.collaborationState` 已有逻辑line 222 在 `selectConversation` 顶部重置)
- 不动 `pendingConversations` / `pendingLastUsedAt`(与本 bug 无关)
**Patterns to follow:**
- `chatStore.ts:264-281` 的 404 fallback 中 `stream.boardState.value = null; stream.debateState.value = null;` 的写法line 266-267— 同样模式
- `chatStore.ts:222``stream.collaborationState.value = null;` — 单行重置
**Test scenarios:**
- Happy path`createConversation()` 后,`chatStore.boardState === null` 且 `chatStore.debateState === null``chatStore.collaborationState === null`
- Edge case`selectConversation` 一个含 board_started 的旧会话boardState 非 null`createConversation()` → 三个 state 全为 null
- Edge case`createConversation()` 后 `currentConversationId` 指向新会话 ID且新会话的 streamingStepsByConv entry 被清空(已有 `stream.clearConvSteps` 行为,回归测试)
**Verification:** `cd src/agentkit/server/frontend && npm run test:unit -- --reporter=verbose 2>&1 | grep -E "chatStore|transient"` 通过;手动验证:先开 @board再点新建对话StickyModeHeader 不再显示私董会专家头像行
### U2. selectConversation 统一 transient state 重置口径
**Goal:** 修复 Bug 1 的第二处泄漏点 — `selectConversation` 从带 boardState 的会话切到其他会话时残留旧 boardState同时与 `createConversation` / `deleteConversation` 口径对齐。
**Requirements:** R2
**Dependencies:** 无(与 U1 并行可合入)
**Files:**
- src/agentkit/server/frontend/src/stores/chatStore.ts修改 line 219-330 `selectConversation`
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts追加测试
**Approach:**
- 在 `selectConversation` 顶部 line 220 `currentConversationId.value = id` 之后,判断是否切换到不同会话:
```ts
const isSwitching = currentConversationId.value !== id; // 注意line 220 已写入 id此处判断为切换条件需对比旧值
```
line 220 已先写入新 id所以需要先把旧 id 缓存到临时变量,再 line 220 写入新 id再判断
- 若 isSwitching 为 true 且新会话不含 board_started`restoreBoardStateFromMessages` 返回 null三个 state 置 null
- 由于后续 line 307 `stream.boardState.value = restoreBoardStateFromMessages(...)` 会覆盖,所以"先置 null 再被覆盖"是安全的
- 但若旧会话的 boardState 包含 stream-derived 数据(如 liveColorByName可能丢失 — 实际上 boardState.value 被整体覆盖为新对象,旧 stream-derived map 在 `allExperts` computed 中会基于新 boardState 重新构建([StickyModeHeader.vue:160-184](src/agentkit/server/frontend/src/components/chat/StickyModeHeader.vue#L160-L184)),所以无副作用
- 简化方案:直接删除 line 222 的 `stream.collaborationState.value = null;` 无条件重置,替换为 line 219 顶部统一三行:
```ts
const prevConvId = currentConversationId.value;
currentConversationId.value = id;
if (prevConvId !== id) {
stream.boardState.value = null;
stream.debateState.value = null;
stream.collaborationState.value = null;
}
```
- 保留 404 分支line 258-281的现有逻辑404 时 `boardState` / `debateState` 也置 nullline 266-267与主流程口径一致
**Patterns to follow:**
- `chatStore.ts:222` 已有无条件 `collaborationState` 重置 — 升级为条件性三 state 重置
- 404 分支 line 264-281 的多 state 重置 + 切换到下一个会话模式
**Test scenarios:**
- Happy path从有 boardState 的会话 A 切到会话 B无 boardState`boardState === null` `debateState === null` `collaborationState === null`
- Edge case从会话 A 切回 Aforce-reload 同一 id→ 三个 state 保持原值(不被无脑清空)
- Edge case从会话 A 切到会话 B也无 boardState→ 三个 state 保持 null无变化也无副作用
- Edge case404 后 `createConversation()` 流程(已有 fallback 测试)— 三个 state 全 null
**Verification:** `cd src/agentkit/server/frontend && npm run test:unit` 通过;手动验证:开 @board再点另一普通会话StickyModeHeader 切到普通模式(不显示私董会头像)
### U3. deleteConversation 补全 collaborationState 重置
**Goal:** 修复 Bug 1 的第三处泄漏点 — `deleteConversation` 删除当前会话时漏了 `collaborationState` 重置,与其他两个 action 口径对齐。
**Requirements:** R3
**Dependencies:** 无(与 U1/U2 并行可合入)
**Files:**
- src/agentkit/server/frontend/src/stores/chatStore.ts修改 line 362-371 `deleteConversation` 分支)
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts追加测试
**Approach:**
- 在 `deleteConversation``if (currentConversationId.value === id)` 分支 line 364 之后追加一行:
```ts
stream.collaborationState.value = null;
```
- 与现有 line 364-365 的 board/debate 重置并列
- 不影响"删除非当前会话"分支line 357 仅从列表移除,三个 state 不变 — 这是正确行为,因为当前会话不切换)
**Patterns to follow:**
- `chatStore.ts:364-365``stream.boardState.value = null; stream.debateState.value = null;` 已有模式
**Test scenarios:**
- Happy path当前会话有 collaborationState来自 collaboration_graph 消息)→ `deleteConversation(currentId)``collaborationState === null`
- Edge case删除非当前会话 → 当前会话的三个 state 不变(无副作用)
- Edge case删除当前会话后自动 `createConversation()` → 三个 state 全 null与 U1 联动)
**Verification:** `cd src/agentkit/server/frontend && npm run test:unit` 通过
### U4. ReAct _build_tool_use_prompt 规则重排 + 措辞调整
**Goal:** 修复 Bug 2 L0 — 重排 `_build_tool_use_prompt` 规则列表,让"何时必须使用工具"排在"何时可以不用工具"之前,并收窄规则 3 的措辞,去除"偷懒窗口"。
**Requirements:** R4, R5, R6
**Dependencies:** 无
**Files:**
- src/agentkit/core/react.py修改 line 1605-1616 `_build_tool_use_prompt` 返回的 rules 字符串)
- tests/unit/test_react_engine.py追加测试 `TestReActToolUsePromptRules`
**Approach:**
- 修改 `return (` 起的多行字符串中规则部分:
- 现有规则 3 改为规则 4措辞从"如果不需要工具就能回答,直接回答即可"改为"仅在确实无需工具时可直接回答"
- 现有规则 1 改为规则 2语义不变"每次只调用一个工具"
- 现有规则 2 改为规则 3语义不变"等待工具返回结果后再决定下一步"
- 现有规则 4 改为规则 5语义不变"不要在回答中重复工具的输出"
- **新增规则 1**(在最前):"涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具"
- 不动 `core_tools` / `extended_tools` 渲染逻辑
- 不动 `_render_core_tools` / `_render_extended_tools` / `_maybe_add_tool_search`
- 不动 `system_prompt` 拼接line 608-611`_build_tool_use_prompt` 仍以同样方式被追加
**Patterns to follow:**
- `react.py:1605-1616` 现有规则结构 — 替换为 5 条而非删除
**Test scenarios:**
- Happy path调用 `_build_tool_use_prompt([web_search_tool, read_file_tool])` → 输出包含"必须使用工具"且规则序号正确1 在 2 前)
- Edge casetools 列表为空 → 走 fast-path不调用 `_build_tool_use_prompt`),无变化
- 文本断言:输出不包含"如果不需要工具就能回答,直接回答即可"(旧规则 3
- 文本断言:输出包含"涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具"(新规则 1
- 文本断言:输出包含 `<tool_use>` XML 格式示例(保持向后兼容)
**Verification:** `pytest tests/unit/test_react_engine.py -k ToolUsePromptRules` 通过;`pytest tests/unit/test_react_engine.py` 全套通过(不破坏现有 200+ 测试)
### U5. 端到端验证测试Bug 1 + Bug 2 联动)
**Goal:** 写一个端到端测试覆盖 Bug 1 的前端 store 行为链 + Bug 2 的后端 prompt 文本,验证两个 fix 在测试套件中都被回归保护。
**Requirements:** 全部 R1-R6
**Dependencies:** U1, U2, U3, U4
**Files:**
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts追加 `describe('transient state reset matrix')` 块)
- tests/unit/test_react_engine.py追加 `describe('Bug 2 L0 prompt rules')` 块)
**Approach:**
- **Bug 1 联动测试**:在 chatStore.test.ts 写一个"建私董会 → 切新对话 → 切回旧私董会"三步流程,断言中间步骤的三个 state 全为 null最终回到旧私董会时通过 `restoreBoardStateFromMessages` 重建(注意:此 case 测的是 store-level 状态切换,不依赖后端响应)
- **Bug 2 联动测试**:在 test_react_engine.py 写一个"注册 web_search 工具 → 调 `_build_tool_use_prompt` → 断言 prompt 文本"测试,验证新规则 1 出现在 prompt 头部、web_search 工具描述完整(包含 description + parameters
- 不跑真实 LLM依赖 API key仅文本层断言
**Patterns to follow:**
- `chatStore.test.ts:18-81``boardStartedMsg` / `speechMsg` / `conclusionMsg` fixture 模式
- `test_react_engine.py:402-420``TestReActSystemPrompt` 模式mock gateway + 调 execute + 断言 messages
**Test scenarios:**
- Bug 1 三步流程:建私董会(注入 board_started fixture`createConversation()` → 断言三 state null → `selectConversation(originalId)` → 断言 boardState 重建
- Bug 1 跨 session建会话 A 含 boardState → `selectConversation(B)`B 无 board→ 断言三 state null → `selectConversation(A)` 重新触发 restore → 断言 boardState 重建
- Bug 2 规则顺序:调 `_build_tool_use_prompt` → 用 regex `r'1\.[^2]*2\.'` 断言规则 1 出现在规则 2 之前
- Bug 2 web_search 描述:调 `_build_tool_use_prompt([web_search_tool])` → 断言输出包含 "搜索互联网信息"description 内容)
**Verification:** `cd src/agentkit/server/frontend && npm run test:unit` + `pytest tests/unit/test_react_engine.py` 全部通过
### U6. Bug 2 L4 真实 LLM smoke test 验证
**Goal:** 验证 U4 的 L0 规则重排在真实 LLM 调用中是否生效 — Agent 面对复杂需求(外部信息 / 实时数据 / 多步分析)时是否调用 `web_search` 而非直接回答。将 Bug 2 状态从 "hypothesis applied, pending L4 verification" 升级为 "verified" 或回退并触发 L1/L2。
**Requirements:** R4 验证闭环
**Dependencies:** U4已 donePR #17 合入后执行
**Files:**
- tests/manual/test_react_l4_smoke.py新建手动 smoke test 脚本,不进 CI
**Approach:**
- 准备 5 个 probe query覆盖外部信息 / 实时数据 / 多步分析 / 不确定事实 / 混合类型:
1. "收集 GitHub Trending 前 10 个项目信息并分析商业价值"(原 bug 复现 query
2. "最新 AI 领域有什么重要新闻?"(实时数据)
3. "对比 React 和 Vue 3 在大型项目中的性能差异"(多步分析 + 外部信息)
4. "今天上海天气怎么样?"(实时数据,简单)
5. "请帮我总结这段文字:..."(无需工具,验证 escape hatch 规则 4 仍有效)
- 对每个 query 跑 `ReActEngine.execute()`,记录是否触发 `web_search` tool call
- query 1-4 期望触发工具调用≥3/4 pass 算通过query 5 期望不触发
- 如通过率 < 3/4触发 L1工具描述扩展并创建独立 plan
- 使用 `agent-browser` 打开 http://localhost:15173 进行前端层验证(可选):在 chat 中输入 probe query观察是否出现 tool_call step
**Test scenarios:**
- probe query 1-4ReAct 循环中至少出现一次 `web_search` tool call
- probe query 5ReAct 循环中无 tool call直接回答
- 回归断言U4 单测仍 passL0 文本未变)
**Verification:** `python3 tests/manual/test_react_l4_smoke.py` 输出报告5 个 query 的 tool call 统计 + pass/fail 判定。通过后更新本 plan Progress 表 U6 状态为 doneBug 2 状态升级为 "L4 verified"
### U7. 工作树未提交变更清理
**Goal:** 清理工作树中 32 个来自前序 session 的未提交变更,按 concern 分组提交,使工作树恢复干净状态。避免后续开发时 diff 噪声干扰 review。
**Requirements:** 无(工程治理)
**Dependencies:** 无(可与 U6 并行)
**Files (按 concern 分组):**
| 分组 | 文件 | 来源 session | 建议提交方式 |
|------|------|-------------|-------------|
| A. expert avatar emoji 移除 | configs/experts/*.yaml (15 个) | emoji 移除 plan (2026-07-02-001) | 单独 commit `refactor: expert avatar 改为首字符` |
| B. dev 环境配置 | docker-compose.yaml, scripts/dev-start.sh, docker-compose.dev.yml, .env.dev, src/agentkit/server/config.py | dev 环境修复 session | 单独 commit `fix: dev 环境配置 + 端口隔离` |
| C. board 元数据持久化 | src/agentkit/experts/board_orchestrator.py, src/agentkit/server/routes/chat.py | 私董会持久化修复 session | 单独 commit `fix: board_speech/round_summary 持久化 avatar/color 元数据` |
| D. 前端方案B + board UI | StickyModeHeader.vue, expertIdentity.ts, useMessageRenderer.ts, BoardRoundCard.vue, MessageShell.vue, Scene4BoardDiscussion.vue, chatStream.ts, types.ts, LoginView.vue | 方案B + 私董会限制 session | 需 review 后 commit部分可能已通过 PR #15 合入 |
| E. .understand-anything | .understand-anything/* (3 tracked + untracked) | knowledge graph 工具 | 加入 .gitignore 或单独 commit |
| F. 未跟踪 plan/brainstorm 文档 | docs/brainstorms/2026-07-02-*.md, docs/plans/2026-07-02-001-*.md | ce-plan/ce-brainstorm 产物 | commit 为决策记录 |
**Approach:**
- 对每组 `git diff HEAD -- <files>` 检查变更内容,确认无冲突
- 分组 commit每组 commit message 遵循 conventional commits 格式
- D 组需特别注意:检查是否与已合入的 PR #15 内容重叠,避免重复提交
- E 组(.understand-anything建议加入 .gitignore 而非 commit是工具生成的本地索引
- 如某组变更属于未完成的 feature如方案B则 stash 而非 commit待对应 plan 完成后再提交
**Test scenarios:**
- 每组 commit 后 `git status --short` 该组文件不再出现
- 全部完成后 `git status --short` 仅显示 untracked.understand-anything 等 gitignored 项)
- `npm run typecheck` + `ruff check src/` 仍通过(确认无回归)
**Verification:** `git status --short` 输出为空(或仅 gitignored 项);`npm run test:unit` + `pytest tests/unit/ -x -q` 全套通过
## Out of Scope
- **L1工具描述扩展**web_search / baidu_search / web_crawl 工具 description 添加"何时使用"触发关键词(如"需要最新互联网信息、新闻、Trending、股价时使用 web_search")。按用户决策推迟为独立 plan
- **L2PLAN_EXEC 启用)**:在 default agent 上注入 `PhasePolicy` 让复杂需求先输出 Plan 再执行。涉及 phase 配置、auto-advance 阈值、违规处理、phase event WS 协议。影响面较大,按用户决策拆为独立 plan
- **重构 stream-owned state 为按 conversationId 拆分**:当前是 store-level 单 ref导致每次"切换会话"必须显式重置。改为 `Map<conversationId, BoardState>` 可从根上消除泄漏,但影响所有 `chatStore.boardState` 读取点StickyModeHeader / useMessageRenderer 等),属于架构重构
- **base_prompt 调整**:保持原样,按用户决策
- **私董会生命周期 / Skill 路由策略 / tool registry 架构 / LLM gateway**:均不动
## Risks & Dependencies
- **R-U4-1**(低):规则重排可能影响现有 LLM 行为 — 风险点在某些 LLM 训练分布下"正向规则 1"可能让 LLM **过度工具化**trivial 输入也调工具)。缓解:测试覆盖 trivial 输入走 DIRECT_CHAT 不进 ReAct 循环request_preprocessor 已保证pytest 单元测试断言新规则在 prompt 中但不验证 LLM 行为
- **R-U1/2/3-1**state 重置顺序在**同步代码路径**createConversation、deleteConversation 重置块)中不会触发中间态渲染 — Vue 响应式批量更新在 microtask 中合并。但 `selectConversation` 有 async 路径:`await apiClient.getConversation(id)` 位于顶部 resetboardState → null和 post-fetch restoreline 307 `restoreBoardStateFromMessages`)之间。在 fetch 期间 Vue 会渲染一帧 boardState=null导致 StickyModeHeader 卸载再重载。这是期望行为("无旧数据残留"),非 race condition。切换两个私董会会话时 header 会短暂消失再出现 — 若需平滑过渡可在 follow-up 中加 skeleton placeholder
- **D-frontend-build**(低):前端改动需要重新 build static`npm run build:frontend`)才能被 backend 静态服务拾取。AGENTS.md 已记录此风险
## Deferred to Follow-Up Work
- L1web_search / baidu_search 工具 description 扩展(独立 plan— 仅在 U6 L4 smoke test 未通过时触发
- L2启用 PLAN_EXEC phase policy 处理复杂需求(独立 plan— 仅在 U6 通过但工具调用率仍不理想时触发
- 重构 stream-owned state 为按 conversationId 拆分(架构性,独立 plan
## Verification (per unit, summary)
- U1/U2/U3`cd src/agentkit/server/frontend && npm run test:unit` 全套通过;新增 3 个 `describe` 块共 8+ test cases
- U4`pytest tests/unit/test_react_engine.py -k ToolUsePromptRules` 通过;新增 1 个 test class 4-5 个 test cases已 done。**Bug 2 状态声明**L0 文本断言通过后 Bug 2 标记为 "hypothesis applied, pending L4 verification"(非 "fixed"),真实 LLM smoke test 在 U6 中执行
- U5完整套件通过端到端 4-5 个联动测试(已 done11 单测全 pass
- U6`python3 tests/manual/test_react_l4_smoke.py` 输出 5 个 probe query 的 tool call 统计≥3/4 外部信息 query 触发 web_search + query 5 不触发 → Bug 2 状态升级为 "L4 verified"
- U7`git status --short` 为空(或仅 gitignored 项);`npm run test:unit` + `pytest tests/unit/ -x -q` 全套通过
- 集成:`python3 -m pytest tests/unit/ -x -q`AGENTS.md 硬约束) + `cd src/agentkit/server/frontend && npm run test:unit` 通过
- Lint`ruff check src/ && ruff format src/`AGENTS.md 硬约束)通过
- TypeScript`cd src/agentkit/server/frontend && npm run typecheck` 通过

View File

@ -17,6 +17,14 @@
# Python >= 3.11, Node.js >= 18, Redis, PostgreSQL (均自动检查) # Python >= 3.11, Node.js >= 18, Redis, PostgreSQL (均自动检查)
# --tauri 需要Rust 工具链rustup / brew install rust # --tauri 需要Rust 工具链rustup / brew install rust
# #
# 端口(与 .env.dev 保持一致):
# 18001 — AgentKit 后端 API
# 18002 — Web GUI前端 + 内置静态服务)
# 15173 — Vite 开发服务器(--tauri 模式)
# 15174 — Vite HMR websocket
# 6381 — Redisagentkit 专属容器,避免与 pms-redis:6379 / geo_redis:6380 冲突)
# 5435 — PostgreSQL+pgvectoragentkit 专属容器,避免与 geo_db:5433 冲突)
#
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
@ -49,12 +57,14 @@ Fischer AgentKit — 本地开发环境启动
模式说明: 模式说明:
默认 Web 模式agentkit gui (前后端 + 内置静态服务) 默认 Web 模式agentkit gui (前后端 + 内置静态服务)
--tauri Tauri 模式:后端 API + Vite (:5173) + Tauri 桌面窗口 --tauri Tauri 模式:后端 API + Vite (:15173) + Tauri 桌面窗口
端口映射: 端口映射:
8000 — 后端 API 18001 — 后端 API
8002 — Web GUI / 前端静态服务 18002 — Web GUI / 前端静态服务
5173 — Vite 开发服务器(--tauri 模式) 15173 — Vite 开发服务器(--tauri 模式)
6381 — Redisagentkit 专属容器)
5435 — PostgreSQL+pgvectoragentkit 专属容器)
EOF EOF
} }
@ -112,11 +122,11 @@ print_status() {
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "$([[ $S_DEPS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_DEPS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 依赖检查" echo -e "$([[ $S_DEPS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_DEPS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 依赖检查"
echo -e "$([[ $S_ENV -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_ENV -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 环境配置" echo -e "$([[ $S_ENV -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_ENV -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 环境配置"
echo -e "$([[ $S_REDIS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_REDIS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Redis" echo -e "$([[ $S_REDIS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_REDIS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Redis (:6381)"
echo -e "$([[ $S_PG -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_PG -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") PostgreSQL" echo -e "$([[ $S_PG -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_PG -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") PostgreSQL (:5435)"
echo -e "$([[ $S_BACKEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_BACKEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 后端服务 (:8000)" echo -e "$([[ $S_BACKEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_BACKEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 后端服务 (:18001)"
if [[ $MODE == "gui" || $MODE == "tauri" ]]; then if [[ $MODE == "gui" || $MODE == "tauri" ]]; then
echo -e "$([[ $S_FRONTEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_FRONTEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 前端服务 (:8002)" echo -e "$([[ $S_FRONTEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_FRONTEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 前端服务 (:18002)"
fi fi
if [[ $MODE == "tauri" ]]; then if [[ $MODE == "tauri" ]]; then
echo -e "$([[ $S_TAURI -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_TAURI -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Tauri 客户端" echo -e "$([[ $S_TAURI -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_TAURI -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Tauri 客户端"
@ -180,30 +190,23 @@ check_redis() {
section "检查 Redis" section "检查 Redis"
set_status redis 1 set_status redis 1
if command -v redis-cli &>/dev/null && redis-cli ping 2>/dev/null | grep -q PONG; then # agentkit 专属容器映射到 6381避免与 pms-redis(6379) / geo_redis(6380) 冲突
ok "Redis 运行中" if redis-cli -h 127.0.0.1 -p 6381 ping 2>/dev/null | grep -q PONG; then
ok "Redis 运行中 (127.0.0.1:6381, agentkit 专属容器)"
set_status redis 2 set_status redis 2
return 0 return 0
fi fi
# Docker 方式 warn "Redis 未运行 (6381),启动 agentkit 专属 Docker 容器..."
local name="fischer-redis-dev" if docker compose -f docker-compose.yaml -f docker-compose.dev.yml up -d redis &>/dev/null; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then
ok "Redis 容器运行中"
set_status redis 2
return 0
fi
warn "Redis 未运行,尝试启动 Docker 容器..."
if docker run -d --name "$name" -p 6379:6379 redis:7-alpine &>/dev/null; then
sleep 2 sleep 2
if docker exec "$name" redis-cli ping 2>/dev/null | grep -q PONG; then if redis-cli -h 127.0.0.1 -p 6381 ping 2>/dev/null | grep -q PONG; then
ok "Redis Docker 容器启动成功 (:6379)" ok "Redis Docker 容器启动成功 (127.0.0.1:6381)"
set_status redis 2 set_status redis 2
return 0 return 0
fi fi
fi fi
fail "Redis 启动失败(请确保 Docker 运行中,或手动启动 Redis" fail "Redis 启动失败(请确保 Docker 运行中)"
set_status redis 3 set_status redis 3
return 1 return 1
} }
@ -212,30 +215,18 @@ check_postgres() {
section "检查 PostgreSQL" section "检查 PostgreSQL"
set_status pg 1 set_status pg 1
if lsof -i :5432 2>/dev/null | grep -q LISTEN; then # agentkit 专属容器映射到 5435避免与 geo_db(5433) 冲突
ok "PostgreSQL 已在 :5432 监听" if PGPASSWORD=agentkit psql -h 127.0.0.1 -p 5435 -U agentkit -d agentkit -c "SELECT 1" &>/dev/null; then
ok "PostgreSQL 运行中 (127.0.0.1:5435, agentkit 专属容器)"
set_status pg 2 set_status pg 2
return 0 return 0
fi fi
# Docker 方式 warn "PostgreSQL 未运行 (5435),启动 agentkit 专属 Docker 容器..."
local name="fischer-pg-dev" if docker compose -f docker-compose.yaml -f docker-compose.dev.yml up -d postgres &>/dev/null; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then
ok "PostgreSQL Docker 容器运行中"
set_status pg 2
return 0
fi
warn "PostgreSQL 未在 :5432 运行,尝试启动 Docker 容器..."
if docker run -d --name "$name" \
-p 5432:5432 \
-e POSTGRES_USER=agentkit \
-e POSTGRES_PASSWORD=agentkit \
-e POSTGRES_DB=agentkit \
pgvector/pgvector:pg15 &>/dev/null; then
sleep 3 sleep 3
if docker exec "$name" pg_isready -U agentkit &>/dev/null; then if PGPASSWORD=agentkit psql -h 127.0.0.1 -p 5435 -U agentkit -d agentkit -c "SELECT 1" &>/dev/null; then
ok "PostgreSQL Docker 容器启动成功 (:5432)" ok "PostgreSQL Docker 容器启动成功 (127.0.0.1:5435)"
set_status pg 2 set_status pg 2
return 0 return 0
fi fi
@ -281,17 +272,17 @@ start_backend() {
section "启动后端服务" section "启动后端服务"
set_status backend 1 set_status backend 1
info "启动后端 API (:8000)..." info "启动后端 API (:18001)..."
source .venv/bin/activate source .venv/bin/activate
agentkit serve --port 8000 & agentkit serve --port 18001 &
BACKEND_PID=$! BACKEND_PID=$!
# 等待健康检查就绪(最多 60 秒) # 等待健康检查就绪(最多 60 秒)
info "等待后端就绪..." info "等待后端就绪..."
local attempt=0 local attempt=0
while [[ $attempt -lt 60 ]]; do while [[ $attempt -lt 60 ]]; do
if curl -sf http://127.0.0.1:8000/api/v1/health &>/dev/null; then if curl -sf http://127.0.0.1:18001/api/v1/health &>/dev/null; then
ok "后端 API 就绪 (http://127.0.0.1:8000, PID $BACKEND_PID)" ok "后端 API 就绪 (http://127.0.0.1:18001, PID $BACKEND_PID)"
set_status backend 2 set_status backend 2
return 0 return 0
fi fi
@ -318,17 +309,17 @@ start_gui() {
section "启动 Web GUI" section "启动 Web GUI"
set_status frontend 1 set_status frontend 1
info "启动 Web GUI (:8002)..." info "启动 Web GUI (:18002)..."
source .venv/bin/activate source .venv/bin/activate
agentkit gui --port 8002 & agentkit gui --port 18002 &
GUI_PID=$! GUI_PID=$!
# 等待就绪 # 等待就绪
info "等待 Web GUI 就绪..." info "等待 Web GUI 就绪..."
local attempt=0 local attempt=0
while [[ $attempt -lt 60 ]]; do while [[ $attempt -lt 60 ]]; do
if curl -sf http://127.0.0.1:8002/api/v1/health &>/dev/null; then if curl -sf http://127.0.0.1:18002/api/v1/health &>/dev/null; then
ok "Web GUI 就绪 (http://127.0.0.1:8002, PID $GUI_PID)" ok "Web GUI 就绪 (http://127.0.0.1:18002, PID $GUI_PID)"
set_status frontend 2 set_status frontend 2
return 0 return 0
fi fi
@ -364,7 +355,7 @@ start_tauri() {
fi fi
info "启动 Tauri 桌面客户端..." info "启动 Tauri 桌面客户端..."
info " (Vite → :5173, 后端 API → :8000)" info " (Vite → :15173, 后端 API → :18001)"
cd "$FE_DIR" cd "$FE_DIR"
npm run tauri dev & npm run tauri dev &
TAURI_PID=$! TAURI_PID=$!
@ -400,7 +391,7 @@ stop_services() {
echo "" echo ""
info "正在停止所有服务..." info "正在停止所有服务..."
for port in 8000 8001 8002 5173; do for port in 18001 18002 15173 15174; do
local pid=$(lsof -ti :$port 2>/dev/null || true) local pid=$(lsof -ti :$port 2>/dev/null || true)
if [[ -n "$pid" ]]; then if [[ -n "$pid" ]]; then
kill $pid 2>/dev/null && ok "端口 $port 已停止" || true kill $pid 2>/dev/null && ok "端口 $port 已停止" || true
@ -474,14 +465,14 @@ if [[ $FAILED -eq 0 ]]; then
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo "" echo ""
if [[ $MODE == "gui" ]]; then if [[ $MODE == "gui" ]]; then
echo -e " Web GUI: ${GREEN}http://localhost:8002${NC}" echo -e " Web GUI: ${GREEN}http://localhost:18002${NC}"
echo " (在浏览器中打开,或直接在 http://localhost:8002 访问)" echo " (在浏览器中打开,或直接在 http://localhost:18002 访问)"
elif [[ $MODE == "tauri" ]]; then elif [[ $MODE == "tauri" ]]; then
echo -e " 后端 API: ${GREEN}http://localhost:8000${NC}" echo -e " 后端 API: ${GREEN}http://localhost:18001${NC}"
echo -e " Vite 热重载: ${GREEN}http://localhost:5173${NC}" echo -e " Vite 热重载: ${GREEN}http://localhost:15173${NC}"
echo " Tauri 桌面窗口应已自动打开" echo " Tauri 桌面窗口应已自动打开"
elif [[ $MODE == "serve" ]]; then elif [[ $MODE == "serve" ]]; then
echo -e " 后端 API: ${GREEN}http://localhost:8000${NC}" echo -e " 后端 API: ${GREEN}http://localhost:18001${NC}"
fi fi
echo "" echo ""
echo -e " ${YELLOW}按 Ctrl+C 停止所有服务${NC}" echo -e " ${YELLOW}按 Ctrl+C 停止所有服务${NC}"
@ -491,7 +482,7 @@ else
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo "" echo ""
echo -e " 诊断命令:" echo -e " 诊断命令:"
echo -e " 查看日志: ${CYAN}curl http://127.0.0.1:8000/api/v1/health${NC}" echo -e " 查看日志: ${CYAN}curl http://127.0.0.1:18001/api/v1/health${NC}"
echo -e " 停止服务: ${CYAN}bash scripts/dev-stop.sh${NC}" echo -e " 停止服务: ${CYAN}bash scripts/dev-stop.sh${NC}"
fi fi
echo "" echo ""

View File

@ -1609,10 +1609,11 @@ class ReActEngine:
'{"name": "工具名", "arguments": {"参数名": "参数值"}}\n' '{"name": "工具名", "arguments": {"参数名": "参数值"}}\n'
"</tool_use>\n\n" "</tool_use>\n\n"
"重要规则:\n" "重要规则:\n"
"1. 每次只调用一个工具\n" "1. 涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具\n"
"2. 等待工具返回结果后再决定下一步\n" "2. 每次只调用一个工具\n"
"3. 如果不需要工具就能回答,直接回答即可\n" "3. 等待工具返回结果后再决定下一步\n"
"4. 不要在回答中重复工具的输出,而是基于结果给出有用的总结\n\n" "4. 仅在确实无需工具时可直接回答\n"
"5. 不要在回答中重复工具的输出,而是基于结果给出有用的总结\n\n"
f"工具列表:\n\n{tools_text}{search_hint}" f"工具列表:\n\n{tools_text}{search_hint}"
) )

View File

@ -167,6 +167,8 @@ class BoardOrchestrator:
"round_summary", "round_summary",
{ {
"moderator_name": moderator.config.name, "moderator_name": moderator.config.name,
"moderator_avatar": moderator.config.avatar,
"moderator_color": moderator.config.color,
"content": summary, "content": summary,
"round": round_num, "round": round_num,
"continue": round_num < self._team.max_rounds, "continue": round_num < self._team.max_rounds,

View File

@ -555,7 +555,11 @@ def load_dotenv(
key, _, value = line.partition("=") key, _, value = line.partition("=")
key = key.strip() key = key.strip()
value = value.strip().strip("\"'") value = value.strip().strip("\"'")
if not key or key in os.environ: # Skip only if key is set to a non-empty value in the environment.
# An empty/whitespace-only value (e.g. from a shell template like
# `${VAR:-}` that expanded to nothing) is treated as "not set" so
# subsequent .env files can still provide a real value.
if not key or (key in os.environ and os.environ[key].strip()):
continue continue
# Apply allowlist if provided # Apply allowlist if provided
if prefixes is not None or exact is not None: if prefixes is not None or exact is not None:
@ -579,8 +583,10 @@ def load_config_with_dotenv(config_path: str | Path) -> ServerConfig:
This is the canonical way to load config in all CLI commands and app factory. This is the canonical way to load config in all CLI commands and app factory.
""" """
config_path = str(config_path) config_path = str(config_path)
dotenv = Path(config_path).parent / ".env" config_dir = Path(config_path).parent
load_dotenv(dotenv) # Load .env, .env.dev, .env.local in order (first non-empty value wins).
for candidate in (".env", ".env.dev", ".env.local"):
load_dotenv(config_dir / candidate)
return ServerConfig.from_yaml(config_path) return ServerConfig.from_yaml(config_path)

View File

@ -346,6 +346,8 @@ export interface IExpertSpeechData {
/** round_summary event payload */ /** round_summary event payload */
export interface IRoundSummaryData { export interface IRoundSummaryData {
moderator_name: string; moderator_name: string;
moderator_avatar?: string;
moderator_color?: string;
content: string; content: string;
round: number; round: number;
continue: boolean; continue: boolean;

View File

@ -37,7 +37,7 @@
class="sticky-mode-header__avatar" class="sticky-mode-header__avatar"
:class="{ 'sticky-mode-header__avatar--lead': expert.isLead }" :class="{ 'sticky-mode-header__avatar--lead': expert.isLead }"
type="button" type="button"
:style="{ borderColor: expert.color }" :style="{ background: expert.color, color: 'var(--text-inverse)', borderColor: expert.color }"
:aria-label="`查看 ${expert.name} 详情`" :aria-label="`查看 ${expert.name} 详情`"
:aria-expanded="openKey === expert.key" :aria-expanded="openKey === expert.key"
> >
@ -62,7 +62,7 @@
> >
<span <span
class="expert-list__avatar" class="expert-list__avatar"
:style="{ borderColor: expert.color }" :style="{ background: expert.color, color: 'var(--text-inverse)', borderColor: expert.color }"
>{{ expert.avatar }}</span> >{{ expert.avatar }}</span>
<span class="expert-list__name">{{ expert.name }}</span> <span class="expert-list__name">{{ expert.name }}</span>
<span v-if="expert.isLead" class="expert-list__tag">Lead</span> <span v-if="expert.isLead" class="expert-list__tag">Lead</span>
@ -153,11 +153,40 @@ const allExperts = computed<IExpertDisplay[]>(() => {
} }
if (mode.value === 'board') { if (mode.value === 'board') {
const list = chatStore.boardState?.experts ?? [] const list = chatStore.boardState?.experts ?? []
// 使 messages board_speech/
// round_summary expert_color MessageShell
// 退 boardState
// YAML color
const liveColorByName = new Map<string, string>()
const liveAvatarByName = new Map<string, string>()
const conv = chatStore.conversations?.find(
(c: { id: string; messages?: unknown[] }) => c.id === chatStore.currentConversationId,
)
if (conv?.messages) {
// walk from latest to earliest to capture the most recent identity
for (let i = conv.messages.length - 1; i >= 0; i--) {
const m = conv.messages[i] as {
message_type?: string
expert_name?: string
expert_color?: string
expert_avatar?: string
}
if (
(m.message_type === 'board_speech' ||
m.message_type === 'board_summary') &&
m.expert_name &&
!liveColorByName.has(m.expert_name)
) {
if (m.expert_color) liveColorByName.set(m.expert_name, m.expert_color)
if (m.expert_avatar) liveAvatarByName.set(m.expert_name, m.expert_avatar)
}
}
}
return list.map((e: IBoardExpert, idx: number) => ({ return list.map((e: IBoardExpert, idx: number) => ({
key: `${e.name}-${idx}`, key: `${e.name}-${idx}`,
name: e.name, name: e.name,
avatar: e.avatar, avatar: liveAvatarByName.get(e.name) ?? e.avatar,
color: e.color, color: liveColorByName.get(e.name) ?? e.color,
persona: e.persona, persona: e.persona,
description: e.is_moderator ? '主持人' : undefined, description: e.is_moderator ? '主持人' : undefined,
isLead: false, isLead: false,

View File

@ -14,20 +14,20 @@ export interface ExpertIdentity {
color: string color: string
} }
/** Non-blue palette. Order is stable — do not reshuffle (would break identity). */ /** Neutral slate palette (GitHub/professional style). Order is stable — do not reshuffle. */
const PALETTE: ReadonlyArray<string> = [ const PALETTE: ReadonlyArray<string> = [
"#d97706", // amber 600 "#4b5563", // slate 600
"#059669", // emerald 600 "#6b7280", // gray 500
"#7c3aed", // violet 600 "#9ca3af", // gray 400
"#db2777", // pink 600 "#374151", // gray 800
"#0e7490", // cyan 700 "#1f2937", // gray 900
"#65a30d", // lime 600 "#1e40af", // blue 800 (deep, non-vivid)
"#c2410c", // orange 700 "#166534", // green 800
"#9333ea", // purple 600 "#7c2d12", // orange 900
"#0891b2", // sky 600 "#581c87", // purple 900
"#a16207", // yellow 700 "#155e75", // cyan 800
"#be185d", // rose 700 "#92400e", // amber 800
"#15803d", // green 700 "#78716c", // stone 500
] ]
/** djb2-style string hash — stable across JS engines and reloads. */ /** djb2-style string hash — stable across JS engines and reloads. */

View File

@ -55,19 +55,19 @@ export function resolveMessageType(message: IChatMessage): MessageViewType {
switch (message.message_type) { switch (message.message_type) {
case 'plan_update': case 'plan_update':
return 'team_plan' return 'team_plan'
// 2026-07-01: board_* events render as plain assistant bubbles (streaming). // 2026-07-02: 恢复 board_* 事件走专用卡片路径落地方案B
// The user's first @board/@team message already shows a structured card // (中性灰阶头像/名字/颜色徽章/对话气泡 + 左侧3px色条)。
// (UserBubble) with topic + expert count + expert list; rendering // UserBubble 仅渲染用户输入的 @board 指令文本卡片,
// dedicated cards (BoardBannerCard / BoardRoundCard / BoardConclusionCard) // BoardBannerCard 渲染后端回送的 board_started 事件 (含专家 chip + 进度条)
// for the subsequent board_started/board_speech/board_summary/ // 二者职责不同,不构成重复。
// board_conclusion events duplicates the banner and breaks the natural
// chat flow. Fall through to 'assistant' so the content streams
// inline.
case 'board_started': case 'board_started':
return 'board_banner'
case 'board_speech': case 'board_speech':
return 'board_speech'
case 'board_summary': case 'board_summary':
return 'board_summary'
case 'board_conclusion': case 'board_conclusion':
return 'assistant' return 'board_conclusion'
case 'debate_started': case 'debate_started':
return 'debate_started' return 'debate_started'
case 'debate_argument': case 'debate_argument':
@ -149,42 +149,34 @@ export function useMessageRenderer(message: IChatMessage) {
} }
} }
case 'board_speech': case 'board_speech': {
const roleTag = message.board_role === 'moderator' ? '主持' : '专家'
return { return {
type, type,
shell: { shell: {
name: message.expert_name || '专家', name: message.expert_name || '专家',
meta: message.board_round ? `${message.board_round}${message.board_role === 'moderator' ? ' · 主持' : ''}` : time, meta: message.board_round ? `${message.board_round} · ${roleTag}` : `${roleTag}`,
avatar: message.expert_avatar, avatar: message.expert_avatar,
color: message.expert_color, color: message.expert_color,
}, },
component: BoardRoundCard, component: BoardRoundCard,
props: { props: {
name: message.expert_name || '专家',
avatar: message.expert_avatar || '',
color: message.expert_color,
round: message.board_round,
role: message.board_role === 'moderator' ? 'moderator' : 'expert',
content: message.content || '', content: message.content || '',
}, },
} }
}
case 'board_summary': case 'board_summary':
return { return {
type, type,
shell: { shell: {
name: message.expert_name || '主持人', name: message.expert_name || '主持人',
meta: message.board_round ? `${message.board_round} 轮 · 小结` : time, meta: message.board_round ? `${message.board_round} 轮 · 小结` : '小结',
avatar: message.expert_avatar, avatar: message.expert_avatar,
color: message.expert_color, color: message.expert_color,
}, },
component: BoardRoundCard, component: BoardRoundCard,
props: { props: {
name: message.expert_name || '主持人',
avatar: message.expert_avatar || '',
color: message.expert_color,
round: message.board_round,
role: 'summary',
content: message.content || '', content: message.content || '',
}, },
} }

View File

@ -1,19 +1,6 @@
<template> <template>
<div class="board-round-card" :class="[`board-round-card--${role}`]"> <div class="board-round-card">
<div class="board-round-card__header"> <AssistantText :message="textMessage" />
<span
class="board-round-card__avatar"
:style="avatarStyle"
>
{{ avatar || name.charAt(0) }}
</span>
<span class="board-round-card__name">{{ name }}</span>
<span v-if="round" class="board-round-card__round"> {{ round }} </span>
<span v-if="roleTag" class="board-round-card__role">{{ roleTag }}</span>
</div>
<div class="board-round-card__content">
<AssistantText :message="textMessage" />
</div>
</div> </div>
</template> </template>
@ -23,37 +10,16 @@ import type { IChatMessage } from '@/api/types'
import AssistantText from './AssistantText.vue' import AssistantText from './AssistantText.vue'
interface Props { interface Props {
name: string
avatar?: string
color?: string
round?: number
role?: 'moderator' | 'expert' | 'summary'
content: string content: string
} }
const props = withDefaults(defineProps<Props>(), { const props = defineProps<Props>()
avatar: '',
role: 'expert',
})
const roleTag = computed(() => {
const tags: Record<string, string> = {
moderator: '主持',
expert: '专家',
summary: '小结',
}
return tags[props.role] || ''
})
const avatarStyle = computed(() => {
if (props.color) {
return { background: props.color }
}
return { background: 'var(--accent-board)' }
})
// B: BoardRoundCard Board
// AssistantText ///
// MessageShell header
const textMessage = computed<IChatMessage>(() => ({ const textMessage = computed<IChatMessage>(() => ({
id: `board-${props.name}-${props.round || 0}`, id: `board-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
role: 'assistant', role: 'assistant',
content: props.content, content: props.content,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -64,63 +30,5 @@ const textMessage = computed<IChatMessage>(() => ({
<style scoped> <style scoped>
.board-round-card { .board-round-card {
width: 100%; width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-board);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
overflow: hidden;
}
.board-round-card--summary {
background: var(--accent-board-soft);
border-left-color: var(--accent-board);
}
.board-round-card__header {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border-color);
}
.board-round-card__avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: var(--radius-full);
font-size: 12px;
color: var(--text-inverse);
}
.board-round-card__name {
font-size: var(--font-sm);
font-weight: var(--font-weight-medium);
color: var(--text-primary);
}
.board-round-card__round {
font-size: var(--font-xs);
color: var(--text-tertiary);
}
.board-round-card__role {
margin-left: auto;
font-size: var(--font-xs);
padding: 1px 6px;
border-radius: var(--radius-sm);
background: var(--accent-board-soft);
color: var(--accent-board);
}
.board-round-card__content {
padding: var(--space-2) var(--space-3);
}
.board-round-card__content :deep(p) {
margin: 0;
} }
</style> </style>

View File

@ -20,13 +20,13 @@
</div> </div>
<div class="message-shell__body"> <div class="message-shell__body">
<div class="message-shell__header"> <div class="message-shell__header">
<!-- U4 R10: 专家身份 badge expert_color 高亮 expert_name流式期间及结束后均保留 --> <!-- 方案B: 专家名字始终是纯文本 (粗体)彩色身份由头像背景承担
不再渲染彩色 pill 名字徽章 -->
<span <span
v-if="expertName" v-if="expertName || name"
class="message-shell__expert-badge" class="message-shell__name"
:style="{ backgroundColor: expertColor || '#1890ff' }" :class="{ 'message-shell__name--expert': !!expertName }"
>{{ expertName }}</span> >{{ expertName || name }}</span>
<span v-else class="message-shell__name">{{ name }}</span>
<span v-if="meta" class="message-shell__meta">{{ meta }}</span> <span v-if="meta" class="message-shell__meta">{{ meta }}</span>
<span <span
v-if="streaming" v-if="streaming"
@ -152,20 +152,10 @@ withDefaults(defineProps<Props>(), {
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
} }
/* U4 R10: 专家身份 badge — 彩色 pill区别于普通 name 文本与 avatar */ /* 方案B: 专家名字 — 粗体文本 + 略深色,与普通 name 区分 (无 pill 背景) */
.message-shell__expert-badge { .message-shell__name--expert {
display: inline-flex; color: var(--text-primary);
align-items: center;
padding: 0 var(--space-2);
border-radius: var(--radius-full);
color: var(--text-inverse, #fff);
font-size: var(--font-xs);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
line-height: 1.6;
max-width: 12em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.message-shell__meta { .message-shell__meta {

View File

@ -14,37 +14,16 @@
/> />
</MessageShell> </MessageShell>
<MessageShell role="assistant" name="架构师老张" meta="第 1 轮" avatar="张" color="var(--accent-board)"> <MessageShell role="assistant" name="架构师老张" meta="第 1 轮 · 专家" avatar="张" color="var(--accent-board)">
<BoardRoundCard <BoardRoundCard :content="speech1.content" />
name="架构师老张"
avatar="张"
color="var(--accent-board)"
:round="1"
role="expert"
:content="speech1.content"
/>
</MessageShell> </MessageShell>
<MessageShell role="assistant" name="后端负责人小李" meta="第 1 轮" avatar="李" color="var(--accent-board)"> <MessageShell role="assistant" name="后端负责人小李" meta="第 1 轮 · 专家" avatar="李" color="var(--accent-board)">
<BoardRoundCard <BoardRoundCard :content="speech2.content" />
name="后端负责人小李"
avatar="李"
color="var(--accent-board)"
:round="1"
role="expert"
:content="speech2.content"
/>
</MessageShell> </MessageShell>
<MessageShell role="assistant" name="主持人" meta="第 1 轮 · 小结" avatar="主" color="var(--accent-board)"> <MessageShell role="assistant" name="主持人" meta="第 1 轮 · 小结" avatar="主" color="var(--accent-board)">
<BoardRoundCard <BoardRoundCard :content="summaryMessage.content" />
name="主持人"
avatar="主"
color="var(--accent-board)"
:round="1"
role="summary"
:content="summaryMessage.content"
/>
</MessageShell> </MessageShell>
<MessageShell role="assistant" name="主持人" meta="11:05"> <MessageShell role="assistant" name="主持人" meta="11:05">

View File

@ -217,9 +217,15 @@ export const useChatStore = defineStore("chat", () => {
/** Select a conversation by ID and load its messages */ /** Select a conversation by ID and load its messages */
async function selectConversation(id: string, force = false): Promise<void> { async function selectConversation(id: string, force = false): Promise<void> {
const prevConvId = currentConversationId.value;
currentConversationId.value = id; currentConversationId.value = id;
// P2 #10: 会话隔离 — 切换会话时重置 collaborationState避免跨会话数据泄漏。 // 会话隔离 — 切换会话时重置三个 transient state避免跨会话数据泄漏。
stream.collaborationState.value = null; // force-reload 同一会话时不重置,防止误清空。
if (prevConvId !== id) {
stream.boardState.value = null;
stream.debateState.value = null;
stream.collaborationState.value = null;
}
const conv = conversations.value.find((c) => c.id === id); const conv = conversations.value.find((c) => c.id === id);
// 本地临时会话尚未同步到服务端,跳过获取避免 404 // 本地临时会话尚未同步到服务端,跳过获取避免 404
@ -305,6 +311,28 @@ export const useChatStore = defineStore("chat", () => {
// 存在与否决定 status已结束 vs 仍在讨论中。Reload 后 BoardStatusView / StickyModeHeader // 存在与否决定 status已结束 vs 仍在讨论中。Reload 后 BoardStatusView / StickyModeHeader
// 才能正常显示专家列表和轮次,私信 0 人的现象也由此修复。 // 才能正常显示专家列表和轮次,私信 0 人的现象也由此修复。
stream.boardState.value = restoreBoardStateFromMessages(restoredConv?.messages ?? []); stream.boardState.value = restoreBoardStateFromMessages(restoredConv?.messages ?? []);
// 颜色一致性兜底board_speech / round_summary 消息如果缺失 expert_avatar
// 或 expert_color早期持久化未写入从 boardState.experts 补全,
// 保证 StickyModeHeader 头像和 MessageShell 头像颜色一致。
if (stream.boardState.value && restoredConv?.messages) {
const expertMap = new Map(
stream.boardState.value.experts.map((e) => [e.name, e]),
);
for (const m of restoredConv.messages) {
if (
(m.message_type === "board_speech" ||
m.message_type === "board_summary") &&
m.expert_name
) {
const expert = expertMap.get(m.expert_name);
if (expert) {
if (!m.expert_avatar) m.expert_avatar = expert.avatar;
if (!m.expert_color) m.expert_color = expert.color;
}
}
}
}
} }
/** Create a new empty conversation */ /** Create a new empty conversation */
@ -319,6 +347,10 @@ export const useChatStore = defineStore("chat", () => {
}; };
conversations.value.unshift(newConversation); conversations.value.unshift(newConversation);
currentConversationId.value = newConversation.id; currentConversationId.value = newConversation.id;
// 重置 transient state避免旧私董会/辩论/协作状态泄漏到新会话
stream.boardState.value = null;
stream.debateState.value = null;
stream.collaborationState.value = null;
stream.clearConvSteps(newConversation.id); stream.clearConvSteps(newConversation.id);
} }
@ -341,6 +373,7 @@ export const useChatStore = defineStore("chat", () => {
currentConversationId.value = null; currentConversationId.value = null;
stream.boardState.value = null; stream.boardState.value = null;
stream.debateState.value = null; stream.debateState.value = null;
stream.collaborationState.value = null;
if (conversations.value.length > 0) { if (conversations.value.length > 0) {
await selectConversation(conversations.value[0].id); await selectConversation(conversations.value[0].id);
} else { } else {

View File

@ -1296,14 +1296,17 @@ export function dispatchWsEvent(
(c) => c.id === conversationId, (c) => c.id === conversationId,
); );
if (!conv) break; if (!conv) break;
// Stable identity for the moderator, just like expert_speech. // Stable identity for the moderator. Prefer the event's
// moderator_avatar/moderator_color (added in 2026-07-02 so persistence
// has the same identity) and fall back to the boardState snapshot
// captured at board_started.
const moderator = state.boardState.value?.experts.find( const moderator = state.boardState.value?.experts.find(
(e) => e.name === summaryData.moderator_name, (e) => e.name === summaryData.moderator_name,
); );
const identity = resolveExpertIdentity( const identity = resolveExpertIdentity(
summaryData.moderator_name, summaryData.moderator_name,
moderator?.avatar, summaryData.moderator_avatar || moderator?.avatar,
moderator?.color, summaryData.moderator_color || moderator?.color,
); );
const summaryMsg: IChatMessage = { const summaryMsg: IChatMessage = {
id: generateId(), id: generateId(),

View File

@ -181,6 +181,20 @@ onMounted(async () => {
font-weight: 600; font-weight: 600;
} }
/* token AntD ConfigProvider token tauri
不稳定这里兜底强制使用项目主色#1a1a1a 近黑避免蓝色兜底 */
.login-submit.ant-btn-primary,
.login-submit.ant-btn-primary:hover,
.login-submit.ant-btn-primary:focus {
background-color: var(--color-primary, #1a1a1a);
border-color: var(--color-primary, #1a1a1a);
color: var(--text-inverse, #ffffff);
}
.login-submit.ant-btn-primary:hover {
background-color: var(--color-primary-hover, #2f2f2f);
border-color: var(--color-primary-hover, #2f2f2f);
}
.login-remember { .login-remember {
margin-bottom: 16px; margin-bottom: 16px;
} }

View File

@ -0,0 +1,172 @@
/**
* Transient state reset matrix tests (U5 / Bug 1).
*
* Verifies that createConversation / selectConversation / deleteConversation
* reset the three stream-owned transient state refs (boardState /
* debateState / collaborationState) to null preventing cross-conversation
* state leakage (e.g. private board header persisting into a new conversation).
*
* Mock strategy follows chat-phase.test.ts: mock apiClient + peer stores,
* use setActivePinia(createPinia()), dynamic-import the real useChatStore.
* All test conversations are is_local=true so no API calls fire.
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
// Mock apiClient so the store never touches the network.
vi.mock('@/api/client', () => ({
apiClient: {
getConversations: vi.fn().mockResolvedValue([]),
getConversation: vi.fn().mockResolvedValue({ id: '', title: '', messages: [] }),
deleteConversation: vi.fn().mockResolvedValue(undefined),
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
patch: vi.fn(),
},
}))
// Mock peer stores to avoid pulling their dependencies.
vi.mock('@/stores/team', () => ({ useTeamStore: vi.fn(() => null) }))
vi.mock('@/stores/documents', () => ({ useDocumentsStore: vi.fn(() => null) }))
vi.mock('@/stores/calendar', () => ({ useCalendarStore: vi.fn(() => null) }))
vi.mock('@/api/documents', () => ({ isDocumentMeta: vi.fn() }))
describe('transient state reset matrix', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('createConversation resets boardState / debateState / collaborationState to null', async () => {
const { useChatStore } = await import('@/stores/chatStore')
const store = useChatStore()
// Simulate a board meeting was active in the previous conversation.
store.boardState = { topic: 'stale', experts: [], max_rounds: 1, current_round: 0, status: 'discussing' } as never
store.debateState = { topic: 'stale' } as never
store.collaborationState = { contracts: [], notices: [], reviews: [], risks: [] } as never
store.createConversation()
expect(store.boardState).toBeNull()
expect(store.debateState).toBeNull()
expect(store.collaborationState).toBeNull()
})
it('selectConversation to a different conversation resets all three states', async () => {
const { useChatStore } = await import('@/stores/chatStore')
const store = useChatStore()
// Pre-populate two local conversations.
store.conversations = [
{ id: 'conv-a', title: 'A', messages: [], created_at: '', updated_at: '', is_local: true },
{ id: 'conv-b', title: 'B', messages: [], created_at: '', updated_at: '', is_local: true },
]
store.currentConversationId = 'conv-a'
// Simulate board state from conversation A.
store.boardState = { topic: 'board in A', experts: [], max_rounds: 1, current_round: 0, status: 'discussing' } as never
store.debateState = { topic: 'debate in A' } as never
store.collaborationState = { contracts: [], notices: [], reviews: [], risks: [] } as never
// Switch to conversation B (no board messages → restoreBoardStateFromMessages returns null).
await store.selectConversation('conv-b')
expect(store.boardState).toBeNull()
expect(store.debateState).toBeNull()
expect(store.collaborationState).toBeNull()
})
it('selectConversation force-reload same id does not clear via conditional reset', async () => {
const { useChatStore } = await import('@/stores/chatStore')
const { restoreBoardStateFromMessages } = await import('@/stores/chatStore')
// Build a conversation with a board_started message so restoreBoardStateFromMessages returns non-null.
const boardStartedMsg = {
id: 'msg-start',
role: 'assistant' as const,
content: '私董会开始:测试主题',
timestamp: '2026-07-01T10:00:00Z',
status: 'completed' as const,
message_type: 'board_started' as const,
board_started: {
team_id: 'team-1',
topic: '测试主题',
max_rounds: 3,
experts: [
{ name: 'Alice', avatar: 'A', color: '#888888', is_moderator: true, persona: '主持人' },
],
},
board_round: 0,
}
const store = useChatStore()
store.conversations = [
{ id: 'conv-a', title: 'A', messages: [boardStartedMsg], created_at: '', updated_at: '', is_local: true },
]
store.currentConversationId = 'conv-a'
// Initial select to populate boardState from messages.
await store.selectConversation('conv-a')
expect(store.boardState).not.toBeNull()
expect(store.boardState?.topic).toBe('测试主题')
// Force-reload the same conversation — boardState should be restored, not cleared.
await store.selectConversation('conv-a', true)
expect(store.boardState).not.toBeNull()
expect(store.boardState?.topic).toBe('测试主题')
// Sanity: restoreBoardStateFromMessages returns non-null for this fixture.
expect(restoreBoardStateFromMessages([boardStartedMsg])).not.toBeNull()
})
it('deleteConversation of current conversation resets all three states including collaborationState', async () => {
const { useChatStore } = await import('@/stores/chatStore')
const store = useChatStore()
// Create a local conversation and select it.
store.conversations = [
{ id: 'conv-a', title: 'A', messages: [], created_at: '', updated_at: '', is_local: true },
{ id: 'conv-b', title: 'B', messages: [], created_at: '', updated_at: '', is_local: true },
]
store.currentConversationId = 'conv-a'
// Simulate all three transient states are active.
store.boardState = { topic: 'board', experts: [], max_rounds: 1, current_round: 0, status: 'discussing' } as never
store.debateState = { topic: 'debate' } as never
store.collaborationState = { contracts: [], notices: [], reviews: [], risks: [] } as never
// Delete the current conversation — should reset all three states.
await store.deleteConversation('conv-a')
expect(store.boardState).toBeNull()
expect(store.debateState).toBeNull()
expect(store.collaborationState).toBeNull()
})
it('deleteConversation of a non-current conversation does not touch transient states', async () => {
const { useChatStore } = await import('@/stores/chatStore')
const store = useChatStore()
store.conversations = [
{ id: 'conv-a', title: 'A', messages: [], created_at: '', updated_at: '', is_local: true },
{ id: 'conv-b', title: 'B', messages: [], created_at: '', updated_at: '', is_local: true },
]
store.currentConversationId = 'conv-a'
const boardState = { topic: 'board in A', experts: [], max_rounds: 1, current_round: 0, status: 'discussing' as const }
store.boardState = boardState
store.debateState = { topic: 'debate' } as never
store.collaborationState = { contracts: [], notices: [], reviews: [], risks: [] } as never
// Delete a non-current conversation — states should be untouched.
await store.deleteConversation('conv-b')
expect(store.boardState).not.toBeNull()
expect(store.boardState?.topic).toBe('board in A')
expect(store.debateState).not.toBeNull()
expect(store.collaborationState).not.toBeNull()
})
})

View File

@ -337,6 +337,8 @@ async def _execute_board_meeting(
{ {
"message_type": "board_summary", "message_type": "board_summary",
"expert_name": event_data.get("moderator_name"), "expert_name": event_data.get("moderator_name"),
"expert_avatar": event_data.get("moderator_avatar"),
"expert_color": event_data.get("moderator_color"),
"board_round": event_data.get("round"), "board_round": event_data.get("round"),
"board_role": "summary", "board_role": "summary",
}, },

View File

@ -0,0 +1,295 @@
"""ReAct L4 真实 LLM smoke test (U6).
手动验证 U4 L0 规则重排`_build_tool_use_prompt` "何时必须使用工具"前置
在真实 LLM 调用下是否生效 Agent 面对复杂需求时是否调用 `web_search` 而非
直接回答
不进 CI依赖真实 LLM API key + 网络运行方式
python3 tests/manual/test_react_l4_smoke.py
判定标准plan U6
- Probe 1-4 期望触发 web_search3/4 pass 算通过
- Probe 5 期望不触发工具调用验证 escape hatch 规则 4 仍有效
- 通过 Bug 2 状态升级为 "L4 verified"
- 未通过 触发 L1工具描述扩展独立 plan
ponytail: 直接复用 cli.chat._build_gateway + server.app._create_provider
不重新实现 provider 注册逻辑升级路径抽到 shared 模块供 cli/server/manual
共用
"""
from __future__ import annotations
import asyncio
import sys
import time
from dataclasses import dataclass
from pathlib import Path
# 确保可以 import agentkit 包(脚本从仓库根目录运行)
ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(ROOT / "src"))
from agentkit.core.react import ReActEngine, ReActResult, ReActStep # noqa: E402
from agentkit.server.config import find_config_path, load_config_with_dotenv # noqa: E402
from agentkit.tools.web_search import WebSearchTool # noqa: E402
# ---------------------------------------------------------------------------
# Probe queries
# ---------------------------------------------------------------------------
@dataclass
class Probe:
"""单个 probe query 及其期望行为。"""
id: int
category: str # external_info / realtime_data / multi_step / uncertain / no_tool
query: str
expect_tool_call: bool # True: 期望触发 web_searchFalse: 期望直接回答
PROBES: list[Probe] = [
Probe(
id=1,
category="external_info",
query="收集 GitHub Trending 前 10 个项目信息并分析商业价值",
expect_tool_call=True,
),
Probe(
id=2,
category="realtime_data",
query="最新 AI 领域有什么重要新闻?",
expect_tool_call=True,
),
Probe(
id=3,
category="multi_step",
query="对比 React 和 Vue 3 在大型项目中的性能差异,给出具体数据",
expect_tool_call=True,
),
Probe(
id=4,
category="realtime_data_simple",
query="今天上海天气怎么样?",
expect_tool_call=True,
),
Probe(
id=5,
category="no_tool_escape_hatch",
query="请帮我总结下面这段文字人工智能AI是计算机科学的一个分支"
"它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出"
"反应的智能机器。AI 的研究包括机器人、语言识别、图像识别、自然语言"
"处理和专家系统等。",
expect_tool_call=False,
),
]
# ---------------------------------------------------------------------------
# Probe runner
# ---------------------------------------------------------------------------
def _count_tool_calls(result: ReActResult) -> tuple[int, list[str]]:
"""统计 trajectory 中的 tool_call 步骤,返回 (count, tool_names)。"""
tool_names: list[str] = []
for step in result.trajectory:
if step.action == "tool_call" and step.tool_name:
tool_names.append(step.tool_name)
return len(tool_names), tool_names
def _format_trajectory(steps: list[ReActStep]) -> str:
"""格式化 trajectory 用于报告输出。"""
lines: list[str] = []
for s in steps:
if s.action == "tool_call":
args_preview = str(s.arguments)[:80] if s.arguments else ""
lines.append(f" [{s.step}] tool_call: {s.tool_name}({args_preview})")
elif s.action == "final_answer":
preview = (s.content or "")[:120].replace("\n", " ")
lines.append(f" [{s.step}] final_answer: {preview}...")
else:
lines.append(f" [{s.step}] {s.action}")
return "\n".join(lines) if lines else " (empty)"
async def run_probe(engine: ReActEngine, probe: Probe) -> dict:
"""运行单个 probe返回结果字典。"""
print(f"\n{'='*60}")
print(f"Probe #{probe.id} [{probe.category}]")
print(f"Query: {probe.query[:80]}{'...' if len(probe.query) > 80 else ''}")
print(f"Expect tool_call: {probe.expect_tool_call}")
print(f"{'-'*60}")
messages = [{"role": "user", "content": probe.query}]
start = time.monotonic()
try:
result = await engine.execute(
messages=messages,
tools=[WebSearchTool()],
model="default",
agent_name="l4_smoke",
task_type="smoke_test",
)
except Exception as e:
elapsed = time.monotonic() - start
print(f"ERROR after {elapsed:.1f}s: {type(e).__name__}: {e}")
return {
"probe_id": probe.id,
"category": probe.category,
"expect_tool_call": probe.expect_tool_call,
"actual_tool_calls": 0,
"tool_names": [],
"status": "error",
"error": f"{type(e).__name__}: {e}",
"elapsed_s": elapsed,
"output_preview": "",
"trajectory": "",
}
elapsed = time.monotonic() - start
tool_count, tool_names = _count_tool_calls(result)
# 判定:实际触发工具 == 期望触发工具
actual_triggered = tool_count > 0
passed = actual_triggered == probe.expect_tool_call
# 特例query 5 期望无工具,但如果 LLM 调用了,也算"行为可观察"(只是不符 escape hatch 期望)
status = "pass" if passed else "fail"
output_preview = (result.output or "")[:200].replace("\n", " ")
traj_str = _format_trajectory(result.trajectory)
print(f"Status: {status.upper()} ({elapsed:.1f}s, {result.total_steps} steps)")
print(f"Tool calls: {tool_count} {tool_names}")
print(f"Output preview: {output_preview}")
print(f"Trajectory:\n{traj_str}")
return {
"probe_id": probe.id,
"category": probe.category,
"expect_tool_call": probe.expect_tool_call,
"actual_tool_calls": tool_count,
"tool_names": tool_names,
"status": status,
"error": None,
"elapsed_s": elapsed,
"output_preview": output_preview,
"trajectory": traj_str,
"total_steps": result.total_steps,
"total_tokens": result.total_tokens,
"react_status": result.status,
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
async def main() -> int:
"""运行所有 probe输出报告返回退出码0=pass, 1=fail"""
print("=" * 60)
print("ReAct L4 Smoke Test (U6)")
print("Verifies U4 L0 prompt rule rearrangement under real LLM calls")
print("=" * 60)
# 1. 加载 server config从 agentkit.yaml + env
print("\n[1/3] Loading server config...")
try:
config_path = find_config_path()
if not config_path:
print("FATAL: no agentkit.yaml found (./agentkit.yaml or ~/.agentkit/agentkit.yaml)")
return 2
server_config = load_config_with_dotenv(config_path)
except Exception as e:
print(f"FATAL: failed to load config: {type(e).__name__}: {e}")
return 2
if not server_config.llm_config.providers:
print("FATAL: no LLM providers configured (check agentkit.yaml / .env)")
return 2
providers_with_key = [
name for name, p in server_config.llm_config.providers.items()
if p.api_key
]
if not providers_with_key:
print("FATAL: no LLM providers with api_key set")
print("Set env vars (e.g. OPENAI_API_KEY) or configure agentkit.yaml")
return 2
print(f" Providers with key: {providers_with_key}")
print(f" Default model alias: {server_config.llm_config.model_aliases.get('default', '<unset>')}")
# 2. 构建 LLM gateway + ReAct engine
print("\n[2/3] Building LLM gateway + ReAct engine...")
from agentkit.cli.chat import _build_gateway
gateway = _build_gateway(server_config)
if not gateway.has_providers:
print("FATAL: gateway has no registered providers")
return 2
print(f" Gateway providers: {list(gateway._providers.keys())}")
engine = ReActEngine(
llm_gateway=gateway,
max_steps=8, # 留够步数让 LLM 多轮搜索
default_timeout=120.0, # 单 query 上限 2 分钟
enable_tool_search=False, # 简化:直接注入 web_search 完整描述
)
# 3. 运行所有 probe
print(f"\n[3/3] Running {len(PROBES)} probes...")
results: list[dict] = []
for probe in PROBES:
# 每次重置 engine 状态(避免 loop detection 跨 query 误判)
engine.reset()
result = await run_probe(engine, probe)
results.append(result)
# 4. 汇总报告
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"{'#':>3} {'Category':<24} {'Expect':>8} {'Actual':>8} {'Status':>6} {'Time':>6}")
print("-" * 60)
for r in results:
expect_str = "tool" if r["expect_tool_call"] else "direct"
actual_str = f"{r['actual_tool_calls']} tool" if r["actual_tool_calls"] > 0 else "direct"
print(
f"{r['probe_id']:>3} "
f"{r['category']:<24} "
f"{expect_str:>8} "
f"{actual_str:>8} "
f"{r['status']:>6} "
f"{r['elapsed_s']:>5.1f}s"
)
# 5. 判定
print("\n" + "-" * 60)
tool_probes = [r for r in results if r["expect_tool_call"]]
direct_probes = [r for r in results if not r["expect_tool_call"]]
tool_pass = sum(1 for r in tool_probes if r["status"] == "pass")
direct_pass = sum(1 for r in direct_probes if r["status"] == "pass")
print(f"Tool-call probes: {tool_pass}/{len(tool_probes)} passed (threshold: ≥3/4)")
print(f"Direct-answer probes: {direct_pass}/{len(direct_probes)} passed")
overall_pass = (tool_pass >= 3) and (direct_pass == len(direct_probes))
print("\n" + "=" * 60)
if overall_pass:
print("VERDICT: PASS — Bug 2 status upgraded to 'L4 verified'")
print("Action: update plan Progress table U6 → done")
return 0
else:
print("VERDICT: FAIL — L0 rule rearrangement insufficient")
print("Action: trigger L1 (web_search description expansion) as independent plan")
print(" also consider L2 (PLAN_EXEC phase policy) if L1 alone insufficient")
return 1
if __name__ == "__main__":
sys.exit(asyncio.run(main()))

View File

@ -788,7 +788,7 @@ class TestLoginCommand:
with open(config_path) as f: with open(config_path) as f:
cfg = yaml.safe_load(f) cfg = yaml.safe_load(f)
assert cfg["token"] == "jwt-token-123" assert cfg["token"] == "jwt-token-123"
assert cfg["server_url"] == "http://localhost:8001" assert cfg["server_url"] == "http://localhost:18001"
def test_login_with_server_url(self): def test_login_with_server_url(self):
"""admin login --server-url saves the custom URL.""" """admin login --server-url saves the custom URL."""

View File

@ -989,3 +989,94 @@ class TestMalformedToolUseNotLeakedAsFinalAnswer:
# 不应把原始 XML 作为最终答案 # 不应把原始 XML 作为最终答案
assert "<tool_use>" not in result.output assert "<tool_use>" not in result.output
assert result.output == "Search completed" assert result.output == "Search completed"
class TestReActToolUsePromptRules:
"""_build_tool_use_prompt 规则文本断言U4 / Bug 2 L0"""
def test_new_rule_1_present_at_top(self):
"""新规则 1 '涉及外部信息...' 出现在规则列表头部"""
from agentkit.core.react import ReActEngine
gateway = make_mock_gateway([])
engine = ReActEngine(llm_gateway=gateway)
prompt = engine._build_tool_use_prompt([])
assert "1. 涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具" in prompt
def test_old_rule_3_absent(self):
"""旧规则 3 '如果不需要工具就能回答,直接回答即可' 不再出现"""
from agentkit.core.react import ReActEngine
gateway = make_mock_gateway([])
engine = ReActEngine(llm_gateway=gateway)
prompt = engine._build_tool_use_prompt([])
assert "如果不需要工具就能回答,直接回答即可" not in prompt
def test_rules_in_correct_order(self):
"""规则序号 1-5 按预期顺序排列"""
from agentkit.core.react import ReActEngine
gateway = make_mock_gateway([])
engine = ReActEngine(llm_gateway=gateway)
prompt = engine._build_tool_use_prompt([])
# 规则 1 在规则 2 之前,规则 2 在规则 3 之前,以此类推
r1 = prompt.index("1. 涉及外部信息")
r2 = prompt.index("2. 每次只调用一个工具")
r3 = prompt.index("3. 等待工具返回结果")
r4 = prompt.index("4. 仅在确实无需工具时")
r5 = prompt.index("5. 不要在回答中重复工具的输出")
assert r1 < r2 < r3 < r4 < r5
def test_tool_use_xml_format_preserved(self):
"""<tool_use> XML 格式示例保持向后兼容"""
from agentkit.core.react import ReActEngine
gateway = make_mock_gateway([])
engine = ReActEngine(llm_gateway=gateway)
prompt = engine._build_tool_use_prompt([])
assert "<tool_use>" in prompt
assert "</tool_use>" in prompt
class TestBug2L0PromptRules:
"""Bug 2 L0 端到端验证_build_tool_use_prompt 包含工具描述 + 新规则
Bug 2 状态hypothesis applied, pending L4 verification fixed
L0 仅做文本断言真实 LLM smoke test L1/L2 独立 plan 中执行
"""
def test_web_search_description_in_prompt(self):
"""注册 web_search 工具后prompt 包含其描述文本"""
from agentkit.core.react import ReActEngine
web_search = FakeTool(
name="web_search",
description="搜索互联网信息,获取实时数据、新闻、趋势等",
)
gateway = make_mock_gateway([])
engine = ReActEngine(llm_gateway=gateway)
prompt = engine._build_tool_use_prompt([web_search])
# web_search 不是 core tool作为 extended tool 渲染
# extended tool 渲染格式: "- name: first_line_of_description"
assert "web_search" in prompt
assert "搜索互联网信息" in prompt
def test_new_rule_1_present_with_tools(self):
"""有工具注册时prompt 仍包含新规则 1"""
from agentkit.core.react import ReActEngine
web_search = FakeTool(
name="web_search",
description="搜索互联网信息",
)
gateway = make_mock_gateway([])
engine = ReActEngine(llm_gateway=gateway)
prompt = engine._build_tool_use_prompt([web_search])
assert "1. 涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具" in prompt
assert "如果不需要工具就能回答,直接回答即可" not in prompt