Sfoglia il codice sorgente

update process pipeline: frontend update

guantao 3 giorni fa
parent
commit
b7cdf98ebb

+ 37 - 6
agent/llm/claude.py

@@ -32,14 +32,45 @@ async def anthropic_native_llm_call(
 ) -> Dict[str, Any]:
     """
     原生 Anthropic API 调用函数
+    支持 CLAUDE_CODE_KEY/CLAUDE_CODE_URL 或 ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL
     """
-    api_key = os.getenv("ANTHROPIC_API_KEY")
-    if not api_key:
-        raise ValueError("ANTHROPIC_API_KEY environment variable not set")
-        
-    base_url = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
+    # 优先使用 CLAUDE_CODE_KEY,如果不存在则使用 ANTHROPIC_API_KEY
+    claude_code_key = os.getenv("CLAUDE_CODE_KEY")
+    anthropic_key = os.getenv("ANTHROPIC_API_KEY")
+
+    if claude_code_key:
+        api_key = claude_code_key
+        key_source = "CLAUDE_CODE_KEY"
+    elif anthropic_key:
+        api_key = anthropic_key
+        key_source = "ANTHROPIC_API_KEY"
+    else:
+        raise ValueError("CLAUDE_CODE_KEY or ANTHROPIC_API_KEY environment variable not set")
+
+    # 优先使用 CLAUDE_CODE_URL,如果不存在则使用 ANTHROPIC_BASE_URL
+    claude_code_url = os.getenv("CLAUDE_CODE_URL")
+    anthropic_url = os.getenv("ANTHROPIC_BASE_URL")
+
+    if claude_code_url:
+        base_url = claude_code_url
+        url_source = "CLAUDE_CODE_URL"
+    elif anthropic_url:
+        base_url = anthropic_url
+        url_source = "ANTHROPIC_BASE_URL"
+    else:
+        base_url = "https://api.anthropic.com"
+        url_source = "default"
+
     endpoint = f"{base_url.rstrip('/')}/v1/messages"
-    
+
+    # 记录使用的配置(只在第一次调用时输出)
+    if not hasattr(anthropic_native_llm_call, '_logged_config'):
+        logger.info(f"[Anthropic Native] Using {key_source}: {api_key[:20]}...")
+        logger.info(f"[Anthropic Native] Using {url_source}: {base_url}")
+        print(f"[Anthropic Native] Using {key_source}: {api_key[:20]}...")
+        print(f"[Anthropic Native] Using {url_source}: {base_url}")
+        anthropic_native_llm_call._logged_config = True
+
     anthropic_version = os.getenv("ANTHROPIC_VERSION", "2023-06-01")
 
     # 去掉 anthropic/ opneai/ 等命名空间前缀(如果传入的话)

+ 1 - 1
examples/process_pipeline/prompts/apply_to_grounding.prompt

@@ -16,7 +16,7 @@
 
 每个 apply_to 条目必须输出 ideal_path,表示该描述"理想上应该挂在哪个路径"(即使树上不存在):
 - 从根往下逐层检查 category_path 的每一层,判断该层是否仍然准确描述了 draft 的语义。
-- 找到第一个"不够精确或有偏差"的层级,从该层级开始自由续写(替换该层及其后的所有层)。
+- 找到第一个"不够精确或有偏差"的层级,从该层级开始续写(替换该层及其后的所有层)。续写的风格应当与路径中的其他层级相符合,至少词性应当一致。
 - 如果 category_path 所有层级都准确,且 draft 没有更细的信息,则 ideal_path = category_path。
 - 如果 category_path 所有层级都准确,但 draft 还有更细的信息未体现,则在末尾续写 1-3 个层级。
 - 续写部分命名风格参考下方"邻近路径参考"(两字名词、层级粒度保持一致)。

+ 5 - 4
examples/process_pipeline/prompts/extract_workflow.prompt

@@ -25,8 +25,8 @@ $system$
 每个步骤包含:
 
 - order:步骤序号,整数。
