feat(ui): 极简摘要行补全工具文案、工作流图标统一新设计、只读模式放行调研工具

- MinimalBlocks: 摘要行分类覆盖全部原生工具,新增 MCP(mcp__前缀)/工作流三分/待办两分等文案;counts 改由 CATEGORY_ORDER 生成,根治新增分类漏初始化导致摘要空白的 bug
- workflow.svg 换新设计(圆+扁菱形+圆弧弯+圆角方块),侧边栏/快捷菜单窗口内联图标同步,侧边栏图标尺寸修正为 18px
- TOOL_ICON_MAP 增加 7 个工作流工具映射,补 submit_plan/terminate_sub_agent
- tools_execution: 只读模式放行 terminal_session/snapshot、子智能体全家、list_mcp_servers、list/get_workflow_status 及全部 mcp__ 前缀工具
- EditSummaryCard: 桌面端 diff 弹窗最大高度改为 min(可用空间, 70vh, 640px)
- 工作流编辑器:移除阶段块「单出线/终点」文字与顶栏激活按钮;提醒/报错改为按钮下方固定高度浮层(内部滚动+点外部关闭),不再挤占页面布局
This commit is contained in:
JOJO 2026-08-22 01:20:14 +08:00
parent 8c1dd407df
commit 9602875391
10 changed files with 197 additions and 80 deletions

View File