-- phase: 该步骤所属阶段,取值为「预处理」/「生成」/「编辑」之一;预处理是产出物的目的,不面向最终成品,生成个和编辑可复用 action 的定义
-- action: 该步骤的核心动作,格式为「一级动作:二级动作」,如「编辑:局部重绘」、「生成:融合」;若二级动作与一级相同可省略,如「生成」
+- phase: phase: 该步骤所属阶段,取值为「非制作」/「预处理」/「生成」/「编辑」之一;「非制作」指创作与运营层面的行为,如故事构思、选题策划、热点参考、发布节奏等,该阶段不含 action;「预处理」是产出物的目的,不面向最终成品;生成和编辑可复用 action 的定义。
+- action: 该步骤的核心动作,格式为「一级动作:二级动作」,如「编辑:局部重绘」、「生成:融合」;若二级动作与一级相同可省略,如「生成」;非制作阶段的 action 值固定为 null。
 - body:具体做法,包含 prompt 写法、参数配置、操作细节等;从帖子原文中提取,未提及则为 null。
 - inputs:该步骤的输入,数组。
 - outputs:该步骤的产出,数组。
@@ -39,7 +39,7 @@ $system$
 ```json
 {
   "modality": "文本",
-  "description": "该项在当前步骤中实际起到的作用,用简短名词短语表达,如:场景参考、角色参考、故事情节与镜头要求等"
+  "description": "该项在当前步骤中实际起到的作用,用简短名词短语表达,如:场景参考、角色参考、故事情节与镜头要求等",不要写指令,中间产物,等没有信息量的词汇
 }
 ```
 
@@ -100,7 +100,7 @@ $user$
         "apply_to_draft": { "实质": ["该步骤操作的内容点"], "形式": ["该步骤的呈现方式"] }
       }
     ],
-    "effects": ["实现 XX 效果"],
+    "effects": ["实现 XX 效果", "实现 YY 效果"],
     "criterion": null,
     "unstructured_what": []
   }
@@ -113,3 +113,4 @@ $user$
 - strategy 顶层不要输出 inputs / outputs / tools / stage。
 - 不要任何前言、解释、标题。
 - 字符串值内禁止出现 ASCII 双引号;需要引号请用中文书名号。
+- **effects 数组中的每个字符串都必须以"实现"开头**,如 "实现快速生成"、"实现风格统一"。

+ 42 - 10
examples/process_pipeline/prompts/extract_workflow.schema.json

@@ -2,7 +2,11 @@
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "extract_workflow_output_v5",
   "type": "object",