@ -206,6 +206,20 @@ class MainTerminalToolsExecutionMixin:
"todo_get",
"sleep",
"ask_user",
# 以下为只读/调研性质或会话状态管理类工具,只读模式(含计划模式)放行:
# 终端会话管理与快照terminal_input 仍禁止,防止经持久终端执行写命令)
"terminal_session",
"terminal_snapshot",
# 子智能体全生命周期(只读权限会传播给子智能体,计划模式调研主力)
"create_sub_agent",
"close_sub_agent",
"terminate_sub_agent",
"get_sub_agent_status",
# MCP 服务列举mcp__* 工具在 evaluate_tool_permission 中按前缀放行)
"list_mcp_servers",
# 工作流只读查询(激活/推进/停用/保存等状态写操作仍禁止)
"list_workflows",
"get_workflow_status",
}
_APPROVAL_REQUIRED_TOOLS = {
"run_command",
@ -383,6 +397,10 @@ class MainTerminalToolsExecutionMixin:
# 计划模式下允许提交计划求批准
if tool_name == "submit_plan" and self._is_plan_work_mode():
return {"allowed": True, "mode": mode}
# MCP 工具mcp__<服务>__<工具>)在只读模式下放行:
# 工具名动态生成无法静态区分读写,经用户确认全部允许(副作用风险由提示词约束)
if tool_name.startswith("mcp__"):
return {"allowed": True, "mode": mode}
if tool_name not in self._READONLY_ALLOWED_TOOLS:
return {
"allowed": False,

View File

@ -5,12 +5,20 @@
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="8" x="3" y="3" rx="2" />
<path d="M7 11v4a2 2 0 0 0 2 2h4" />
<rect width="8" height="8" x="13" y="13" rx="2" />
<path d="M17 9V7a2 2 0 0 0-2-2h-4" />
<!-- 顶部圆节点 -->
<circle cx="12" cy="4.6" r="2.6" />
<!-- 圆 → 菱形 竖直连线 -->
<path d="M12 7.2 V10.1" />
<!-- 中部菱形扁平半宽3.5 / 半高2.2 -->
<path d="M8.5 12.3 L12 10.1 L15.5 12.3 L12 14.5 Z" stroke-linejoin="miter" />
<!-- 左右连接线(圆弧弯) -->
<path d="M8.5 12.3 H6.1 Q4.5 12.3 4.5 13.9 V16.3" />
<path d="M15.5 12.3 H17.9 Q19.5 12.3 19.5 13.9 V16.3" />
<!-- 底部圆角方块 -->
<rect x="2.4" y="16.3" width="4.2" height="4.2" rx="1.1" />
<rect x="17.4" y="16.3" width="4.2" height="4.2" rx="1.1" />
</svg>

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 794 B

View File

@ -66,6 +66,10 @@ const activeFile = computed<SummaryFile | null>(() => {
const PANEL_MAX_WIDTH = 720;
const VIEWPORT_MARGIN = 16;
const MIN_PANEL_HEIGHT = 140;
// 2026-08-22 CSS min(70vh, 640px)
// min
const PANEL_MAX_HEIGHT_VH_RATIO = 0.7;
const PANEL_MAX_HEIGHT_PX = 640;
//
const PREFER_ABOVE_MIN_HEIGHT = 260;
@ -89,7 +93,8 @@ function computePanelStyle() {
// = -
// = +
//
// = min(, 70vh, 640px) MIN_PANEL_HEIGHT
const maxCapHeight = Math.min(vh * PANEL_MAX_HEIGHT_VH_RATIO, PANEL_MAX_HEIGHT_PX);
const rowRect = anchorEl.value.getBoundingClientRect();
const anchorAbove = rowRect.top - rowRect.height / 2;
const anchorBelow = rowRect.bottom + rowRect.height / 2;
@ -111,7 +116,7 @@ function computePanelStyle() {
...base,
top: 'auto',
bottom: `${Math.round(vh - bottomEdge)}px`,
maxHeight: `${Math.round(bottomEdge - VIEWPORT_MARGIN)}px`
maxHeight: `${Math.round(Math.min(bottomEdge - VIEWPORT_MARGIN, maxCapHeight))}px`
};
} else {
const topEdge = Math.min(
@ -122,7 +127,7 @@ function computePanelStyle() {
...base,
top: `${Math.round(topEdge)}px`,
bottom: 'auto',
maxHeight: `${Math.round(vh - VIEWPORT_MARGIN - topEdge)}px`
maxHeight: `${Math.round(Math.min(vh - VIEWPORT_MARGIN - topEdge, maxCapHeight))}px`
};
}
}

View File

@ -598,10 +598,23 @@ type ToolCategory =
| 'edit'
| 'search'
| 'webpage'
| 'mcp'
| 'workflow_activate'
| 'workflow_advance'
| 'workflow'
| 'todo_create'
| 'todo_update'
| 'memory_update'
| 'memory_read'
| 'conversation'
| 'sub_agent'
| 'sub_agent_manage'
| 'wait'
| 'ask'
| 'plan'
| 'skill'
| 'settings'
| 'easter_egg'
| 'other';
const TOOL_CATEGORY_MAP: Record<string, ToolCategory> = {
@ -633,6 +646,22 @@ const TOOL_CATEGORY_MAP: Record<string, ToolCategory> = {
extract_webpage: 'webpage',
save_webpage: 'webpage',
// MCPlist_mcp_servers mcp__ getToolCategory
list_mcp_servers: 'mcp',
// / /
activate_workflow: 'workflow_activate',
report_workflow_stage: 'workflow_advance',
choose_workflow_branch: 'workflow_advance',
get_workflow_status: 'workflow',
deactivate_workflow: 'workflow',
list_workflows: 'workflow',
save_workflow: 'workflow',
// /
todo_create: 'todo_create',
todo_update_task: 'todo_update',
//
update_memory: 'memory_update',
update_project_memory: 'memory_update',
@ -645,8 +674,19 @@ const TOOL_CATEGORY_MAP: Record<string, ToolCategory> = {
conversation_search: 'conversation',
conversation_review: 'conversation',
//
create_sub_agent: 'sub_agent'
// /
create_sub_agent: 'sub_agent',
close_sub_agent: 'sub_agent_manage',
terminate_sub_agent: 'sub_agent_manage',
get_sub_agent_status: 'sub_agent_manage',
//
sleep: 'wait',
ask_user: 'ask',
submit_plan: 'plan',
create_skill: 'skill',
manage_personalization: 'settings',
trigger_easter_egg: 'easter_egg'
};
const CATEGORY_LABELS: Record<ToolCategory, (count: number) => string> = {
@ -655,10 +695,23 @@ const CATEGORY_LABELS: Record<ToolCategory, (count: number) => string> = {
edit: (n) => `编辑了 ${n} 次文件`,
search: (n) => `搜索了 ${n}`,
webpage: (n) => `查看了 ${n} 次网页`,
mcp: (n) => `执行了 ${n} 次 MCP 工具`,
workflow_activate: (n) => `激活了 ${n} 个工作流`,
workflow_advance: (n) => `推进了 ${n} 次工作流进度`,
workflow: (n) => `执行了 ${n} 次工作流操作`,
todo_create: (n) => `创建了 ${n} 次待办`,
todo_update: (n) => `更新了 ${n} 次待办`,
memory_update: (n) => `更新了 ${n} 次记忆`,
memory_read: (n) => `查看了 ${n} 次记忆`,
conversation: (n) => `回顾了 ${n} 次对话`,
sub_agent: (n) => `创建了 ${n} 个子智能体`,
sub_agent_manage: (n) => `管理了 ${n} 次子智能体`,
wait: (n) => `等待了 ${n}`,
ask: (n) => `询问了 ${n}`,
plan: (n) => `提交了 ${n} 次计划`,
skill: (n) => `归档了 ${n} 次技能`,
settings: (n) => `更新了 ${n} 次个性化设置`,
easter_egg: (n) => `触发了 ${n} 次彩蛋`,
other: (n) => `执行了 ${n} 次其他操作`
};
@ -668,16 +721,31 @@ const CATEGORY_ORDER: ToolCategory[] = [
'edit',
'search',
'webpage',
'mcp',
'workflow_activate',
'workflow_advance',
'workflow',
'todo_create',
'todo_update',
'memory_update',
'memory_read',
'conversation',
'sub_agent',
'sub_agent_manage',
'wait',
'ask',
'plan',
'skill',
'settings',
'easter_egg',
'other'
];
const getToolCategory = (action: Action): ToolCategory => {
const name = action.tool?.name;
if (typeof name !== 'string') return 'other';
// MCP mcp__<>__<>
if (name.startsWith('mcp__')) return 'mcp';
return TOOL_CATEGORY_MAP[name] || 'other';
};
@ -748,18 +816,11 @@ const getCompletedSummaryText = (actions: Action[]): string => {
return getToolSummaryText(toolActions[0]);
}
const counts: Record<ToolCategory, number> = {
read: 0,
command: 0,
edit: 0,
search: 0,
webpage: 0,
memory_update: 0,
memory_read: 0,
conversation: 0,
sub_agent: 0,
other: 0
};
// counts CATEGORY_ORDER
// undefined NaN count > 0
const counts = Object.fromEntries(
CATEGORY_ORDER.map((category) => [category, 0])
) as Record<ToolCategory, number>;
toolActions.forEach((action) => {
counts[getToolCategory(action)]++;

View File

@ -5,22 +5,23 @@
:class="{ 'qd-window-enter': windowEntering, 'qd-window-exit': windowExiting }"
>
<header class="qd-window__header">
<svg class="qd-window__icon" viewBox="0 0 16 16" fill="none">
<circle cx="3" cy="3.5" r="1.8" stroke="currentColor" stroke-width="1.3" />
<circle cx="13" cy="8" r="1.8" stroke="currentColor" stroke-width="1.3" />
<circle cx="3" cy="12.5" r="1.8" stroke="currentColor" stroke-width="1.3" />
<path
d="M4.8 3.5h4.4a2 2 0 0 1 2 2v.7"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
/>
<path
d="M4.8 12.5h4.4a2 2 0 0 0 2-2v-.7"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
/>
<!-- 图形与 /static/icons/workflow.svg 保持一致 -->
<svg
class="qd-window__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="4.6" r="2.6" />
<path d="M12 7.2 V10.1" />
<path d="M8.5 12.3 L12 10.1 L15.5 12.3 L12 14.5 Z" stroke-linejoin="miter" />
<path d="M8.5 12.3 H6.1 Q4.5 12.3 4.5 13.9 V16.3" />
<path d="M15.5 12.3 H17.9 Q19.5 12.3 19.5 13.9 V16.3" />
<rect x="2.4" y="16.3" width="4.2" height="4.2" rx="1.1" />
<rect x="17.4" y="16.3" width="4.2" height="4.2" rx="1.1" />
</svg>
<span class="qd-window__title">{{ snapshot.name }}</span>
</header>

View File

@ -102,7 +102,8 @@
@click="$emit('open-workflows')"
>
<span class="sidebar-nav-icon workflow-icon" aria-hidden="true">
<!-- 与其他导航按钮统一内联 workflow 图标避免 mask 图标异步加载闪烁 -->
<!-- 与其他导航按钮统一内联 workflow 图标避免 mask 图标异步加载闪烁
图形与 /static/icons/workflow.svg 保持一致 -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -110,14 +111,17 @@
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="8" x="3" y="3" rx="2" />
<path d="M7 11v4a2 2 0 0 0 2 2h4" />
<rect width="8" height="8" x="13" y="13" rx="2" />
<path d="M17 9V7a2 2 0 0 0-2-2h-4" />
<circle cx="12" cy="4.6" r="2.6" />
<path d="M12 7.2 V10.1" />
<path d="M8.5 12.3 L12 10.1 L15.5 12.3 L12 14.5 Z" stroke-linejoin="miter" />
<path d="M8.5 12.3 H6.1 Q4.5 12.3 4.5 13.9 V16.3" />
<path d="M15.5 12.3 H17.9 Q19.5 12.3 19.5 13.9 V16.3" />
<rect x="2.4" y="16.3" width="4.2" height="4.2" rx="1.1" />
<rect x="17.4" y="16.3" width="4.2" height="4.2" rx="1.1" />
</svg>
</span>
<span class="sidebar-nav-label">工作流</span>

View File

@ -11,15 +11,25 @@
<span v-if="dirty" class="wf-editor__dirty">未保存</span>
</div>
<div class="wf-editor__topbar-right">
<button
type="button"
class="wf-btn wf-btn--ghost"
:class="{ 'wf-btn--issue': errorCount > 0 }"
@click="issuesOpen = !issuesOpen"
>
<span class="icon" :style="iconSrc(errorCount > 0 ? ICONS.circleAlert : ICONS.check)" aria-hidden="true"></span>
<span>{{ issueLabel }}</span>
</button>
<div class="wf-editor__issues-anchor" ref="issuesAnchorRef">
<button
type="button"
class="wf-btn wf-btn--ghost"
:class="{ 'wf-btn--issue': errorCount > 0 }"
title="查看结构提醒与错误"
@click.stop="issuesOpen = !issuesOpen"
>
<span class="icon" :style="iconSrc(errorCount > 0 ? ICONS.circleAlert : ICONS.check)" aria-hidden="true"></span>
<span>{{ issueLabel }}</span>
</button>
<!-- 提醒/报错弹层不占页面布局固定高度 + 内部滚动点外部关闭 -->
<div v-if="issuesOpen && issues.length" class="wf-editor__issues-pop">
<div v-for="(issue, i) in issues" :key="i" class="wf-issue" :class="`wf-issue--${issue.level}`">
<span class="icon" :style="iconSrc(issue.level === 'error' ? ICONS.circleAlert : ICONS.triangleAlert)" aria-hidden="true"></span>
<span>{{ issue.message }}</span>
</div>
</div>
</div>
<button type="button" class="wf-btn wf-btn--ghost" title="自动排版" @click="onAutoLayout">
<span class="icon" :style="iconSrc(ICONS.layoutGrid)" aria-hidden="true"></span>
<span>自动排版</span>
@ -62,19 +72,9 @@
<span class="icon" :style="iconSrc(ICONS.save)" aria-hidden="true"></span>
<span>{{ saving ? '保存中…' : '保存' }}</span>
</button>
<button type="button" class="wf-btn wf-btn--ghost" title="在对话中激活此工作流" @click="onActivate">
<span class="icon" :style="iconSrc(ICONS.play)" aria-hidden="true"></span>
<span>激活</span>
</button>
</div>
</header>
<div v-if="issuesOpen && issues.length" class="wf-editor__issues">
<div v-for="(issue, i) in issues" :key="i" class="wf-issue" :class="`wf-issue--${issue.level}`">
<span class="icon" :style="iconSrc(issue.level === 'error' ? ICONS.circleAlert : ICONS.triangleAlert)" aria-hidden="true"></span>
<span>{{ issue.message }}</span>
</div>
</div>
<div v-if="flashMessage" class="wf-editor__flash">{{ flashMessage }}</div>
<div class="wf-editor__body">
@ -487,6 +487,7 @@ const dirty = ref(false);
const saving = ref(false);
const addNodeMenuOpen = ref(false);
const addNodeSelectRef = ref<HTMLElement | null>(null);
const issuesAnchorRef = ref<HTMLElement | null>(null);
const confirmingStageDelete = ref(false);
let flashTimer: ReturnType<typeof setTimeout> | null = null;
@ -750,14 +751,13 @@ async function onSave() {
}
}
function onActivate() {
flash('正式版将在对话 / 菜单中激活此工作流');
}
function onDocumentClick(event: MouseEvent) {
if (addNodeMenuOpen.value && addNodeSelectRef.value && !addNodeSelectRef.value.contains(event.target as HTMLElement)) {
addNodeMenuOpen.value = false;
}
if (issuesOpen.value && issuesAnchorRef.value && !issuesAnchorRef.value.contains(event.target as HTMLElement)) {
issuesOpen.value = false;
}
}
function iconSrc(url: string) {
@ -837,13 +837,27 @@ onBeforeUnmount(() => {
flex-shrink: 0;
}
&__issues {
max-height: 160px;
/* 提醒/报错弹层:锚固在按钮下方,不占页面布局,固定高度 + 内部滚动 */
&__issues-anchor {
position: relative;
}
&__issues-pop {
position: absolute;
top: calc(100% + 6px);
right: 0;
width: 320px;
max-width: calc(100vw - 32px);
height: 240px;
overflow-y: auto;
padding: 8px 12px;
border-bottom: 1px solid var(--border-default);
background: var(--surface-soft);
flex-shrink: 0;
border: 1px solid var(--border-default);
border-radius: 10px;
background: var(--surface-raised);
box-shadow: var(--shadow-mid);
z-index: 30;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text-secondary) 55%, transparent) transparent;
}
&__flash {

View File

@ -34,7 +34,6 @@
</div>
<div class="stage-node__foot">
<span class="stage-node__id">{{ data.node.id }}</span>
<span class="stage-node__routes">{{ data.isTerminal ? '终点' : '单出线' }}</span>
</div>
</div>
</template>
@ -166,9 +165,5 @@ function iconSrc(url: string) {
overflow: hidden;
text-overflow: ellipsis;
}
&__routes {
flex-shrink: 0;
}
}
</style>

View File

@ -180,8 +180,8 @@
}
.workflow-icon svg {
width: 20px;
height: 20px;
width: 18px;
height: 18px;
}
.monitor-mode-btn.blocked {

View File

@ -99,7 +99,18 @@ export const TOOL_ICON_MAP = Object.freeze({
trigger_easter_egg: 'sparkles',
list_mcp_servers: 'mcpLogo',
view_image: 'camera',
view_video: 'eye'
view_video: 'eye',
// 工作流工具统一使用 workflow 专用图标
activate_workflow: 'workflow',
report_workflow_stage: 'workflow',
choose_workflow_branch: 'workflow',
get_workflow_status: 'workflow',
deactivate_workflow: 'workflow',
list_workflows: 'workflow',
save_workflow: 'workflow',
// 补齐原先回落到默认齿轮的原生工具
submit_plan: 'clipboard',
terminate_sub_agent: 'bot'
});
export const TOOL_CATEGORY_ICON_MAP = Object.freeze({