-  "required": ["skip", "skip_reason", "strategy"],
+  "required": [
+    "skip",
+    "skip_reason",
+    "strategy"
+  ],
   "properties": {
     "skip": {
       "type": "boolean"
@@ -17,7 +21,12 @@
         },
         {
           "type": "object",
-          "required": ["steps", "effects", "criterion", "unstructured_what"],
+          "required": [
+            "steps",
+            "effects",
+            "criterion",
+            "unstructured_what"
+          ],
           "properties": {
             "steps": {
               "type": "array",
@@ -41,11 +50,19 @@
                   },
                   "phase": {
                     "type": "string",
-                    "enum": ["预处理", "生成", "编辑"]
+                    "enum": [
+                      "非制作",
+                      "预处理",
+                      "生成",
+                      "编辑"
+                    ]
                   },
                   "action": {
                     "type": "object",
-                    "required": ["main_action", "mechanism"],
+                    "required": [
+                      "main_action",
+                      "mechanism"
+                    ],
                     "properties": {
                       "main_action": {
                         "type": "string",
@@ -60,13 +77,19 @@
                     }
                   },
                   "body": {
-                    "type": ["string", "null"]
+                    "type": [
+                      "string",
+                      "null"
+                    ]
                   },
                   "inputs": {
                     "type": "array",
                     "items": {
                       "type": "object",
-                      "required": ["modality", "description"],
+                      "required": [
+                        "modality",
+                        "description"
+                      ],
                       "properties": {
                         "modality": {
                           "type": "string",
@@ -85,7 +108,10 @@
                     "type": "array",
                     "items": {
                       "type": "object",
-                      "required": ["modality", "description"],
+                      "required": [
+                        "modality",
+                        "description"
+                      ],
                       "properties": {
                         "modality": {
                           "type": "string",
@@ -106,7 +132,10 @@
                   },
                   "apply_to_draft": {
                     "type": "object",
-                    "required": ["实质", "形式"],
+                    "required": [
+                      "实质",
+                      "形式"
+                    ],
                     "properties": {
                       "实质": {
                         "type": "array",
@@ -133,7 +162,10 @@
               }
             },
             "criterion": {
-              "type": ["string", "null"]
+              "type": [
+                "string",
+                "null"
+              ]
             },
             "unstructured_what": {
               "type": "array",
@@ -146,4 +178,4 @@
       ]
     }
   }
-}
+}

+ 16 - 6
examples/process_pipeline/run_pipeline.py

@@ -534,6 +534,7 @@ async def main():
     parser.add_argument("--start-from", type=str, choices=STEP_NAMES, help="Start from this step (inclusive)")
     parser.add_argument("--end-at", type=str, choices=STEP_NAMES, help="End at this step (inclusive)")
     parser.add_argument("--case-index", type=int, help="Rerun extraction for a specific case index (only for workflow-extract, capability-extract, apply-grounding)")
+    parser.add_argument("--use-claude-sdk", action="store_true", help="Use Claude SDK (CLAUDE_CODE_KEY/URL) instead of OpenRouter")
     args = parser.parse_args()
 
     # ── 参数验证 ──
@@ -755,12 +756,21 @@ async def main():
 
     # Instantiate two distinct LLM orchestrators
     qwen_model = "qwen3.5-plus"  # maps to qwen3.5-plus via Qwen interface
-    
-    # 使用 OpenRouter 代理的 GPT-5.4(支持结构化输出 strict mode)
-    claude_model = "openai/gpt-5.4"
-    args.use_claude_sdk = False  # 禁用纯 Native SDK 模式,走内部通用 AgentRunner (即可对接 OpenRouter)
-    from agent.llm.openrouter import create_openrouter_llm_call
-    claude_llm_call = create_openrouter_llm_call(model=claude_model)
+
+    # 根据 --use-claude-sdk 参数选择 LLM 提供商
+    if args.use_claude_sdk:
+        # 使用 Claude SDK (CLAUDE_CODE_KEY/URL 或 ANTHROPIC_API_KEY/BASE_URL)
+        claude_model = "claude-sonnet-4-5"
+        print(f"✅ Using Claude SDK with model: {claude_model}")
+        print(f"   API Key: {os.getenv('CLAUDE_CODE_KEY', 'N/A')[:20]}...")
+        print(f"   Base URL: {os.getenv('CLAUDE_CODE_URL', os.getenv('ANTHROPIC_BASE_URL', 'https://api.anthropic.com'))}")
+        claude_llm_call = create_claude_llm_call(model=claude_model)
+    else:
+        # 使用 OpenRouter 代理的 GPT-5.4(支持结构化输出 strict mode)
+        claude_model = "openai/gpt-5.4"
+        print(f"✅ Using OpenRouter with model: {claude_model}")
+        from agent.llm.openrouter import create_openrouter_llm_call
+        claude_llm_call = create_openrouter_llm_call(model=claude_model)
     
     runner_qwen = AgentRunner(
         trace_store=store,

+ 13 - 2
examples/process_pipeline/script/generate_case.py

@@ -186,8 +186,18 @@ async def normalize_source_item(
     author = _extract_author(post, platform)
     body = _extract_body(post, platform)
     url = _extract_url(post, platform) or source_item.get("source_url", "")
-    likes = post.get("like_count", 0)
-    comments = post.get("comment_count", 0)
+
+    # 收集反馈数据(兼容不同平台,没有的字段填 None)
+    feedback = {
+        "like_count": post.get("like_count") if post.get("like_count") is not None else None,
+        "collect_count": post.get("collect_count") if post.get("collect_count") is not None else None,
+        "comment_count": post.get("comment_count") if post.get("comment_count") is not None else None,
+        "share_count": post.get("share_count") if post.get("share_count") is not None else None,
+    }
+
+    # 用于 note 字段的简化显示
+    likes = feedback["like_count"] or 0
+    comments = feedback["comment_count"] or 0
 
     # 解析发布时间
     publish_timestamp = post.get("publish_timestamp", "")
@@ -251,6 +261,7 @@ async def normalize_source_item(
         "url": url,
         "note": f"platform={platform} | likes={likes} | comments={comments}",
         "published_at": published_at,  # bigint, nullable
+        "feedback": feedback,
         "_raw": {
             "case_id": case_id,
             "platform": platform,

+ 181 - 14
examples/process_pipeline/ui/app.js

@@ -971,7 +971,7 @@ function renderAggregatedPerCaseData(cases, type) {
         </div>`;
         
         // Always Expanded Structured Data
-        contentHtml += window.renderStructuredData(items, type);
+        contentHtml += window.renderStructuredData(items, type, c);
         
         // Add JSON toggle at the bottom of the case section
         const caseJsonStr = JSON.stringify(c, null, 2).replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -1308,6 +1308,39 @@ function setupEventListeners() {
         memo.classList.toggle('hidden');
     });
 
+    // Refresh Data without changing page position
+    const btnRefresh = document.getElementById('btn-refresh-data');
+    if (btnRefresh) {
+        btnRefresh.addEventListener('click', async () => {
+            if (currentSelectedIndex === null) {
+                alert("请先选择一个需求项目!");
+                return;
+            }
+            
+            const oldText = btnRefresh.innerHTML;
+            btnRefresh.innerHTML = '🔄 刷新中...';
+            btnRefresh.disabled = true;
+            
+            const modalCaseDetail = document.getElementById('case-detail-modal');
+            const isModalOpen = modalCaseDetail && !modalCaseDetail.classList.contains('hidden');
+            const activeSidebarItem = document.querySelector('.modal-sidebar-item.active');
+            const activeCaseIdx = activeSidebarItem ? parseInt(activeSidebarItem.id.replace('sidebar-item-', '')) : null;
+            
+            try {
+                await fetchRequirementData(currentSelectedIndex);
+                
+                if (isModalOpen && activeCaseIdx !== null && typeof window.renderSingleCaseDetail === 'function') {
+                    window.renderSingleCaseDetail(activeCaseIdx);
+                }
+            } catch (e) {
+                console.error("Failed to refresh data", e);
+            } finally {
+                btnRefresh.innerHTML = oldText;
+                btnRefresh.disabled = false;
+            }
+        });
+    }
+
     // Tabs
     document.querySelectorAll('.tab-btn-pill').forEach(btn => {
         btn.addEventListener('click', () => {
@@ -1960,7 +1993,7 @@ window.renderSingleCaseDetail = function(idx) {
                 ${btnWorkflowHtml}
             </div>
             <div class="hidden" style="padding-top: 1.2rem;">
-                ${window.renderStructuredData(wf && wf.workflow ? [wf.workflow] : null, 'workflow')}
+                ${window.renderStructuredData(wf && wf.workflow ? [wf.workflow] : null, 'workflow', wf)}
             </div>
         </div>
         
@@ -1973,7 +2006,7 @@ window.renderSingleCaseDetail = function(idx) {
                 ${btnCapabilityHtml}
             </div>
             <div class="hidden" style="padding-top: 1.2rem;">
-                ${window.renderStructuredData(wf && wf.capabilities ? wf.capabilities : null, 'capabilities')}
+                ${window.renderStructuredData(wf && wf.capabilities ? wf.capabilities : null, 'capabilities', wf)}
             </div>
         </div>
     `;
@@ -1989,7 +2022,7 @@ window.switchDetailTab = function(tabId) {
     document.getElementById(`tab-content-${tabId}`).style.display = 'block';
 };
 
-window.renderStructuredData = function(items, type) {
+window.renderStructuredData = function(items, type, parentItem = null) {
     if (!items || items.length === 0) {
         return `<div style="color:var(--text-muted); padding: 1rem;">暂无${type === 'workflow' ? '工序' : '能力'}数据</div>`;
     }
@@ -2090,7 +2123,7 @@ window.renderStructuredData = function(items, type) {
             </div>
         `;
         
-        const renderApplyToVal = (valObj) => {
+        const renderApplyToVal = (valObj, isIdeal = false) => {
             if (!valObj || typeof valObj !== 'object') return '-';
             let res = '<div style="display:flex; flex-direction:column; gap:6px;">';
             Object.entries(valObj).forEach(([k, v]) => {
@@ -2098,6 +2131,9 @@ window.renderStructuredData = function(items, type) {
                     res += `<div class="apply-to-subrow">
                         <span class="apply-to-key-badge" style="background: rgba(0,0,0,0.05); color: #475569;">${k}</span>
                         <div class="apply-to-values" style="display:flex; flex-wrap:wrap; gap:4px;">`;
+                    
+                    let idealBadges = [];
+                    
                     v.forEach(pathObj => {
                         let pathStr = '';
                         let elementStr = '';
@@ -2134,7 +2170,54 @@ window.renderStructuredData = function(items, type) {
                         if (htmlParts) {
                             res += `<span class="apply-to-path-item has-tooltip">${htmlParts}${tooltipHtml}</span>`;
                         }
+                        
+                        if (typeof pathObj === 'object' && pathObj !== null && pathObj.ideal_path) {
+                            let fullNormalPath = pathStr || '';
+                            if (elementStr) {
+                                if (!fullNormalPath.endsWith('/')) fullNormalPath += '/';
+                                fullNormalPath += elementStr;
+                            }
+                            
+                            if (fullNormalPath !== pathObj.ideal_path) {
+                                const normalParts = fullNormalPath.split('/');
+                                const idealParts = pathObj.ideal_path.split('/');
+                                const idealLeaf = idealParts.pop();
+                                
+                                let prefixHtml = '';
+                                if (idealParts.length > 0) {
+                                    prefixHtml += `<span class="apply-to-path-prefix">`;
+                                    for (let i = 0; i < idealParts.length; i++) {
+                                        if (i === 0 && idealParts[0] === '') {
+                                            prefixHtml += '/';
+                                            continue;
+                                        }
+                                        const p = idealParts[i];
+                                        const isNew = i >= normalParts.length || p !== normalParts[i];
+                                        if (isNew) {
+                                            prefixHtml += `<span style="color:#3b82f6; font-weight:600;">${p}</span>/`;
+                                        } else {
+                                            prefixHtml += `${p}/`;
+                                        }
+                                    }
+                                    prefixHtml += `</span>`;
+                                }
+                                
+                                const isLeafNew = idealParts.length >= normalParts.length || idealLeaf !== normalParts[idealParts.length];
+                                
+                                const leafHtml = isLeafNew 
+                                    ? `<span class="apply-to-path-leaf" style="background:#eff6ff; color:#2563eb; border:1px solid #bfdbfe;">${idealLeaf}</span>`
+                                    : `<span class="apply-to-path-leaf" style="background:#f1f5f9; color:#475569; border:1px solid #cbd5e1;">${idealLeaf}</span>`;
+
+                                const idealHtml = `${prefixHtml}${leafHtml}`;
+                                idealBadges.push(`<span class="apply-to-path-item has-tooltip" style="border: 2px dashed #94a3b8; background: transparent; margin-top: 4px;">${idealHtml}${tooltipHtml}</span>`);
+                            }
+                        }
                     });
+                    
+                    if (idealBadges.length > 0) {
+                        res += `<div style="flex-basis: 100%; height: 0; margin: 0;"></div>` + idealBadges.join('');
+                    }
+                    
                     res += `</div></div>`;
                 }
             });
@@ -2162,6 +2245,79 @@ window.renderStructuredData = function(items, type) {
                 </div>
             </div>`;
         }
+
+        // Render confidence fields
+        const formatDate = (ts) => {
+            if (!ts) return '-';
+            if (typeof ts === 'string' && ts.includes('-')) return ts;
+            const num = Number(ts);
+            if (isNaN(num) || num <= 0) return '-';
+            const d = new Date(num > 10000000000 ? num : num * 1000);
+            return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
+        };
+
+        html += `<div class="structured-row">
+            <div class="structured-label" style="display:flex; align-items:center; gap:4px;">
+                置信度
+            </div>
+            <div class="structured-value" style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 0.85em;">
+                <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                    <span style="color: var(--text-muted);">Maturity:</span>
+                    <span style="font-weight: 500; color: var(--text-main);">${String((item.maturity || (parentItem && parentItem.maturity)) || '-').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</span>
+                </div>
+                <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                    <span style="color: var(--text-muted);">Validation:</span>
+                    <span style="font-weight: 500; color: var(--text-main);">${(item.validation_count !== undefined && item.validation_count !== null) ? String(item.validation_count).replace(/</g, '&lt;').replace(/>/g, '&gt;') : (parentItem && parentItem.validation_count !== undefined && parentItem.validation_count !== null) ? String(parentItem.validation_count).replace(/</g, '&lt;').replace(/>/g, '&gt;') : '-'}</span>
+                </div>
+                <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                    <span style="color: var(--text-muted);">Published:</span>
+                    <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.published_at || (parentItem && parentItem.published_at))}</span>
+                </div>
+                <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                    <span style="color: var(--text-muted);">Last Verified:</span>
+                    <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.last_verified_at || (parentItem && parentItem.last_verified_at))}</span>
+                </div>
+                <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                    <span style="color: var(--text-muted);">Created:</span>
+                    <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.created_at || (parentItem && parentItem.created_at))}</span>
+                </div>
+                <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                    <span style="color: var(--text-muted);">Updated:</span>
+                    <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.updated_at || (parentItem && parentItem.updated_at))}</span>
+                </div>
+            </div>
+        </div>`;
+        
+        // Render feedback if available
+        const feedbackVal = item.feedback || (parentItem && parentItem.feedback);
+        if (feedbackVal) {
+            let feedbackHtml = '';
+            if (typeof feedbackVal === 'object' && feedbackVal !== null) {
+                Object.entries(feedbackVal).forEach(([k, v]) => {
+                    if (v !== null && v !== undefined && String(v).trim() !== '') {
+                        feedbackHtml += `<div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
+                            <span style="color: var(--text-muted);">${k}:</span>
+                            <span style="font-weight: 500; color: var(--text-main);">${v}</span>
+                        </div>`;
+                    }
+                });
+                if (feedbackHtml !== '') {
+                    feedbackHtml = `<div style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 0.85em;">${feedbackHtml}</div>`;
+                }
+            } else if (typeof feedbackVal === 'string' && feedbackVal.trim() !== '') {
+                feedbackHtml = `<div style="color: var(--text-main); font-size: 0.95em; line-height: 1.5; white-space: pre-wrap; padding-top: 2px;">${String(feedbackVal).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`;
+            }
+
+            if (feedbackHtml !== '') {
+                html += `<div class="structured-row">
+                    <div class="structured-label">feedback</div>
+                    <div class="structured-value" style="display: flex; align-items: center;">
+                        ${feedbackHtml}
+                    </div>
+                </div>`;
+            }
+        }
+
         
         // Render body
         if (item.body && typeof item.body === 'string') {
@@ -2179,12 +2335,12 @@ window.renderStructuredData = function(items, type) {
                 const desc = isValid(io.description) ? io.description.replace(/</g, '&lt;').replace(/>/g, '&gt;') : '';
                 const mod = isValid(io.modality) ? io.modality : '';
                 let content = '';
+                if (mod) {
+                    content += `<span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-right:6px;margin-bottom:2px;display:inline-block;">${mod}</span>`;
+                }
                 if (desc) {
                     content += desc;
                 }
-                if (mod) {
-                    content += `<span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-left:6px;margin-top:2px;">${mod}</span>`;
-                }
                 if (!content) {
                     const keys = Object.keys(io);
                     if (keys.length === 1 && typeof io[keys[0]] === 'string') {
@@ -2217,11 +2373,11 @@ window.renderStructuredData = function(items, type) {
                                 <th style="padding: 12px 10px; width: 60px;">序号</th>
                                 <th style="padding: 12px 10px; width: 70px;">阶段</th>
                                 <th style="display: none;">操作流</th>
-                                <th style="padding: 12px 10px; width: 200px;">输入 (Inputs)</th>
+                                <th style="padding: 12px 10px; width: 200px;">输入</th>
                                 <th style="padding: 12px 10px; width: 120px;">动作</th>
-                                <th style="padding: 12px 10px; width: 200px;">输出 (Outputs)</th>
-                                <th style="padding: 12px 10px; width: 200px;">操作对象 (Apply)</th>
-                                <th style="padding: 12px 10px; width: 280px;">具体做法</th>
+                                <th style="padding: 12px 10px; width: 200px;">输出</th>
+                                <th style="padding: 12px 10px; width: 200px;">作用域</th>
+                                <th style="padding: 12px 10px; width: 280px;">做法</th>
                                 <th style="padding: 12px 10px; width: 100px;">工具</th>
                             </tr>
                         </thead>
@@ -2248,6 +2404,17 @@ window.renderStructuredData = function(items, type) {
                     if (!stepTitle || stepTitle === '未知') stepTitle = escapeHtml(`步骤 ${step.order || stepIdx + 1}`);
                 }
                 
+                let actionHtml = escapeHtml(actionText);
+                if (step.action && step.action.main_action) {
+                    const badgeHtml = `<span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-right:6px;margin-bottom:2px;display:inline-block;">${escapeHtml(step.action.main_action)}</span>`;
+                    actionHtml = step.action.mechanism ? badgeHtml + escapeHtml(step.action.mechanism) : badgeHtml;
+                } else {
+                    const match = actionText.match(/^\[(.*?)\]\s*(.*)$/);
+                    if (match) {
+                        actionHtml = `<span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-right:6px;margin-bottom:2px;display:inline-block;">${escapeHtml(match[1])}</span>${escapeHtml(match[2])}`;
+                    }
+                }
+                
                 let toolsHtml = '';
                 if (step.tools && step.tools.length > 0) {
                     toolsHtml = step.tools.map(t => `<span class="structured-badge tool-badge" style="display:inline-block; margin:2px;">${escapeHtml(t)}</span>`).join('');
@@ -2270,8 +2437,8 @@ window.renderStructuredData = function(items, type) {
                             <div class="cell-content" style="max-height: 50px; overflow: hidden;">${step.inputs && Array.isArray(step.inputs) && step.inputs.length > 0 ? renderDataObjList(step.inputs) : '-'}</div>
                             <div class="cell-fade"></div>
                         </td>
-                        <td style="padding: 14px 10px; font-weight: 600; color: var(--accent-primary);">
-                            ${escapeHtml(actionText)}
+                        <td style="padding: 14px 10px; font-weight: bold; color: var(--text-main); line-height: 1.5;">
+                            ${actionHtml}
                         </td>
                         <td style="padding: 14px 10px; position: relative;">
                             <div class="cell-content" style="max-height: 50px; overflow: hidden;">${step.outputs && Array.isArray(step.outputs) && step.outputs.length > 0 ? renderDataObjList(step.outputs) : '-'}</div>

+ 1 - 0
examples/process_pipeline/ui/index.html

@@ -149,6 +149,7 @@
                 <!-- Search Input -->
                 <input type="text" class="search-input" placeholder="搜索标题 / 能力 / 工序 / 关键词" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 0.85rem; outline: none; min-width: 220px; margin-right: 15px;">
                 
+                <button class="btn btn-secondary btn-small" id="btn-refresh-data" title="局部刷新当前数据,保持页面位置">🔄 刷新数据</button>
                 <button class="btn btn-secondary btn-small" id="btn-toggle-memo" title="切换需求笔记面板">📝 笔记</button>
                 <button class="btn btn-primary btn-small" id="btn-open-run-modal">🚀 运行流水线</button>
             </div>