From 1111d208c416d873f6a20f7563601a12d8b9acfd Mon Sep 17 00:00:00 2001 From: minorQAQ <202200120184@mail.sdu.edu.cn> Date: Fri, 10 Jul 2026 20:17:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=82=AE=E4=BB=B6=E5=8F=91?= =?UTF-8?q?=E9=80=81=EF=BC=88=E6=94=AF=E6=8C=81QQ=E5=92=8C=E7=BD=91?= =?UTF-8?q?=E6=98=93.126.163=EF=BC=89PPT=E5=88=B6=E4=BD=9C=E5=85=A8?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=88SVG=E8=B7=AF=E7=BA=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 +- dongtai.py | 660 -------- jingtai.py | 306 ---- .../config_manager.cpython-312.pyc | Bin 20399 -> 20487 bytes .../core/__pycache__/llm.cpython-312.pyc | Bin 32344 -> 32008 bytes .../core/__pycache__/nodes.cpython-312.pyc | Bin 11845 -> 13188 bytes .../core/__pycache__/runtime.cpython-312.pyc | Bin 27501 -> 27674 bytes src/agent/core/agent_config.json | 8 +- src/agent/core/config_manager.py | 5 + src/agent/core/env_config.json | 10 +- src/agent/core/llm.py | 21 +- src/agent/core/nodes.py | 45 +- src/agent/core/runtime.py | 10 +- src/agent/core/tool_config.json | 48 +- .../tool_call_recorder.cpython-312.pyc | Bin 22901 -> 23787 bytes src/agent/history/data_base/chat_history.db | Bin 28672 -> 45056 bytes .../20260708_155304/ui_messages.json | 4 - .../20260708_160449/ui_messages.json | 4 - src/agent/history/tool_call_recorder.py | 17 + .../__pycache__/system_prompt.cpython-312.pyc | Bin 14987 -> 15239 bytes .../data_level0.bin | Bin 423600 -> 423600 bytes .../length.bin | Bin 400 -> 400 bytes src/agent/memory/chroma_rag_db/chroma.sqlite3 | Bin 430080 -> 434176 bytes src/agent/memory/system_prompt.py | 22 +- .../attachments/01-workflow-overview.md | 123 ++ .../ppt_maker/attachments/02-strategist.md | 177 ++ .../attachments/03-modes-and-styles.md | 141 ++ .../ppt_maker/attachments/04-design-system.md | 131 ++ .../ppt_maker/attachments/05-executor.md | 176 ++ .../ppt_maker/attachments/06-svg-standards.md | 151 ++ .../attachments/07-image-embedding.md | 131 ++ .../attachments/08-layout-patterns.md | 243 +++ .../attachments/09-quality-and-export.md | 87 + .../ppt_maker/attachments/10-svg-to-pptx.md | 56 + src/agent/skills/ppt_maker/skill.json | 8 + src/agent/skills/ppt_maker/skill.md | 41 + src/agent/tools/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 2366 -> 2422 bytes .../__pycache__/date_query.cpython-312.pyc | Bin 1560 -> 1554 bytes .../tools/__pycache__/doc.cpython-312.pyc | Bin 30983 -> 34382 bytes .../tools/__pycache__/email.cpython-312.pyc | Bin 0 -> 31939 bytes .../tools/__pycache__/rag.cpython-312.pyc | Bin 26646 -> 27666 bytes .../__pycache__/web_search.cpython-312.pyc | Bin 3507 -> 3501 bytes src/agent/tools/date_query.py | 2 +- src/agent/tools/doc.py | 87 +- src/agent/tools/email.py | 665 ++++++++ src/agent/tools/rag.py | 69 +- src/agent/tools/web_search.py | 2 +- .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 174 bytes .../__pycache__/agent_utils.cpython-312.pyc | Bin 37597 -> 39093 bytes .../__pycache__/agent_utils.cpython-313.pyc | Bin 37569 -> 41907 bytes src/agent/utils/agent_utils.py | 39 +- src/agent/utils/database_utils.py | 302 ---- src/agent/utils/ppt_utils.py | 1449 +++++++++++++++++ src/web/__pycache__/server.cpython-312.pyc | Bin 51290 -> 51411 bytes .../__pycache__/ui_session.cpython-312.pyc | Bin 32968 -> 32963 bytes src/web/app.js | 3 +- src/web/index.html | 5 +- src/web/server.py | 10 +- src/web/ui_session.py | 4 +- 60 files changed, 3911 insertions(+), 1394 deletions(-) delete mode 100644 dongtai.py delete mode 100644 jingtai.py delete mode 100644 src/agent/history/multi_inputs/20260708_155304/ui_messages.json delete mode 100644 src/agent/history/multi_inputs/20260708_160449/ui_messages.json create mode 100644 src/agent/skills/ppt_maker/attachments/01-workflow-overview.md create mode 100644 src/agent/skills/ppt_maker/attachments/02-strategist.md create mode 100644 src/agent/skills/ppt_maker/attachments/03-modes-and-styles.md create mode 100644 src/agent/skills/ppt_maker/attachments/04-design-system.md create mode 100644 src/agent/skills/ppt_maker/attachments/05-executor.md create mode 100644 src/agent/skills/ppt_maker/attachments/06-svg-standards.md create mode 100644 src/agent/skills/ppt_maker/attachments/07-image-embedding.md create mode 100644 src/agent/skills/ppt_maker/attachments/08-layout-patterns.md create mode 100644 src/agent/skills/ppt_maker/attachments/09-quality-and-export.md create mode 100644 src/agent/skills/ppt_maker/attachments/10-svg-to-pptx.md create mode 100644 src/agent/skills/ppt_maker/skill.json create mode 100644 src/agent/skills/ppt_maker/skill.md create mode 100644 src/agent/tools/__pycache__/email.cpython-312.pyc create mode 100644 src/agent/tools/email.py create mode 100644 src/agent/utils/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/agent/utils/database_utils.py create mode 100644 src/agent/utils/ppt_utils.py diff --git a/README.md b/README.md index d7c5f8d..7710538 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,6 @@ git push minor master └─ 图结束后 (runtime.py / execute_agent) └─ 检测到强制终止 → 向前端输出: "检测到异常循环,工具 `xxx` 已连续调用 5 次..." - - -1、打开https://erpdemo.fanpusoft.com/main.aspx# -2、点击采购管理 -3、点击新增供应商 -4、在新增供应商页面进行以下操作:输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”、输入联系人:“赵永顺”、输入生日:“2004/07/09” -5、点击保存、点击确定 - --- ## 提示词工程文档 @@ -418,9 +410,36 @@ You are a visual grounding model for desktop GUI control. ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -p 60007 minor123@semi-ai.tech #### 本地端口映射 -ssh -fN -L 8901:127.0.0.1:8901 -L 8902:127.0.0.1:8902 -L 8903:127.0.0.1:8903 minor123@semi-ai.tech -p 60007 - +ssh -fN -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -L 8901:127.0.0.1:8901 -L 8902:127.0.0.1:8902 -L 8903:127.0.0.1:8903 minor123@semi-ai.tech -p 60007 +ssh -fN -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -L 8903:127.0.0.1:8903 minor123@semi-ai.tech -p 60007 # 查看状态码(200表示健康) +curl.exe -o nul -s -w "%{http_code}\n" http://localhost:8901/health +curl.exe -o nul -s -w "%{http_code}\n" http://localhost:8902/health +curl.exe -o nul -s -w "%{http_code}\n" http://localhost:8903/health + curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8901/health curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8902/health -curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8903/health \ No newline at end of file +curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8903/health + +(email、sendfile) +今天和昨天有什么邮件 +发封邮件给19553226590@163.com,内容就是我桌面的图片"C:\Users\86166\Desktop\风景\8.png" + +(websearch、terminal、sendfile) +查一下济南和青岛近10年的GDP并画个图表 + +(gui、browser、human_interaction、改成英文) +进行以下gui任务,严格按照我的指令操作: +1、打开https://erpdemo.fanpusoft.com/main.aspx# +2、进行登录(账号密码调用human_interaction工具来问我) +3、点击销售管理 +4、点击新增客户 +5、在新增客户界面输入: +客户名称:minor;客户类别:零售商;公司地址:中国·青岛;公司简介:大狗叫叫叫;联系人姓名:叮咚鸡; +点击性别选项:女;部门:抽象部;生日:2004-07-09; +6、点击客户档案并连按两次空格(用来下滑到网页底部) +7、点击批量上传 +8、在弹出的文件选择器的文件名输入框中输入"C:\Users\86166\Desktop\风景\8.png"并回车 +9、输入跟进日期:2026-07-16 +10、点击保存 +11、点击确定 diff --git a/dongtai.py b/dongtai.py deleted file mode 100644 index 90c1050..0000000 --- a/dongtai.py +++ /dev/null @@ -1,660 +0,0 @@ -import os -import json -import argparse -import subprocess -import time -import socket -import re -from pathlib import Path -from urllib.parse import urljoin, urlparse -from typing import List, Dict, Any, Optional - -from selenium import webdriver -from selenium.webdriver.edge.options import Options -from selenium.webdriver.edge.service import Service -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from webdriver_manager.microsoft import EdgeChromiumDriverManager - - -# ---------------------------- 工具函数 ---------------------------- -def get_absolute_url(base_url: str, href: str) -> Optional[str]: - if not href: - return None - href = href.strip() - if href.startswith(('javascript:', 'mailto:', 'tel:', '#')): - return None - absolute = urljoin(base_url, href) - parsed = urlparse(absolute) - if parsed.scheme in ('http', 'https', 'file'): - return absolute - return None - - -def sanitize_filename(url: str) -> str: - """根据 URL 生成合法的文件名(不含扩展名)""" - parsed = urlparse(url) - path = parsed.path.strip('/') - if not path: - path = "index" - safe = re.sub(r'[\\/*?:"<>|]', '_', path) - if len(safe) > 100: - safe = safe[:100] - return safe - - -# ---------------------------- 语义上下文推断(加强版) ---------------------------- -def infer_context_from_attrs(tag_name: str, attrs: Dict[str, str], text: str = "") -> str: - class_list = attrs.get("class", "").split() - id_ = attrs.get("id", "") - href = attrs.get("href", "") - features = " ".join(class_list) + " " + id_ - - # 对 标签的判断 - if tag_name == "a": - if any(w in features for w in ["btn", "button", "tab", "nav-btn", "menu-item", "action-btn"]): - return "按钮" - - # 通用特征判断 - if "search" in features or "nav-search" in features: - return "搜索框" - if any(w in features for w in ["header", "top-bar", "masthead", "nav"]): - return "顶部导航" - if any(w in features for w in ["sidebar", "aside", "right-entry"]): - return "侧边栏" - if "footer" in features: - return "页脚" - if any(w in features for w in ["channel", "partition", "channel-icons"]): - return "频道/分区" - if any(w in features for w in ["carousel", "banner", "slider"]): - return "轮播/横幅" - if any(w in features for w in ["video-card", "feed-card", "card-item", "card"]): - return "视频卡片" - if "main" in features or "content" in features: - return "主要内容区" - if tag_name == "a": - if "cm.bilibili.com" in href or "ad" in features: - return "广告链接" - return "普通链接" - if tag_name in ("input", "textarea"): - return "输入控件" - if tag_name == "button": - return "按钮" - if tag_name == "select": - return "下拉选择" - # 对新加入的交互元素(div/li/span/img等)做一些推断 - if tag_name in ("div", "li", "span", "img", "td", "label"): - if any(w in features for w in ["btn", "button", "action", "clickable", "tab", "menu-item"]): - return "按钮" - if any(w in features for w in ["card", "item", "cell", "list-item"]): - return "卡片/列表项" - if "dropdown" in features or "menu" in features: - return "菜单" - if "dialog" in features or "modal" in features: - return "弹窗" - if "icon" in features: - return "图标" - if tag_name == "img": - return "图片" - return "交互区域" - return "其他" - - -# ---------------------------- 事件绑定与递归(增强版:模拟点击所有可交互控件) ---------------------------- -def add_events_and_recurse(controls: List[Dict[str, Any]], base_url: str, - depth: int, max_depth: int, visited: set, driver): - """ - 增强版:对于点击后 URL 不变的控件,会检测页面中动态出现的新控件, - 并对新控件(如下拉菜单项)进行递归导航抓取。 - """ - def get_control_signature(ctrl): - """生成控件的唯一签名""" - tp = ctrl["type"] - attrs = ctrl["attributes"] - text = ctrl.get("text", "") - cls = attrs.get("class", "") - id_ = attrs.get("id", "") - return f"{tp}|{text}|{cls}|{id_}" - - original_signatures = {get_control_signature(c) for c in controls} - - for ctrl in controls: - tp = ctrl["type"] - attrs = ctrl["attributes"] - idx = ctrl.get("index") - events = [] - navigate_url = None - reason = "" - clicked_for_navigation = False - - # ---------- URL 预判 ---------- - if tp == "a": - href = attrs.get("href") - if href: - abs_url = get_absolute_url(base_url, href) - if abs_url: - navigate_url = abs_url - reason = "点击链接跳转" - - if tp == "button" or (tp == "input" and attrs.get("type", "").lower() in ("submit", "button", "image")): - form_action = ctrl.get("form_action") - if form_action: - abs_action = get_absolute_url(base_url, form_action) - if abs_action: - navigate_url = abs_action - reason = "提交表单跳转" - else: - events.append({"type": "submit", "description": "提交表单(无明确跳转)"}) - else: - events.append({"type": "submit", "description": "提交表单(无 action)"}) - - if "alert" in attrs.get("onclick", "") or "confirm" in attrs.get("onclick", ""): - events.append({"type": "popup", "description": "触发弹窗"}) - - if tp == "input": - input_type = attrs.get("type", "text").lower() - if input_type in ("text", "password", "email", "search", "number", "tel", "url", ""): - events.append({"type": "input", "description": "输入文字"}) - events.append({"type": "change", "description": "内容改变"}) - elif input_type == "range": - events.append({"type": "input", "description": "滑动改变值"}) - elif input_type in ("checkbox", "radio"): - events.append({"type": "change", "description": "切换状态"}) - - if tp == "select": - events.append({"type": "change", "description": "选择项改变"}) - - if tp == "textarea": - events.append({"type": "input", "description": "输入多行文字"}) - events.append({"type": "change", "description": "内容改变"}) - - if tp in ("div", "li", "span", "img", "td", "label") and ctrl.get("is_interactive"): - onclick = attrs.get("onclick", "") - data_href = attrs.get("data-href", "") - if data_href: - abs_url = get_absolute_url(base_url, data_href) - if abs_url: - navigate_url = abs_url - reason = "点击交互跳转" - elif "location.href" in onclick or "window.open" in onclick: - match = re.search(r"""['"]([^'"]+)['"]""", onclick) - if match: - abs_url = get_absolute_url(base_url, match.group(1)) - if abs_url: - navigate_url = abs_url - reason = "点击交互跳转" - - # 弹窗兜底 - if tp in ("input", "button") or (tp in ("div", "li", "span", "img") and ctrl.get("is_interactive")): - if ("alert" in attrs.get("onclick", "") or "confirm" in attrs.get("onclick", "")) and \ - not any(e["type"] == "popup" for e in events): - events.append({"type": "popup", "description": "触发弹窗"}) - - # ========== 模拟点击与动态内容探索 ========== - if depth < max_depth and idx is not None: - # 情况1:有预知 URL - if navigate_url and navigate_url not in visited: - try: - target_element = driver.find_element(By.CSS_SELECTOR, f'[data-ctrl-idx="{idx}"]') - pre_url = driver.current_url - target_element.click() - time.sleep(1) - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - - handles = driver.window_handles - if len(handles) > 1: - original_handle = driver.current_window_handle - for handle in handles: - if handle != original_handle: - driver.switch_to.window(handle) - break - new_url = driver.current_url - if new_url not in visited: - visited.add(new_url) - target_page = process_source(new_url, depth + 1, max_depth, visited, driver) - event = {"type": "navigate", "url": new_url, "description": reason, - "target_page": target_page} - events.append(event) - driver.close() - driver.switch_to.window(original_handle) - else: - new_url = driver.current_url - if new_url != pre_url and new_url not in visited: - visited.add(new_url) - target_page = process_source(new_url, depth + 1, max_depth, visited, driver) - event = {"type": "navigate", "url": new_url, "description": reason, - "target_page": target_page} - events.append(event) - driver.back() - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - clicked_for_navigation = True - except Exception as e: - events.append({"type": "navigate", "url": navigate_url, "description": reason, - "target_page": {"url": navigate_url, "error": str(e)}}) - clicked_for_navigation = True - - # 情况2:无预知 URL,但可交互 → 尝试点击并检查是否出现下拉菜单 - elif ctrl.get("is_interactive") and not clicked_for_navigation: - try: - target_element = driver.find_element(By.CSS_SELECTOR, f'[data-ctrl-idx="{idx}"]') - pre_url = driver.current_url - target_element.click() - time.sleep(0.8) - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - - new_url = driver.current_url - if new_url != pre_url and new_url not in visited: - # 直接跳转了 - visited.add(new_url) - target_page = process_source(new_url, depth + 1, max_depth, visited, driver) - event = {"type": "navigate", "url": new_url, "description": "点击交互跳转(运行时发现)", - "target_page": target_page} - events.append(event) - driver.back() - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - clicked_for_navigation = True - else: - # URL 没变,检测动态出现的下拉菜单项 - # 使用更可靠的方法:直接查找可能是下拉项的
  • 或 ,并排除原始控件 - sub_items = driver.execute_script(""" - let candidates = []; - // 查找常见菜单结构:ul.submenu, div.dropdown-menu 下的 li/a - let menus = document.querySelectorAll('ul, div[class*="menu"], div[class*="dropdown"]'); - for (let menu of menus) { - let items = menu.querySelectorAll('li, a'); - for (let item of items) { - // 跳过隐藏元素 - if (item.offsetParent === null) continue; - let text = item.innerText.trim().substring(0, 50); - if (!text) continue; - // 排除与原始控件文本相同的元素(父菜单本身可能被包含) - candidates.push({ - tag: item.tagName.toLowerCase(), - text: text, - outerHTML: item.outerHTML.substring(0, 200) - }); - } - } - return candidates; - """) - - # 去重,只保留之前没有的项 - clicked_sub_items = set() - for item in sub_items: - sig = f"{item['tag']}|{item['text']}" - if sig not in original_signatures and sig not in clicked_sub_items: - # 尝试定位并点击 - try: - # 用文本查找具体元素(更稳定) - elem = driver.find_element(By.XPATH, - f"//{item['tag']}[normalize-space()='{item['text']}']") - elem.click() - time.sleep(0.8) - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - new_url2 = driver.current_url - if new_url2 != pre_url and new_url2 not in visited: - visited.add(new_url2) - sub_target = process_source(new_url2, depth + 1, max_depth, visited, driver) - event = {"type": "navigate", "url": new_url2, - "description": f"下拉菜单项: {item['text']}", - "target_page": sub_target} - events.append(event) - driver.back() - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - # 回退后重新点击父菜单,确保下拉菜单仍然展开 - target_element = driver.find_element(By.CSS_SELECTOR, f'[data-ctrl-idx="{idx}"]') - target_element.click() - time.sleep(0.5) - clicked_sub_items.add(sig) - except Exception: - continue - except Exception as e: - # 可以在这里打印调试信息,但不影响流程 - pass - - if not events: - events.append({"type": "unknown", "description": "未识别的事件"}) - - ctrl["events"] = events - - -# ---------------------------- 核心抓取逻辑(增强版:去重+注入索引) ---------------------------- -def build_page_tree_from_url(driver, url: str, depth: int, max_depth: int, - visited: set) -> Dict[str, Any]: - print(f"正在抓取 (深度 {depth}/{max_depth}): {url}") - visited.add(url) - - try: - driver.get(url) - WebDriverWait(driver, 15).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - driver.implicitly_wait(2) - except Exception as e: - return {"url": url, "error": str(e)} - - # ---------- 提取所有交互元素(去重:只保留最外层交互元素,并注入索引) ---------- - try: - raw_controls = driver.execute_script(""" - const tags = ['a', 'input', 'button', 'select', 'textarea', - 'div', 'li', 'span', 'img', 'td', 'label']; - let allElements = []; - for (let tag of tags) { - let elems = document.querySelectorAll(tag); - for (let elem of elems) { - if (tag === 'input' && elem.type === 'hidden') continue; - if (tag === 'a' && !elem.href && !elem.onclick) continue; - - let isInteractive = false; - if (['div', 'li', 'span', 'img', 'td', 'label'].includes(tag)) { - if (elem.onclick) isInteractive = true; - let role = elem.getAttribute('role'); - if (role && ['button', 'link', 'tab', 'menuitem'].includes(role)) isInteractive = true; - if (elem.hasAttribute('tabindex') && elem.getAttribute('tabindex') !== '-1') isInteractive = true; - if (elem.hasAttribute('data-action') || elem.hasAttribute('data-href')) isInteractive = true; - let cls = elem.className || ''; - if (/(btn|button|clickable|action|link|tab|menu-item|card|cell)/i.test(cls)) { - isInteractive = true; - } - if (!isInteractive) continue; - } - - allElements.push({element: elem, tag: tag, isInteractive: isInteractive}); - } - } - - // 去重:如果某元素被另一个更外层的交互元素包含,则丢弃它 - let filtered = []; - let idx = 0; - for (let item of allElements) { - let elem = item.element; - let isChild = false; - // 检查是否被之前的某个元素包含 - for (let prev of filtered) { - if (prev.element.contains(elem) && prev.element !== elem) { - isChild = true; - break; - } - } - if (!isChild) { - // 注入唯一索引 - elem.setAttribute('data-ctrl-idx', idx); - let attrs = {}; - for (let attr of elem.attributes) { - attrs[attr.name] = attr.value; - } - let formAction = null; - if (item.tag === 'button' || (item.tag === 'input' && ['submit','button','image'].includes(elem.type))) { - let form = elem.closest('form'); - if (form) formAction = form.getAttribute('action') || ''; - } - let text = elem.innerText ? elem.innerText.trim().substring(0, 50) : ''; - filtered.push({ - element: elem, // 用于后续 contains 检查 - tag: item.tag, - attributes: attrs, - text: text, - form_action: formAction, - is_interactive: item.isInteractive, - index: idx - }); - idx++; - } - } - // 只返回需要的数据(去掉 DOM 引用) - return filtered.map(({tag, attributes, text, form_action, is_interactive, index}) => ({ - tag, attributes, text, form_action, is_interactive, index - })); - """) - except Exception as e: - return {"url": url, "error": f"JavaScript提取失败: {str(e)}"} - - # 转换为统一格式 - controls = [] - for rc in raw_controls: - ctrl = { - "type": rc["tag"], - "attributes": rc["attributes"], - "text": rc.get("text", ""), - "context": infer_context_from_attrs(rc["tag"], rc["attributes"], rc.get("text", "")), - "events": [], - "form_action": rc.get("form_action"), - "is_interactive": rc.get("is_interactive", False), - "index": rc.get("index") # 保存索引,用于模拟点击 - } - controls.append(ctrl) - - # 填充事件和递归(将 driver 传入,实现模拟点击) - add_events_and_recurse(controls, url, depth, max_depth, visited, driver) - - # 删除内部使用的字段 - for ctrl in controls: - ctrl.pop("form_action", None) - if not ctrl["is_interactive"]: - del ctrl["is_interactive"] - - return {"url": url, "controls": controls} - - -def process_source(source: str, depth: int, max_depth: int, - visited: set, driver) -> Dict[str, Any]: - if source.startswith(('http://', 'https://', 'file://')): - url = source - else: - abs_path = os.path.abspath(source) - url = Path(abs_path).as_uri() - return build_page_tree_from_url(driver, url, depth, max_depth, visited) - - -# ---------------------------- 调试浏览器管理 ---------------------------- -def is_port_open(host, port): - try: - with socket.create_connection((host, port), timeout=1): - return True - except OSError: - return False - - -def ensure_debug_browser(): - debug_host = "127.0.0.1" - debug_port = 9222 - - if not is_port_open(debug_host, debug_port): - print("未检测到调试浏览器,正在启动 Edge 调试窗口...") - edge_path = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" - user_data_dir = r"C:\temp\edge_debug_profile" - try: - subprocess.Popen([ - edge_path, - f"--remote-debugging-port={debug_port}", - f"--user-data-dir={user_data_dir}" - ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except FileNotFoundError: - edge_path = "msedge" - subprocess.Popen([ - edge_path, - f"--remote-debugging-port={debug_port}", - f"--user-data-dir={user_data_dir}" - ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - print("请在新打开的 Edge 窗口中访问目标网站并完成登录,然后回到这里按 Enter 继续...") - input() - else: - print("检测到已有的调试浏览器,将直接使用。") - - if not is_port_open(debug_host, debug_port): - raise RuntimeError("无法连接到调试端口,请检查 Edge 是否正常启动。") - - -# ---------------------------- 人性化报告生成 ---------------------------- -def short_text_for_control(ctrl: Dict[str, Any]) -> str: - tp = ctrl["type"] - attrs = ctrl["attributes"] - is_interactive = ctrl.get("is_interactive", False) - base = "" - - if tp == "a": - href = attrs.get("href", "") - text = ctrl.get("text") or attrs.get("title") or attrs.get("aria-label") or href[:60] - base = f"链接 →{text} : {href}" - elif tp == "button": - text = ctrl.get("text") or attrs.get("value") or attrs.get("aria-label") or "按钮" - base = f"按钮: {text}" - elif tp == "input": - input_type = attrs.get("type", "text") - text = ctrl.get("text") or attrs.get("value") or attrs.get("placeholder") or "" - base = f"输入框 ({input_type})" - if text: - base += f" [{text}]" - elif tp == "textarea": - text = ctrl.get("text") or attrs.get("placeholder") or "" - base = "多行文本框" - if text: - base += f" [{text}]" - elif tp == "select": - text = ctrl.get("text") or "" - base = "下拉选择框" - if text: - base += f" [{text}]" - elif is_interactive: - text = ctrl.get("text") or "" - role = attrs.get("role", "") - tag_desc = f"{tp}控件" - if role == "button": - tag_desc = "按钮(ARIA)" - elif role == "link": - tag_desc = "链接(ARIA)" - elif "card" in (attrs.get("class", "") + attrs.get("id", "")): - tag_desc = "卡片" - base = f"可交互{tag_desc}" - if text: - base += f" [{text}]" - if attrs.get("data-href"): - base += f" (目标: {attrs['data-href']})" - else: - text = ctrl.get("text") or "" - base = f"{tp} 控件" - if text: - base += f" [{text}]" - return base - - -def describe_page_tree(page: Dict[str, Any], prefix: str = "", - max_depth: int = 1, current_depth: int = 1, - is_last: bool = True) -> str: - url = page.get("url", "未知页面") - if "error" in page: - return f"{prefix}页面 {url} 抓取失败: {page['error']}\n" - - controls = page.get("controls", []) - lines = [] - connector = "└── " if is_last else "├── " - lines.append(f"{prefix}{connector}页面: {url}") - - groups: Dict[str, List[Dict]] = {} - for ctrl in controls: - ctx = ctrl["context"] - groups.setdefault(ctx, []).append(ctrl) - - sorted_groups = sorted(groups.items(), key=lambda x: x[0]) - group_count = len(sorted_groups) - - for idx, (context, items) in enumerate(sorted_groups): - is_last_group = (idx == group_count - 1) - group_connector = "└── " if is_last_group else "├── " - group_line = f"{prefix}{' ' if is_last else '│ '}{group_connector}【{context}】({len(items)}个)" - lines.append(group_line) - - item_prefix = prefix + (" " if is_last else "│ ") + (" " if is_last_group else "│ ") - - for j, ctrl in enumerate(items): - is_last_item = (j == len(items) - 1) - item_connector = "└── " if is_last_item else "├── " - desc = short_text_for_control(ctrl) - lines.append(f"{item_prefix}{item_connector}{desc}") - - for event in ctrl.get("events", []): - if "target_page" in event and current_depth < max_depth: - sub_page = event["target_page"] - new_prefix = item_prefix + (" " if is_last_item else "│ ") - sub_desc = describe_page_tree( - sub_page, - prefix=new_prefix, - max_depth=max_depth, - current_depth=current_depth + 1, - is_last=True - ) - lines.append(sub_desc) - - return "\n".join(lines) + "\n" - - -def generate_human_report(root_page: Dict[str, Any], max_depth: int) -> str: - header = f"🌐 页面交互控件分析报告(增强版)\n深度限制: {max_depth}\n" + "=" * 40 + "\n" - tree = describe_page_tree(root_page, prefix="", max_depth=max_depth, - current_depth=1, is_last=True) - return header + tree - - -# ---------------------------- 命令行入口 ---------------------------- -def main(): - parser = argparse.ArgumentParser(description="通用网页交互控件分析工具(增强版:模拟点击、去重)") - parser.add_argument("source", help="起始 URL 或本地 HTML 文件路径") - parser.add_argument("-N", "--depth", type=int, default=1, help="最大递归深度(默认1)") - parser.add_argument("-o", "--output", help="输出文本文件路径,默认打印到控制台") - parser.add_argument("--json", action="store_true", help="同时保存机器可读的 JSON 文件(与 -o 同名 .json)") - args = parser.parse_args() - - max_depth = max(1, args.depth) - - if args.source.startswith(('http://', 'https://')): - start_url = args.source - else: - abs_path = os.path.abspath(args.source) - start_url = Path(abs_path).as_uri() - visited = {start_url} - - ensure_debug_browser() - - service = Service(EdgeChromiumDriverManager().install()) - options = Options() - options.add_experimental_option("debuggerAddress", "127.0.0.1:9222") - - driver = webdriver.Edge(service=service, options=options) - driver.implicitly_wait(5) - - try: - tree = process_source(start_url, depth=1, max_depth=max_depth, - visited=visited, driver=driver) - finally: - driver.quit() - - report = generate_human_report(tree, max_depth) - - if args.output: - with open(args.output, 'w', encoding='utf-8') as f: - f.write(report) - print(f"人性化报告已保存到 {args.output}") - if args.json: - json_path = args.output.rsplit('.', 1)[0] + '.json' - with open(json_path, 'w', encoding='utf-8') as jf: - json.dump(tree, jf, ensure_ascii=False, indent=2) - print(f"机器可读 JSON 已保存到 {json_path}") - else: - print(report) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/jingtai.py b/jingtai.py deleted file mode 100644 index 863382d..0000000 --- a/jingtai.py +++ /dev/null @@ -1,306 +0,0 @@ -import os -import re -import json -import argparse -from pathlib import Path -from typing import Dict, Any, List, Optional -from urllib.parse import urljoin, urlparse - -from selenium import webdriver -from selenium.webdriver.edge.options import Options -from selenium.webdriver.edge.service import Service -from selenium.webdriver.support.ui import WebDriverWait -from webdriver_manager.microsoft import EdgeChromiumDriverManager - - -# -------------------- 工具函数 -------------------- -def get_absolute_url(base_url: str, href: str) -> Optional[str]: - if not href: - return None - href = href.strip() - if href.startswith(('javascript:', 'mailto:', 'tel:', '#')): - return None - absolute = urljoin(base_url, href) - parsed = urlparse(absolute) - if parsed.scheme in ('http', 'https', 'file'): - return absolute - return None - - -# -------------------- 语义上下文推断 -------------------- -def infer_context_from_attrs(tag_name: str, attrs: Dict[str, str], text: str = "") -> str: - class_list = attrs.get("class", "").split() - id_ = attrs.get("id", "") - href = attrs.get("href", "") - features = " ".join(class_list) + " " + id_ - - if tag_name == "a": - if any(w in features for w in ["btn", "button", "tab", "nav-btn", "menu-item", "action-btn"]): - return "按钮" - if "cm.bilibili.com" in href or "ad" in features: - return "广告链接" - return "普通链接" - - if "search" in features or "nav-search" in features: - return "搜索框" - if any(w in features for w in ["header", "top-bar", "masthead", "nav"]): - return "顶部导航" - if any(w in features for w in ["sidebar", "aside", "right-entry"]): - return "侧边栏" - if "footer" in features: - return "页脚" - if any(w in features for w in ["channel", "partition", "channel-icons"]): - return "频道/分区" - if any(w in features for w in ["carousel", "banner", "slider"]): - return "轮播/横幅" - if any(w in features for w in ["video-card", "feed-card", "card-item", "card"]): - return "视频卡片" - if "main" in features or "content" in features: - return "主要内容区" - if tag_name in ("input", "textarea"): - return "输入控件" - if tag_name == "button": - return "按钮" - if tag_name == "select": - return "下拉选择" - if tag_name in ("div", "li", "span", "img", "td", "label"): - if any(w in features for w in ["btn", "button", "action", "clickable", "tab", "menu-item"]): - return "按钮" - if any(w in features for w in ["card", "item", "cell", "list-item"]): - return "卡片/列表项" - if "dropdown" in features or "menu" in features: - return "菜单" - if "dialog" in features or "modal" in features: - return "弹窗" - if "icon" in features: - return "图标" - if tag_name == "img": - return "图片" - return "交互区域" - return "其他" - - -# -------------------- 控件提取(去重 + 仅保留元素大小) -------------------- -def extract_controls(driver) -> List[Dict[str, Any]]: - """ - 无头模式下提取所有可交互元素(去重), - 仅返回元素的尺寸(CSS 像素),不包含坐标。 - """ - raw = driver.execute_script(""" - const tags = ['a', 'input', 'button', 'select', 'textarea', - 'div', 'li', 'span', 'img', 'td', 'label']; - let allElements = []; - - // 1. 收集候选元素 - for (let tag of tags) { - let elems = document.querySelectorAll(tag); - for (let elem of elems) { - if (tag === 'input' && elem.type === 'hidden') continue; - if (tag === 'a' && !elem.href && !elem.onclick) continue; - - let isInteractive = false; - if (['div', 'li', 'span', 'img', 'td', 'label'].includes(tag)) { - if (elem.onclick) isInteractive = true; - let role = elem.getAttribute('role'); - if (role && ['button', 'link', 'tab', 'menuitem'].includes(role)) isInteractive = true; - if (elem.hasAttribute('tabindex') && elem.getAttribute('tabindex') !== '-1') isInteractive = true; - if (elem.hasAttribute('data-action') || elem.hasAttribute('data-href')) isInteractive = true; - let cls = elem.className || ''; - if (/(btn|button|clickable|action|link|tab|menu-item|card|cell)/i.test(cls)) { - isInteractive = true; - } - if (!isInteractive) continue; - } - allElements.push(elem); - } - } - - // 2. 去重:只保留不被其他任何元素包含的最外层元素 - let uniqueElements = []; - for (let elem of allElements) { - let isChild = false; - for (let other of allElements) { - if (other !== elem && other.contains(elem)) { - isChild = true; - break; - } - } - if (!isChild) { - uniqueElements.push(elem); - } - } - - // 3. 获取每个元素的属性、文本和尺寸 - return uniqueElements.map(elem => { - let tag = elem.tagName.toLowerCase(); - let attrs = {}; - for (let attr of elem.attributes) { - attrs[attr.name] = attr.value; - } - let formAction = null; - if (tag === 'button' || (tag === 'input' && ['submit','button','image'].includes(elem.type))) { - let form = elem.closest('form'); - if (form) formAction = form.getAttribute('action') || ''; - } - let text = elem.innerText ? elem.innerText.trim().substring(0, 50) : ''; - let rect = elem.getBoundingClientRect(); - - return { - tag: tag, - attributes: attrs, - text: text, - form_action: formAction, - is_interactive: true, - size: { - width: rect.width, - height: rect.height - } - }; - }); - """) - - controls = [] - for item in raw: - ctrl = { - "type": item["tag"], - "attributes": item["attributes"], - "text": item.get("text", ""), - "context": infer_context_from_attrs(item["tag"], item["attributes"], item.get("text", "")), - "form_action": item.get("form_action"), - "is_interactive": item.get("is_interactive", False), - "size": item.get("size", {}) - } - controls.append(ctrl) - return controls - - -# -------------------- 人性化报告生成 -------------------- -def short_text_for_control(ctrl: Dict[str, Any]) -> str: - tp = ctrl["type"] - attrs = ctrl["attributes"] - is_interactive = ctrl.get("is_interactive", False) - size = ctrl.get("size", {}) - size_str = "" - if size: - w, h = size.get("width", 0), size.get("height", 0) - size_str = f" [大小:{w:.0f}x{h:.0f}]" - - base = "" - if tp == "a": - href = attrs.get("href", "") - text = ctrl.get("text") or attrs.get("title") or attrs.get("aria-label") or href[:60] - base = f"链接 →{text}" - if href: - base += f" : {href}" - elif tp == "button": - text = ctrl.get("text") or attrs.get("value") or attrs.get("aria-label") or "按钮" - base = f"按钮: {text}" - elif tp == "input": - input_type = attrs.get("type", "text") - text = ctrl.get("text") or attrs.get("value") or attrs.get("placeholder") or "" - base = f"输入框 ({input_type})" - if text: - base += f" [{text}]" - elif tp == "textarea": - text = ctrl.get("text") or attrs.get("placeholder") or "" - base = "多行文本框" - if text: - base += f" [{text}]" - elif tp == "select": - text = ctrl.get("text") or "" - base = "下拉选择框" - if text: - base += f" [{text}]" - elif is_interactive: - text = ctrl.get("text") or "" - role = attrs.get("role", "") - tag_desc = f"{tp}控件" - if role == "button": - tag_desc = "按钮(ARIA)" - elif role == "link": - tag_desc = "链接(ARIA)" - elif "card" in (attrs.get("class", "") + attrs.get("id", "")): - tag_desc = "卡片" - base = f"可交互{tag_desc}" - if text: - base += f" [{text}]" - if attrs.get("data-href"): - base += f" (目标: {attrs['data-href']})" - else: - text = ctrl.get("text") or "" - base = f"{tp} 控件" - if text: - base += f" [{text}]" - return base + size_str - - -def generate_report(url: str, controls: List[Dict[str, Any]]) -> str: - lines = [f"🌐 页面交互控件分析报告(静态单页·无头模式)\n页面: {url}\n" + "=" * 50] - groups: Dict[str, List[Dict]] = {} - for ctrl in controls: - ctx = ctrl["context"] - groups.setdefault(ctx, []).append(ctrl) - - sorted_groups = sorted(groups.items(), key=lambda x: x[0]) - for context, items in sorted_groups: - lines.append(f"├── 【{context}】({len(items)}个)") - for item in items: - desc = short_text_for_control(item) - lines.append(f"│ ├── {desc}") - return "\n".join(lines) - - -# -------------------- 主程序 -------------------- -def main(): - parser = argparse.ArgumentParser(description="静态单页交互控件分析(Edge·无头模式·仅元素大小)") - parser.add_argument("source", help="起始 URL 或本地 HTML 文件路径") - parser.add_argument("-o", "--output", help="输出报告文件路径,默认打印到控制台") - parser.add_argument("--json", action="store_true", help="同时保存机器可读的 JSON 文件(与 -o 同名 .json)") - args = parser.parse_args() - - # 构造 URL - if args.source.startswith(('http://', 'https://')): - url = args.source - else: - abs_path = os.path.abspath(args.source) - url = Path(abs_path).as_uri() - - # 无头模式 Edge,设置窗口大小 - options = Options() - options.add_argument("--headless=new") # 新版无头模式 - options.add_argument("--window-size=1920,1080") # 固定视口大小 - options.add_argument("--disable-gpu") # 禁用 GPU 加速(无头下建议) - - service = Service(EdgeChromiumDriverManager().install()) - driver = webdriver.Edge(service=service, options=options) - - try: - driver.get(url) - WebDriverWait(driver, 15).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - controls = extract_controls(driver) - finally: - driver.quit() - - report = generate_report(url, controls) - - # 输出结果 - if args.output: - with open(args.output, 'w', encoding='utf-8') as f: - f.write(report) - print(f"人性化报告已保存到 {args.output}") - if args.json: - json_path = args.output.rsplit('.', 1)[0] + '.json' - with open(json_path, 'w', encoding='utf-8') as jf: - json.dump({ - "url": url, - "controls": controls - }, jf, ensure_ascii=False, indent=2) - print(f"机器可读 JSON 已保存到 {json_path}") - else: - print(report) - - -if __name__ == "__main__": - main() diff --git a/src/agent/core/__pycache__/config_manager.cpython-312.pyc b/src/agent/core/__pycache__/config_manager.cpython-312.pyc index cd16cb2848955cf03f49f8f6dae17c9008260b23..43740b2cc9f18b970bccabc9b522bbdf84035bb9 100644 GIT binary patch delta 1954 zcmZ`)ZERCj7`~_N?RwXBjE&K?-PV@2>+aaPFWA@zUt0?dyG^%Yup-^uDPyKv@w6Zi zg=k=gKSDVCXvhL4M6yH^AeX=dVlZlqF;R?vNd4hgmJq&jabiFv_`bKB64ADKp67kf zdC$jt-gA29BzgN?;{VcO$ra%9jJfxf=YoRpGaB=soU9`Eggq+}k3col7j)zL5;$F6%Rk`a?az=(?_ODB9l@2}Q$Q-CYA2OQ?HoS2(&h80-u6_h%N&nVSYS zL_2$fAz5VS`I-7??hH&&M+70cl9x0@_ZR0GubYamS^cUWC4y9%9#Czl?Sf=aH>nQPrGn&0htx9E<$_c_HK00Cy97y|@~CdqO9ZKMsz~*q z_6ly_b(`b5qwE(MZJ}7{YurORF2SbRR)ot4m;;9AzgG1Dhrg4g)0h04~?uT7O43 z!a@FH`y=ZeP%zyC^xmCsVt4rcl^)RmLmpGgR#lMYT0U?!*;M>10htBy#1M^+>>P~_ zC&sspj<6gEJ6v4Z&#WF1`n1dYX` z!`ntQcTxv~&E+6t9*L#2)nd?M2)d&9)=B!dUva|hn*J}vu+INVyoH}zy@Euj2KV67Gvxoguo;>fHHYwiSf}?fJXB8lGz1`A^Ti1@X~j>cs(g zB0IICdkKf&@vy7ysW2=8*`O^gdqJM+;hUfK^KgUlJQ(Z(e;k(0`%vP46#I!ccKeJc zP+sC&yQ}wWFRBe~e0CW=p9~*Mh7Tlr9|7OT>^;;f;wqZj-PGp&uN(~AvW9Z_q*qw zbML$7o_qElr60aaeP8G0nF##sx&1F{9fbUfgXy0!Bj{A&M?MR6H!;h2){u=YV6%Y> z*+NzTT*QLR3T$W1Vl}NED>-5qaZdSgP z4wbRR%nKtAcvS$G1N(rztd&&)SFkpw0{eigfGgP&Rt>DOr~+Ak5LypBkF~P~;5ybgPKN4XG#^F{I}K`w_KakoE2##-ZK`&{ zw#q=aiwE`<#_ez%zDo=iFSCq+OywkYZVQLv3C?$m_luk4*T8sGn=HO2(F5X}QiuB> z25dk`AcO$aT)qak}ZasCGUl9|pr$+N(}u7Cj{Pdwle;_{4KuehRakYd5ZFlKyeD-;toK* zdKyZ!3;x-ZPKY0BYUIzs>J{06rF4(h8`wo3v?e;h>Nxj^vjV zL0wK1=h3WNUev^@z16UjwohON*CU)m!1HrVHXlIPfKZ2U9$;qXKSLwJGHr1F8k##} z9Wl|=EKkDJDsivrn(1xyLEoqe0+Zcg8sGpkwp|UV0oyn98k1Mh*Q?=&~b+{oHWgV5W2^@}T105Ho z8kDL0ja$MaiNK~94+o;rVQ!vYK$TL;aEyhcp-r2&aI@&?Dv=9d@~jr^QVa|9$6Nnj zy5Mu3NTT?lduutSu^-N}0oS+*5SIX^Gx!JSQNOs^QzV}4vC>Hq?g=jX?_3Kk8GB+j{@8QW5{<+YYX@VoXh&>hb7af9M@x+6v&g2zTFAKH1+Y%d;%jhkTsgDH z%>D4P!2`cK^AKk2nV)g@q6>VU$nUE+o2WL@ zj+MYMsoaiX#l!&!|Ipc6plV)g|$#aJ6e2uky^g!~B;ss9A zvgT+_7L|ne8lOue$&~{+~cvL#cKm6-pRM%X^NV?x!}Tc-lxEg zOL-RbG^gXoUJFQ%k$^vW3thG+w~_wRl>MKU_b8;lJQY7R2Qy_FqJWPSkpm*kD!B6poI4ROw0=rfW zVq`b3lsWP>;LYZx-In82i!b-r z3hLbTmD~-jx!%p+$f`3?u;S+HJ;F+X&@0-YcA%IL*-HP!wDTudJI%b_MR%;O*D(V(b^54gyi|{Mc_$jsEyyol472R2pz8UN#@s;P>%icpmE#e%kWIgb&gi mS^X?XK8%mX%b^A(r4dfyuyWq$6(D~)agQ*)bvn7J48XrSiafUf delta 1439 zcmY*XZA?>V6n@`ATkgHRE%ehCE#)gxkV0KR#khqqstzqeQBr|*Ncpf3s<#(fvB;(~ zA=59J*q3vA-?F;x-sCywIq!4M zJ?}Yp{CAvuiq&89Jd4P)vTI%7=C;%7uNZt0t~2AR&LRKSpe<_K5)}JlJ*?S^b7Ff+ zCS{rKxT!IgS2j?fs>wj;5Q@tDco@Us_1(A>{#xIo7ISDp`a~50Eja6RFy&T(pwq#p zZcf?8pePEMAmPsA%=0>g0v4jPfXQKiQ*CSpBO*}eEtCcF;f)1)*xH!8s4y*!vi__S zvUyg8M!6^#D2C}qCy14LcviuJ)M(@N&($bU3X3+FAjhYJKP%WVOdHnpmmnl4{n=54 zk4Z}>@c!~l6vZA?h@2>d!!nG#2$OezdnCh)!nhELj1+_w&#y1VDx?K6At z%zSK{-^uOc)3?qZ`0>m>TUT&U>>dqsgDy4%T~i^O9NbPSxCeWTF#dwb=IlHmv#1JLD59rI}N`{tB|=` za$%Hh9Or*2N5&;_L)jmXis0hL%F3MD#r00v=gYPA#quxMS|cH|#qxRwGh3>pzLRlT z1UNqD&i zXIkXgt_JWfwFys%FZ5B+Qh1jFO&1Ojuyb(1>(D%Vh&!jutv31zAGu=H>*Wy1|OU;DE2I zb6_9@r~MnrIF;Q^Mh)pf$R=RVxFOr>7naaI)iBuo3ZIjT3e*T1nC;$xpMbGvk4%B# zNY4stuJ!zk^>C>-A76|8)T>o0s;IUaGJ^GZO>AxOzHI4oT0|i%qGdG{@+hb%tf1o5 zh!g@@6%sB{sD?hF9dC)v3d5MwQ%MblRWX-1gvXZ2jZ+p05p$+6W#UaO@z%CPQ``Z72B{?BvBpTC%I9YdaM?#%1DQPGR8Lg9D2RkIH zmRO`SN*wi6h}A#li8r?yZ^Lrqd8irwUcQ#}GX~c1J-i&4$SUO=X?x@a7Mdas_$ZtS zW0CE+7Um*eY>YMR@L+BYy#UP;UWA#E3cLcYkF3V8LjKM{bu)vl<0Fp{wR3&3qdWWY E-}L2`GXMYp diff --git a/src/agent/core/__pycache__/nodes.cpython-312.pyc b/src/agent/core/__pycache__/nodes.cpython-312.pyc index 2faf7775d89470c2112f13c31f03c35568fb4c30..7bf6d08c219390cb2c823d3021b6259944d1c344 100644 GIT binary patch delta 3564 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 diff --git a/src/agent/core/__pycache__/runtime.cpython-312.pyc b/src/agent/core/__pycache__/runtime.cpython-312.pyc index 1a0136920db712a8054fa5e2d47d77dca606e24f..d0da7fc15b5e9b6aa9cd254da0e944e72a862e39 100644 GIT binary patch delta 1124 zcmZWlYfM{Z82;WKTA-{g?I^ubdSSpi3Jg}Nc%?wx=7qSJ;J_gg59$D6DP<_Ovt>+N z!fpGe+ZYqbz>ol|^DG9Tg-qu#Lt~<-6A$2^AUuwu}Y19C_V-TT(!>uMm)5yPH&=R(}K8LE6_5J-mRx) z&6VUZyQ?0z6d3UyTcO;nAtUZgIx80SGrMq;%>>!F&t`xeJY##FU_wcuGI#rOx0K#J zlHf794YUMzmFV^NJ5I{2^O##jZkZE}iMZ-!?pLR& zcoh(>!b1h=_(6L%DDjK-I`HFwy$JR&zu0RaMdislk@Xy+Wn?U~^bq9XW2F@jSowdq zZ2+i-i12em&{q~`o#4`L+wMZn!^Gx43a0-lklibt=nbj{b%G_o<2&a2()}Pj-gflm zMke>+D7?K9PddILx?=IBcZj|80XxyZb!8wt7KnW43t#Kr^qvv3LCl`+XD50>^8*q8 z?A9m~S;TDbe0XY{4GjJo=nDnMxZ$4g`@QV!`M3nTbZTpSf&D1R4*DY}gWOdwH#*CG zyvTJ2`1v_6J2)As@n0sri2r(c?o@bkm|M6Un(ODzO@-zrNPE-U&0V@49+>17F7jq$ zbL`T|(9(F==O+@}h>v@3lJ%br5BIQV``CBRhk_S*sS5*Skx@N*5DqMoXE~R7_>#be zyrrStRqtwZHM9_HZftFL-r$>`@=akw2Ys~7k`T+B^L+g_4>t+mtB);15FdYh6h6c0 zWmWJ6ZY(=PDu0$e4L5P6;~d;#)*a7)R)msv4!!zqqJB-C9Yx5}Mg4}5ri*%4!em#} z0+ch}${Y&c*ne92f)J%@en*fj-`J5FrAYki-eIQxz*F#2hC@^>M9aAq8P!U(l3?aB zw;V|L+Tz$t!Z+F{v?QzwldBcfsvLM!ijSzNRYSR=)<&%rn+dL`0V%C(9cd)lusMt* zyQ?BKM*QTR)RfQp=Y7J%67y57LID3Te;>Iescfb|y(ZB_DW3eO$cYMh5)~nNS~M|6 elaN9gO^#79l53+WF}eeZw0}rq^fjE={K~&qoP*y0 delta 904 zcmZWkOH30{6n(cti=~~WEv-_@&@W&uwqi>aQS9(h@Iyj0C;@_+7g14&6@$j9afJ&t zQLc%JA!;HEBgSaP;3sH66#T^ai6Wp2H}2GopPiccrUW!`7U!OO&b#N{IXeNL$HDZJ z=Q9ZVoQB$6zZ2I@yMnM$J{U9s5FGZ~@LHjk`|D9j{x)=h6<_c*W2s{P!w3WVA*EmjJd7>_l> zBRm%?f`I%Y)&K^3(s$Umfl!{>drInH0d_B~SQ4a}y_%V9APsC@eRdL#$ER5rbY{SB>t&JQv30@U_z@*85Xk*PgPIR;GeLmCVn`_>f4foZ5 z98lq&e5TQ=fnW0F=25 dict: "LOOP_DETECT_REPEATED_TOOL_WARN": "3", "LOOP_DETECT_REPEATED_TOOL_END": "5", "SEND_FILE_SIZE_LIMIT": "10", + "EMAIL_ADDRESS": "", + "EMAIL_AUTH_CODE": "", } diff --git a/src/agent/core/env_config.json b/src/agent/core/env_config.json index c91f779..6d8bc7e 100644 --- a/src/agent/core/env_config.json +++ b/src/agent/core/env_config.json @@ -1,9 +1,9 @@ { "WORKING_DIR": "c:\\Users\\86166\\Desktop\\Agent_Learning_minor\\Agent", "WORKSPACE_DIR": "src\\agent\\workspace", - "TTS_BASE_URL": "127.0.0.0:8902", - "ASR_BASE_URL": "127.0.0.0:8901", - "RAG_BASE_URL": "127.0.0.0:8903", + "TTS_BASE_URL": "http://localhost:8902", + "ASR_BASE_URL": "http://localhost:8901", + "RAG_BASE_URL": "http://localhost:8903", "TAVILY_API_KEY": "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", "LLM_CONTEXT_WINDOW": "262144", "MINI_COMPRESS_RATE": "0.4", @@ -17,7 +17,9 @@ "LOOP_DETECT_TODOLIST_STALE_ROUNDS": "20", "LOOP_DETECT_REPEATED_TOOL_WARN": "3", "LOOP_DETECT_REPEATED_TOOL_END": "5", - "SEND_FILE_SIZE_LIMIT": "10", + "SEND_FILE_SIZE_LIMIT": "30", + "EMAIL_ADDRESS": "3139785954@qq.com", + "EMAIL_AUTH_CODE": "hfjkjqnlhjvqdeef", "models": [ { "id": "a44b226e", diff --git a/src/agent/core/llm.py b/src/agent/core/llm.py index 356d641..de6fe7e 100644 --- a/src/agent/core/llm.py +++ b/src/agent/core/llm.py @@ -41,7 +41,7 @@ class ChatQwen(ChatOpenAI): """Qwen 系列模型的 LangChain 封装,扩展多模态与 TTS 能力。 属性: - output_type: 默认输出模式 text | audio | both。 + output_type: 默认输出模式 text | voice。 stream_play: TTS 后是否本地播放。 last_audio_kwargs: 最近一次 TTS 的文件路径与时长等元数据。 @@ -213,7 +213,7 @@ class ChatQwen(ChatOpenAI): } ] } - endpoint = f"{env_utils.ASR_BASE_URL.rstrip('/')}/chat/completions" + endpoint = f"{env_utils.ASR_BASE_URL.rstrip('/')}/v1/chat/completions" resp = requests.post( endpoint, headers={"Content-Type": "application/json"}, @@ -370,11 +370,11 @@ class ChatQwen(ChatOpenAI): 输入: result: ChatResult 含 AIMessage。 - output_type: audio | both。 + output_type: voice。 audio_output_dir: WAV 保存目录。 输出: - 可能清空 audio 模式下 content 的 ChatResult;设置 last_audio_kwargs。 + 设置 last_audio_kwargs 的 ChatResult。 系统定位: _generate 在模型返回后的后处理。 @@ -382,7 +382,7 @@ class ChatQwen(ChatOpenAI): 可扩展性: 可上传对象存储而非本地路径。 """ - if output_type not in ("audio", "both") or not audio_output_dir: + if output_type != "voice" or not audio_output_dir: return result os.makedirs(audio_output_dir, exist_ok=True) for idx, gen in enumerate(result.generations): @@ -391,6 +391,9 @@ class ChatQwen(ChatOpenAI): msg = gen.message if not isinstance(msg, AIMessage): continue + # 跳过含 tool_calls 的中间决策消息,仅对最终回复合成语音 + if msg.tool_calls: + continue text = assistant_text(msg.content) if not text.strip(): continue @@ -414,8 +417,6 @@ class ChatQwen(ChatOpenAI): } if self.stream_play: self.play_audio_bytes(audio_bytes) - if output_type == "audio": - msg.content = "" except Exception as e: print(f"[TTS ERROR] 合成语音失败: {e}") # 发生错误时也清空 last_audio_kwargs,避免污染 @@ -459,12 +460,8 @@ class ChatQwen(ChatOpenAI): result = super()._generate(messages, stop=stop, run_manager=run_manager, **kwargs) - if output_type in ("audio", "both"): + if output_type == "voice": result = self._apply_tts(result, output_type, audio_output_dir=audio_output_dir) - if output_type == "audio": - for gen in result.generations: - if isinstance(gen, ChatGeneration) and isinstance(gen.message, AIMessage): - gen.message.content = "" return result def _create_chat_result(self, response, generation_info=None) -> ChatResult: diff --git a/src/agent/core/nodes.py b/src/agent/core/nodes.py index d984bf5..18eade9 100644 --- a/src/agent/core/nodes.py +++ b/src/agent/core/nodes.py @@ -27,12 +27,29 @@ from agent.utils.image_utils import image_bytes_to_openai_image_url_part from agent.utils.tool_call_utils import normalize_tool_call +def _has_image_content(msg: HumanMessage) -> bool: + """判断 HumanMessage 是否包含图片内容(截图等 data URL)。 + + 输入: + msg: 待检查的 HumanMessage。 + + 输出: + True 表示 content 中包含 image_url 类型的内容块。 + """ + content = msg.content + if isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "image_url": + return True + return False + + def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list: - """过滤短期记忆:保留所有工具调用,工具返回结果仅保留最近 trajectory_rounds 轮。 + """过滤短期记忆:保留所有工具调用记录和工具文字返回值,图片截图仅保留最近 trajectory_rounds 轮。 输入: messages: 完整消息列表。 - trajectory_rounds: 保留最近 N 轮的工具返回结果;或负数表示不过滤。 + trajectory_rounds: 保留最近 N 轮的图片截图(合成 HumanMessage);<=0 表示不过滤任何消息。 输出: 过滤后的消息列表。 @@ -42,9 +59,9 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list: 场景下上下文过长。 规则: - - 保留所有非 ToolMessage 消息(SystemMessage、HumanMessage、AIMessage) - - ToolMessage 仅保留最近 trajectory_rounds 轮工具调用对应的返回结果 - - 每轮由一个 AIMessage(含 tool_calls)定义,其后的 ToolMessage 属于该轮 + - 保留所有 SystemMessage、HumanMessage(非合成图片)、AIMessage、ToolMessage + - 仅 process_tool_artifact 生成的合成图片 HumanMessage 按 trajectory_rounds 裁剪 + - 每轮由一个 AIMessage(含 tool_calls)定义 """ if trajectory_rounds <= 0: return list(messages) @@ -59,7 +76,8 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list: if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None): rounds_seen += 1 - if isinstance(msg, ToolMessage): + # 合成图片消息:仅保留最近 trajectory_rounds 轮 + if isinstance(msg, HumanMessage) and msg.additional_kwargs.get("synthetic") and _has_image_content(msg): if rounds_seen < trajectory_rounds: kept_indices.add(i) else: @@ -274,9 +292,18 @@ def process_tool_artifact(state: AgentState) -> dict: artifact = last_msg.artifact img_bytes_list: list[bytes] = [] + source_path = "" - if isinstance(artifact, dict) and "image_bytes" in artifact: - img_bytes_list.append(artifact["image_bytes"]) + if isinstance(artifact, dict): + if "image_bytes" in artifact: + img_bytes_list.append(artifact["image_bytes"]) + if "images" in artifact and isinstance(artifact["images"], list): + for item in artifact["images"]: + if isinstance(item, bytes): + img_bytes_list.append(item) + elif isinstance(item, dict) and "image_bytes" in item: + img_bytes_list.append(item["image_bytes"]) + source_path = artifact.get("file_path", "") or artifact.get("source_path", "") elif isinstance(artifact, list): for item in artifact: if isinstance(item, dict) and "image_bytes" in item: @@ -305,7 +332,7 @@ def process_tool_artifact(state: AgentState) -> dict: ) return {"messages": [human_msg]} - content_parts.append({"type": "text", "text": "上面是工具返回的截图。"}) + content_parts.append({"type": "text", "text": "上面是工具返回的截图。" + (f"(来源:{source_path})" if source_path else "")}) if failed_count > 0: content_parts[-1]["text"] += f"(其中 {failed_count} 张图片处理失败,请忽略)" diff --git a/src/agent/core/runtime.py b/src/agent/core/runtime.py index d8808b8..8713f8c 100644 --- a/src/agent/core/runtime.py +++ b/src/agent/core/runtime.py @@ -282,7 +282,7 @@ def execute_agent( runtime: Agent 运行时配置。 chat_history: Web UI 聊天历史(dict 列表);末条须为 user。 session_id: 会话 ID,用于历史、文件目录、manifest 同步。 - output_type: text | audio | both,传入图 metadata。 + output_type: text | voice,传入图 metadata。 agent_mode: agent(默认)| team(占位提示)。 输出: @@ -317,6 +317,10 @@ def execute_agent( # Plan 模式使用专用系统提示词 system_prompt = PLAN_MODE_PROMPT if agent_mode == "plan" else runtime.system_prompt + # 语音模式下追加简洁性提示 + if output_type == "voice": + system_prompt = system_prompt + "\n\n【回复风格要求】\n当前处于语音输出模式,你的文字回复必须尽可能简短精炼,一句话能说清楚绝不用两句。直接给出核心信息,省略寒暄和冗余说明。" + # 当 skill_enabled 开启时,在每次用户输入后先做 skill 路由: # 由 SkillRouter 根据当前 query 匹配相关 skill 指南,注入到系统提示词中 if runtime.skill_enabled: @@ -451,9 +455,7 @@ def execute_agent( "turn_id": turn_id, } - if output_type_from_config == "audio" and assistant_meta.get("audio"): - assistant_content = [] - elif output_type_from_config == "both" and assistant_meta.get("audio"): + if output_type_from_config == "voice" and assistant_meta.get("audio"): assistant_content = text_content else: assistant_content = text_content if text_content else "(空回复)" diff --git a/src/agent/core/tool_config.json b/src/agent/core/tool_config.json index 92349d1..9905774 100644 --- a/src/agent/core/tool_config.json +++ b/src/agent/core/tool_config.json @@ -1,6 +1,6 @@ [ { - "name": "web_search_class", + "name": "web_search", "display_name": "WebSearch", "description": "一个网络搜索工具,可以根据用户的查询返回相关的搜索结果。仅当需要查询近期知识或已有知识无法解答需要联网查询时调用,其余任何情况严禁调用。", "permission": "direct", @@ -8,26 +8,18 @@ "enabled": true }, { - "name": "date_query_class", + "name": "date_query", "display_name": "DateQuery", "description": "一个日期查询工具,可以返回当前的日期信息。仅当需要查询当前时间时调用,其余任何情况严禁调用。", "permission": "direct", "auto_execute_rules": [], "enabled": true }, - { - "name": "screen_capture", - "display_name": "ScreenCapture", - "description": "截取当前主屏幕画面。除了返回文字描述外,也会提供原始截图给模型进行视觉分析。注意:这个工具仅用于获取当前屏幕的实时画面,其余任何情况严禁调用。", - "permission": "direct", - "auto_execute_rules": [], - "enabled": true - }, { "name": "browser_control", "display_name": "BrowserControl", "description": "统一控制浏览器:打开/关闭浏览器、刷新/关闭标签页、打开指定 URL。", - "permission": "direct", + "permission": "confirm", "auto_execute_rules": [ { "parameter": "action", @@ -84,7 +76,7 @@ { "name": "todo_list", "display_name": "TodoList", - "description": "可选的任务管理工具。当你需要拆分复杂任务为多个步骤并跟踪进度时调用。输入 steps (list[str]) 为每一步的描述,step (int) 为已完成的步骤索引(从0开始)。任务默认状态为 pending,根据 step 自动标记完成。工具返回当前 todo list 文本。仅在需要跟踪多步骤任务进度时调用,简单任务不需要调用。", + "description": "任务管理工具。用于将复杂任务拆分为多个步骤并跟踪执行进度。", "permission": "direct", "auto_execute_rules": [], "enabled": true @@ -92,7 +84,7 @@ { "name": "gui_tool", "display_name": "GUITool", - "description": "GUI automation tool. Visually locates desktop elements from a screenshot, maps coordinates to the real screen, and executes pyautogui actions. Supported actions: click, type, drag_and_drop, scroll, ho", + "description": "GUI 自动化工具。通过截图视觉定位桌面元素,将坐标映射到真实屏幕,并执行 pyautogui 动作。接收一个有序的动作列表,按顺序依次执行。每个动作项支持的操作:click(点击)、type(输入文本,可通过 element_description 指定目标输入框自动聚焦)、drag_and_drop(拖拽)、scroll(滚动)、hotkey(快捷键)、hold_and_press(按住组合键", "permission": "direct", "auto_execute_rules": [], "enabled": true @@ -100,7 +92,7 @@ { "name": "call_subagent", "display_name": "CallSubAgentTool", - "description": "调用已启用的子 Agent 执行指定任务。子 Agent 会独立运行并返回最终结果。", + "description": "调用一个已启用的子 Agent 执行指定任务。子 Agent 会独立运行并返回最终结果。适用于需要专业能力(如代码分析、文档处理等)进行辅助的场景。可用的子 Agent 名称取决于 agent_config.json 中已启用且 role='sub' 的配置。", "permission": "direct", "auto_execute_rules": [], "enabled": true @@ -108,15 +100,7 @@ { "name": "doc_tool", "display_name": "DocTool", - "description": "文档操作工具。支持纯文本(plain_text)、表格(tabular: CSV/XLSX)、Word(docx)、PDF 文档的增删改查。", - "permission": "direct", - "auto_execute_rules": [], - "enabled": true - }, - { - "name": "gui_tool(test)", - "display_name": "GUIToolTest", - "description": "GUI 自动化工具(测试版)。通过截图视觉定位桌面元素,将坐标映射到真实屏幕,并执行 pyautogui 动作。接收一个有序的动作列表,按顺序依次执行。每个动作项支持的操作:click(点击)、type(输入文本,可通过 element_description 指定目标输入框自动聚焦)、drag_and_drop(拖拽)、scroll(滚动)、hotkey(快捷键)、hold_and_press(", + "description": "文档操作工具。支持纯文本(plain_text)、表格(tabular: CSV/XLSX)、Word(docx)、PowerPoint(pptx)、PDF 文档的增删改查,以及图片(image)、音频(audio)的读取。", "permission": "direct", "auto_execute_rules": [], "enabled": true @@ -138,7 +122,7 @@ { "name": "send_file", "display_name": "SendFileTool", - "description": "向用户发送文件的工具。根据文件类型自动处理:图片前端直接展示,音频前端播放+ASR转文字,压缩包和各类文件供用户下载(超过大小限制时提示发送到邮箱)。", + "description": "向用户发送文件的工具。根据文件类型自动处理:", "permission": "direct", "auto_execute_rules": [], "enabled": true @@ -146,9 +130,23 @@ { "name": "terminal_execute", "display_name": "TerminalExecute", - "description": "终端命令执行工具。在本地终端(PowerShell)中执行 shell 命令并返回执行结果。支持执行 Python 脚本、安装依赖、运行程序、文件操作等任意终端命令。", + "description": "终端命令执行工具。在本地终端(PowerShell)中执行 shell 命令并返回执行结果。", "permission": "direct", "auto_execute_rules": [], "enabled": true + }, + { + "name": "mail_tool", + "display_name": "MailTool", + "description": "QQ email tool. Supports sending and reading email without GUI.", + "permission": "direct", + "auto_execute_rules": [ + { + "parameter": "action", + "operator": "equals", + "value": "read" + } + ], + "enabled": true } ] \ No newline at end of file diff --git a/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc b/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc index 6fbc5a41907d5935f7cb1e7c469315af826528d4..fc5c93c39e0f83e64fda1c7b29eaf4ce08478445 100644 GIT binary patch delta 2439 zcmZuyeN0nV6u;-a*VpzHQ0RL=tw$Xfb<%9tzTi*dn zQ}G4se2WUyke(o3zDUAjcA-kuol)KehEjx-eL*BMHgc;#TRRZyfjB!*XSL$=X97uh z+G%%O;`1IcN#-Ipfs`2*?h^&-y5ZZ?tL1`73 z@FgiHo7>$HP{V9MRl-6*ZB^93!47qY#~9{1RDWlf?daT&|CBriBQbMeduk=@#;vKo z-g_1)B%j_fuw$_McB=bg&V(g@D$PDv^Wl*YJ8jJvY#UxNVf9RjId{Y*3`VXJ2z&aa~_}d)2y$hNj8<<~Y^S#PPhBCdHTUn^F4WhXN9dj+ak~ z>8EQ3Y6dsp`s0<;^RypQ17J`#Q7~ocmGCh>mR=3bcq~1`p=>1FFz@eH{av!hpj42b z%2rCqlOkmcPPDyZNHaJe6=0vO3R<)?wqAfCY_~UZEkpx%Wu|I<_HO|$;)aYMQ5ehE z1aMXJWO@KL;tdWj+|c?Q0>F>>qpUSVVIu1+19!B2IsH`Osj~zgXx_ypfXBGhbq$_= z;I=~xUg%aB25`tN5tGl{^&B+fM|sl>58ADz$AD;zW!SVpzQz!NtDp^*;=Y3Sd74uj z;BEDyR#gC!ml7SQLEXQ1x8K7moAKX84yeMm6~8lx!%$qng+OuR6~$G(G9N?- zkTRf>A)Ps7&Uo7Sswu*Ww&M}i65&O(B8^4nq7$JlWDa}y9%Gz0qdK>39Wob$Ge|ZD zjXe1>ePY1pOPn@ikP8}ClL-CA&p6=deA%mIHfTJ6$Q;#0&Eu>^qs-E@qdxXIDe5Va#_EOu14vHl zG0XEPprH8-!skldjBLTx-Ync-n8K2p;T~_sJibSW10~?BVyn$XAPJ=U+0O$60{+Kq zX9ZPXFF3EnWHtul026szNtS(Xbr;ThGm%tQ!hG#+i4iPxx|MT;9Om$^D+@|JLYUbT zP#P%PM@caydf%A+UHd!zN~`KuHc)0JC8r1pn*w`xlj-kPJr>-&%H14>4t>%T^7JUo z5`jKR0oo;w*ir1%!>19_Kvrs?mUPNd^e_P#q^UI9(aNb8a*ozc8inPW$eMB2%2@~Z zlaamTehNwuHPeetWInZRtG{B&Y)YlS!Q0y7(lHi3*S@QG58!LOwrVjM;Qduk;VM?E zbBXe~>P&_#ifgsw3`}SPbtmb(XRgnHCcI|-VYsJVULS;BI^7;lSwPFCPj)-KZKqsD z(eIQ?G@Q@Tn8Vd|O>w%*qE^ev?Maf{(WY)D+dj`x)^%vB(yoW3(G-agkMLa6qNo$P zOP>hD)Dd*7-P7ft$xzRhn_09#o?U6jW}h9}vD4=wwBG0DXQ1J?eBSyPC_XOQyU`f2 z0&sdiDm)d%bK^>Z|Mg{ibs0+nlQM0l%EqV#y?OYV)O7AQR5Y8-rj7Z%`ZZSl@2MD3 z48!!s>uAXBF!^^>WOV delta 1803 zcmah}YfKzf6rM9P`vex)W!dfm<*_UBg4tj@!C(FM`l~!c3pC(S?JQkad1oL@dtM~h?wn}H8B^L zJy}kfZ{{BA=D10~UQaGI<~Z9Xc@0<;HgsMyBeMB2&yeF(m<#Frk&MJD6=h+0)l+pd z2F?`Zq_3G^ZWm^HeE4^c15bDhz=AitKCd+yAQ_&7bpfLuFTUw@w%$exx%;+4vXesi zWx(Q`LY6)N-1zDv+4jWK`eV^PrT5Udrl`1-TL(7$GPiV-plT%URJAD4)F=A2$0Tc9 z?AMeSZuUtclZ_p|766?0bqUuz+l{VGPl6>b^vC+}D}M{jV=&MPVH^k)6vxHEepQQG zl`%~jk|hgirtah*gD`^!@r}TJmMlx?w^F1Q7yFRukZs9knY7Zr zemp&gHCr^-6S5||O?LWe=rK@v;N-MPo1Cu0BB$FHxt9cdxi%zlvK?J@#Z4|sGe#iC7nDVoL(b6c6kWJfTVCjp1*St6 zb;}O=o@+?;)ZF+@U7jt44-VW?Ukq;SsP_x5gi<_S?}kj{)p{G?xgB@3lUHb<39)iy zs84dn`C*mpqXyE&pqfD;gOe2Ec6DTscI=pz*b;{_G#XP#R8zPLH^Hn!*fsE1k1=uloEmEmuoQXLhgHvY)G1YQBP} z8mf({d&0C~j4v9`2((+Cx&K=jF=EXZ1$f?Q*!47P=L@^btPvXUFkrMK2gSDUU>UEs zZ4sFfXM(v#WxJKP8Y~}T`rb#37dr=eIB(eY9v2JEc3WVgK46fj_fS+-hN85_188(j zz~@HgejOARPZCLh@k}>`%^qb|o|toHrHs)3Fg93Cu04f&r|E^6VSjinF!8d8&J8R^ zty;^%Zh=3YO+({FdeNTYAtfQk6ecFF2%yCi1*gM#px|P-oZ|U#g*8zH{u-|7T-R?+ z$@bJ*jx|ZNzLU(#Zm$0%3axW-v^(fCJp`JVda%PwY^Ddk)g4GCzSzArYHC>jr=Ktq z$K;-3Gpi|_&L-#$N>=OJ_>1Z3j4$)?O97W-4lXd<8;py>#5R%a#`zaV1E9wM diff --git a/src/agent/history/data_base/chat_history.db b/src/agent/history/data_base/chat_history.db index 1f3e08f2bd250b0cd71929c5d9984657a292d98f..528df9066db66ca5be7a4590b0a2986faade2410 100644 GIT binary patch literal 45056 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 O*w}cFWAlp)33dQ|i6Auq diff --git a/src/agent/history/multi_inputs/20260708_155304/ui_messages.json b/src/agent/history/multi_inputs/20260708_155304/ui_messages.json deleted file mode 100644 index 91260cb..0000000 --- a/src/agent/history/multi_inputs/20260708_155304/ui_messages.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "version": 3, - "messages": [] -} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260708_160449/ui_messages.json b/src/agent/history/multi_inputs/20260708_160449/ui_messages.json deleted file mode 100644 index 91260cb..0000000 --- a/src/agent/history/multi_inputs/20260708_160449/ui_messages.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "version": 3, - "messages": [] -} \ No newline at end of file diff --git a/src/agent/history/tool_call_recorder.py b/src/agent/history/tool_call_recorder.py index 5886421..ebaa04d 100644 --- a/src/agent/history/tool_call_recorder.py +++ b/src/agent/history/tool_call_recorder.py @@ -127,6 +127,13 @@ def _artifact_image_data_urls(artifact: Any | None) -> list[str]: return if isinstance(item.get("image_bytes"), bytes): image_bytes_list.append(item["image_bytes"]) + # 支持 doc_tool 的 "images" 键(多图片列表) + if isinstance(item.get("images"), list): + for img in item["images"]: + if isinstance(img, bytes): + image_bytes_list.append(img) + elif isinstance(img, dict) and isinstance(img.get("image_bytes"), bytes): + image_bytes_list.append(img["image_bytes"]) for key in ("image_url", "url", "data_url"): value = item.get(key) if isinstance(value, str) and (value.startswith("data:image/") or value.startswith("http")): @@ -342,6 +349,16 @@ def _save_artifact_files(artifact: Any, base_name: str) -> tuple[list[str], dict fname = f"_artifact_{base_name}_0.png" files.append(fname) bytes_map[fname] = artifact["image_bytes"] + if "images" in artifact and isinstance(artifact["images"], list): + for i, img in enumerate(artifact["images"]): + if isinstance(img, bytes): + fname = f"_artifact_{base_name}_{i}.png" + files.append(fname) + bytes_map[fname] = img + elif isinstance(img, dict) and "image_bytes" in img: + fname = f"_artifact_{base_name}_{i}.png" + files.append(fname) + bytes_map[fname] = img["image_bytes"] if artifact.get("file_type") == "file" and "file_bytes" in artifact: file_name = artifact.get("file_name", "file") ext = _os.path.splitext(file_name)[1] or ".bin" diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc index f63d2c8ab34feb0546189c600dbea031c01256c3..92504c114bf024f0f0a89828339b74bb63fa4d91 100644 GIT binary patch delta 1189 zcmZuw?@tqF9DlBNrMGK~4k&On>IvJjPNn{evBqeK8h&v%yp4!i^jL^6(Je!!r}}1+_l+@lCVEu&%<#EnR}9c`+T40^ZkB5 zpU>UhZtHC?{oUzg09wyKG&FtfdtU0Gi8*Bh%~cmhUh45lEr}zXxzBysaORtnM-QJ> z8t7xSjo*jDVSzKR8c&@#eY(+q6Y&ZAkQ6 zrU(Fr!6zU<1T10DOhiDlB|;E5C&Oej6|oRNw3}rX&|wiBW;Tv$@oHK(xWm1-X}yuH zjRA^9X~23FbuiHpQYkGz!r2 zbL`G});G)g(pzTP1L-TX?6Eo5ahX{ta}Rq5z3;1Q|8#Gib8mlNwPnc$%4?PZRnEw^ ze@c}<9NS&k-0u2K^&arnW3N9%zCB3PA4<8LZ#H)>QUEZE(mA z=STYWNlE`{0j=|LEvnX|I$t=vqkgQz# zHqyuJtm9TSB59p#w3(`8#2DThfz!Xj$$m{9(BvU)IEgqK0AnB$tWVrq9lK(Io_T=O zY6(ft$c7#NF~Z=E7%{s{3Wa_DGyfsdjIA~mBH!}T^<4TEI?*P^^o)WYkTUp1<)q}E%7=O>^+T1OtnU|=%5z5d>7buB92rL2v4Z=@iX`z+Y*<=!l$NNd0o4n?2 z^9IgD+r=hsI+q0XsgHdz38XylIj2t=_6PKw9h0IL-uFEB-|u;z(`)ZdZBm@VkOE&wm70>&qK64P=zvd7pQE-w$9!}KdNthSY2CBZyO}V`n2YLU0g*4YCNdV!8>|$hLTo6by^~} zt+rdM>l+_5S(Tt+i{aTN6A0;Wt#{yI(0;O7mAa$3@ijSf)G}n*O3(e0Y5ts9xUWJf zIR{9YqGi>;^jKjmOg;_?9eEZM(3Pr`?-Qs}Rp`2L^K4rxz-f0Gmlz0>ZkBi!xY&#^ z(M8--WX{1wd?Xkn&Vbk%6a0)Y?IWI`_zsia^}{fygJ&l9HU@;QRpJ^XJq|J0DGV>b z3P^XDXGetSI0W+^Mu@U<&0S_l3&}q`j5yy;+PYaO{9h2j=OPSodd0vbFycpgiEk9R z0F=mda*;mp@;)zVpWxPexG2M|v3ncST%=#Ni-EZ7k6orFZo0+U0l^(1>%&riae`tn zL1K%)6{Xcc^K1`4<&w*bF+czEwUiOX{FG;JV?wrbkwv+~BV@S};LDNxSb%3;q;Fc> z90QrmEsHB&@M_Ov&a#SkEh*SnY)ljbEBx3X%mGJ>&21NpvJP3w(+jNq%~B?E)E-P5 z2218UfEO2ylD=6IeoOpdbP;x3aIf%#UU}5e8qbFF`OpI2JO`Ae3SJJ4AB*~z;`lPA0`ElJfItNq=u5}A9l$<`H c(%+*P&3=-J%9&9fAo%Q`!&@aw3E2?y8-1D2asU7T diff --git a/src/agent/memory/chroma_rag_db/93b83aa9-b3da-41a4-b34c-90fcd21bda19/data_level0.bin b/src/agent/memory/chroma_rag_db/93b83aa9-b3da-41a4-b34c-90fcd21bda19/data_level0.bin index 5e59e2856ecdd271e581eef019635383272294f2..7292420e78f9ba2a85fe0b7c23120fc6430cdde8 100644 GIT binary patch delta 110 zcmdn+R&v8zNxlHa^u}671_pxi_@% diff --git a/src/agent/memory/chroma_rag_db/93b83aa9-b3da-41a4-b34c-90fcd21bda19/length.bin b/src/agent/memory/chroma_rag_db/93b83aa9-b3da-41a4-b34c-90fcd21bda19/length.bin index 804579b7f13f24fe762254cc91072cd05daadd4f..a8a9ff6542e6c2dd2ff6c9b447ca6517fc045620 100644 GIT binary patch literal 400 zcmd;NfPjQ^J6F~+GB9vL`OFZyK?=%0P_+as590?cII3g@luKIm+2wyd0|OICT^5HU zR5_4(@c;k+_l_J;0f%|3!TMo*sBi<6WlJe_XuNFXSnLWY<@o&w1@Fd!uOF=X%E zRD1$%fN{9R-hG_2XAcbPI2nVm0KK?!s~{FAfkBHIHZwF(tfe6Gc9DOSaPb66mrAAx zG=&Xu<*I?KVO!#66Kx4~4-Chj=2=E(bpyRVsFkIVj6khMOiX#8)M(7JKRD_{xYeg+ z)FowJ8pA-Rh!Y#k8Mg!?fTn=$`rZrAcDXK5Cwde;Jf#2p@%|=pO9?AGOdG;ydOP2p zUXhvAV9yRn?}XVmKz9c{HM|Ku`(NFkHqq&z+=E4q2smo^^Ev#cSX~5n7Rr2^flv*; z-dH&kmf1F1Dw*Lu5#$iC6D}86+Jsq)kReQYg?w&nd$T7QiJl$DPEHT56N&ic!&Q8J JaTi+gQ^1hq@$!WlhYkf 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 delta 4627 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 diff --git a/src/agent/memory/system_prompt.py b/src/agent/memory/system_prompt.py index fdccc92..484eb26 100644 --- a/src/agent/memory/system_prompt.py +++ b/src/agent/memory/system_prompt.py @@ -14,12 +14,13 @@ _BASE_PROMPT = """你是一个具备多模态理解能力的桌面操作助手 _WS_TAIL = """\n\n【工作空间配置】 -- 工作目录 (WORKING_DIR): {working_dir} +- 项目根目录 (PROJECT_DIR): {working_dir} - 工作空间 (WORKSPACE_DIR): {workspace_dir} +- Skills 目录 (SKILLS_DIR): {skills_dir} - 你产生的代码、脚本、图片、文档等文件,默认保存到工作空间目录下。 - 使用 terminal_execute 工具时,如果不指定 working_dir,默认在此工作空间执行命令。 -- 使用 doc_tool 创建/修改文件时,文件路径请使用工作空间的绝对路径。""" - +- 使用 doc_tool 创建/修改文件时,文件路径请使用工作空间的绝对路径。 +""" def get_main_system_prompt() -> str: """动态构建主系统提示词(含工作空间路径)。 @@ -32,9 +33,12 @@ def get_main_system_prompt() -> str: """ try: from agent.utils.env_utils import WORKING_DIR, get_workspace_dir + from pathlib import Path + skills_dir = str(Path(__file__).resolve().parents[1] / "skills") ws_info = _WS_TAIL.format( working_dir=WORKING_DIR, workspace_dir=get_workspace_dir(), + skills_dir=skills_dir, ) except Exception: ws_info = "" @@ -143,12 +147,12 @@ PLAN_FIRST_CALL_PROMPT = """ """.strip() REFLECTION_PROMPT = """ -【请先在你的回复中尽可能简短地输出你的反思,然后再调用工具或给出最终回复】 -你需要先用尽可能简短的一句话在文字中思考上一步做了什么?下一步该做什么?然后进行相应的工具调用。格式如下: -1. 先分析上一轮执行结果:上一个调用了什么工具?结果是成功还是失败? -2. 如果上一轮是 GUI 操作(点击、输入等)但未命中目标或未达预期效果,请根据最新的截图重新估算目标位置,调整坐标或参数后重试。 -3. 如果上一轮操作成功,说明你根据分析结果进行下一步操作。 -4. 然后调用对应的工具执行下一步,或如果任务已完成则直接回复用户(注意只需要回复用户最终结果即可)。 +【重要:如果你判断不需要调用任何工具,请直接回复用户,不要输出任何反思内容!】 +只有在需要调用工具时,才先进行简短反思(一句话即可),然后调用工具。格式如下: +1. 如果不需要调用工具(任务已完成或无需工具即可回答),直接给出最终回复,不要输出反思。 +2. 如果需要调用工具:先用一句话思考上一步结果和下一步计划,然后调用工具。 +3. 如果上一轮是 GUI 操作(点击、输入等)但未命中目标或未达预期效果,请根据最新的截图重新估算目标位置,调整坐标或参数后重试。 +4. 如果上一轮操作成功,根据分析结果进行下一步操作。 【gui_tool 批量调用规则 - 务必遵守】 如果你计划在同一页面/界面执行多个连续的 GUI 操作(例如:依次点击多个按钮、填写多个表单字段、连续输入多项内容),必须将所有这些操作合并为一次 gui_tool 调用,在 actions 列表中一次性传入所有动作。严禁将同一页面的连续操作拆分多次 gui_tool 调用——这样会导致严重的延迟。仅当需要等页面跳转/加载后才能确定后续操作时,才分步调用。 diff --git a/src/agent/skills/ppt_maker/attachments/01-workflow-overview.md b/src/agent/skills/ppt_maker/attachments/01-workflow-overview.md new file mode 100644 index 0000000..0db7c7a --- /dev/null +++ b/src/agent/skills/ppt_maker/attachments/01-workflow-overview.md @@ -0,0 +1,123 @@ +# 01 — 总调度:SVG 幻灯片生成工作流总览 + +> **角色**:主调度器(Master Orchestrator) +> **读取时机**:任何幻灯片生成任务启动时,**最先读取此文件** +> **职责**:定义完整的 4 步串行流水线、全局执行纪律、入口路由规则 + +--- + +## 核心流水线 + +``` +源内容 → Strategist 设计规划 ⛔ → Executor 逐页生成 SVG → 质检 → 交付 SVG 文件 +``` + +4 个步骤**严格串行**,每步的输出是下一步的输入。 + +**本 Skill 的终点是生成 SVG 文件**。SVG → PPTX 转换通过独立脚本 `10-svg-to-pptx.md` 完成,仅在用户明确要求 PPT 格式时运行。 + +--- + +## 全局执行纪律 + +| # | 规则 | 说明 | +|---|------|------| +| 1 | **串行执行** | 步骤必须按顺序,不可跳跃 | +| 2 | **BLOCKING = 硬停止** | 标记为 ⛔ BLOCKING 的步骤必须等待用户显式确认 | +| 3 | **禁止跨阶段捆绑** | Strategist 确认阶段是唯一 BLOCKING 点,确认后后续步骤自动顺序执行 | +| 4 | **入口前验证 (GATE)** | 每步进入前验证前置条件 | +| 5 | **禁止推测执行** | 不要"提前准备"后续步骤内容 | +| 6 | **禁止子代理生成 SVG** | SVG 必须由当前主 Agent 端到端完成 | +| 7 | **仅支持顺序页面生成** | SVG 页面必须逐页生成,禁止批处理 | +| 8 | **每页重读 spec_lock** | 生成每个 SVG 前必须重新读取锁定设计规范 | +| 9 | **SVG 必须手写** | 禁止脚本批量生成 | +| 10 | **遵循确定性路由** | 不要额外添加阻塞性路由问题 | + +--- + +## 五步详解 + +### Step 1 — 源内容处理 + +- 有源文件或用户提供的文本内容 → 理解并整理为结构化内容大纲 +- 仅有主题、无详细材料 → 基于主题研究,整理为 Markdown 大纲 + +**GATE**:存在可用的文本内容源 + +### Step 2 — Strategist 策略师阶段 ⛔ BLOCKING + +> **进入前必须先读取 `02-strategist.md`** + +三阶段交互确认: + +| 阶段 | 确认内容 | 输出 | +|------|----------|------| +| Stage 1 — 方向锚定 | 画布格式、受众/场合/核心信息/交付目的、模式+视觉风格 | 方向确认 | +| Stage 2 — 设计系统 | 页数、配色方案、图标策略、字体方案 | 设计确认 | +| Stage 3 — 图片/内容 | 用户提供的图片清单(哪些页需要图片、图片尺寸与用途) | 图片清单 | + +最终输出:`design_spec.md`(人工可读)+ `spec_lock.md`(机器可读执行合约) + +**⛔ 必须等待用户在每个阶段确认后才进入下一阶段。** + +### Step 3 — Executor 执行师阶段 + +> **进入前必须先读取 `05-executor.md` + `06-svg-standards.md` + `08-layout-patterns.md`** +> **如有用户图片,还需读取 `07-image-embedding.md`** + +核心流程: +1. 逐页生成 SVG(**每页前重读 `spec_lock.md`**) +2. 按 `page_rhythm`(anchor / dense / breathing)控制节奏 +3. 将用户提供的图片以 Base64 嵌入对应页面 +4. 生成演讲备注 +5. 运行 SVG 质量检查 + +**硬约束**:禁止子代理生成 SVG;必须逐页顺序;每页重读 spec_lock。 + +### Step 4 — 质检与交付 SVG + +> **进入前必须先读取 `09-quality-and-export.md`** + +1. 逐页 SVG 质量检查(对照 `spec_lock.md` 一致性验证) +2. 视觉自检(软规则) +3. 使用 `doc_tool create` 将每页 SVG 写入文件: + - 格式:`doc_tool create <文件名>.svg content="..."` + - 每页一个独立 SVG 文件 +4. 生成演讲备注(如有需要) + +**doc_tool 在本 Skill 中仅用于写入 SVG 文件,不生成 PPTX。** + +--- + +## SVG → PPTX 转换(可选,独立步骤) + +> 仅在用户**明确表示**"不要 SVG,要 PPTX 格式"或"导出为 PPT"时触发。 +> 进入前必须先读取 `10-svg-to-pptx.md` + +独立脚本:将已生成的 SVG 文件批量转换为原生 PPTX。 + +--- + +## 入口路由 + +根据用户意图选择执行路径: + +| 用户意图 | 执行路径 | +|----------|----------| +| 从零生成幻灯片 | 完整 4 步流水线 → 交付 SVG | +| 只有主题无材料 | Step 1 走主题研究 → 完整后续 | +| 仅生成设计规范 | Step 1-2,止于 design_spec | +| 从 spec 继续生成 | 跳过 Step 1-2,直接 Step 3-4 | +| 已有 SVG 要导出 PPTX | 读取 `10-svg-to-pptx.md`,运行转换脚本 | + +--- + +## 角色切换协议 + +| 切换到 | 必须先读取 | +|--------|-----------| +| Strategist | `02-strategist.md` | +| Executor | `05-executor.md` + `06-svg-standards.md` | +| 图片嵌入参考 | `07-image-embedding.md` | +| 质检与 SVG 交付 | `09-quality-and-export.md` | +| SVG→PPTX 转换 | `10-svg-to-pptx.md` | diff --git a/src/agent/skills/ppt_maker/attachments/02-strategist.md b/src/agent/skills/ppt_maker/attachments/02-strategist.md new file mode 100644 index 0000000..8aacdc0 --- /dev/null +++ b/src/agent/skills/ppt_maker/attachments/02-strategist.md @@ -0,0 +1,177 @@ +# 02 — Strategist(策略师):设计规划与三阶段确认 + +> **角色**:策略师 — 内容分析、设计规划、用户确认 +> **读取时机**:Step 4 进入策略阶段前 +> **产出**:`design_spec.md` + `spec_lock.md` + +--- + +## 核心使命 + +接收源内容 → 分析 → 规划 → 三阶段交互确认 → 输出设计规范与内容大纲。 + +--- + +## 三阶段确认流程 + +### Stage 1 — 方向锚定 + +#### a) 画布格式 + +| ID | 格式 | viewBox | 宽高比 | 适用场景 | +|----|------|---------|--------|----------| +| `ppt169` | PPT 16:9 | `0 0 1280 720` | 16:9 | **默认**,演示/会议 | +| `ppt43` | PPT 4:3 | `0 0 1024 768` | 4:3 | 传统投影 | +| `story` | 竖屏 Story | `0 0 1080 1920` | 9:16 | 抖音/TikTok | +| `banner` | 横幅 | `0 0 1920 1080` | 16:9 | 公众号头图 | + +#### b) 关键信息 + +向用户确认: +- **受众**:高管 / 技术团队 / 客户 / 学生 / 公众 +- **场合**:正式汇报 / 路演 / 培训 / 发布会 / 内部会议 +- **核心信息**:一句话总结 PPT 要传递的核心观点 +- **交付目的**:`text`(阅读型,正文20px)/ `balanced`(商务型,默认24px)/ `presentation`(演讲型32px) +- **内容发散度**:`focused`(1个主题) / `moderate`(2-3个子主题) / `broad`(多主题概览) + +#### c) 模式(Mode) + +> **详细定义见 `03-modes-and-styles.md`** + +5 种沟通模式: +- `pyramid` — 结论先行,MECE 论证(决策/分析/策略) +- `narrative` — 故事弧线:情境→张力→解决(路演/案例/品牌) +- `instructional` — 概念分解,逐步递进(培训/教程) +- `showcase` — 视觉主导,情感节奏(发布会/品牌揭示) +- `briefing` — 中立完整可扫描(状态更新/参考/目录) + +#### d) 视觉风格(Visual Style) + +> **详细定义见 `03-modes-and-styles.md`** + +17 种视觉风格供选择,分类为: + +| 类目 | 风格 | +|------|------| +| 企业/产品 | `swiss-minimal` `soft-rounded` `glassmorphism` `dark-tech` `blueprint` | +| 编辑/出版 | `editorial` `photo-editorial` `data-journalism` `brutalist` | +| 表现力/印刷 | `memphis` `zine` `vintage-poster` `paper-cut` | +| 手绘/画笔 | `sketch-notes` `ink-notes` `chalkboard` `ink-wash` | +| 特殊 | `pixel-art` | + +--- + +### Stage 2 — 设计系统 + +#### e) 页数 + +根据内容量 + 交付目的给出建议页数范围,由用户确认。 +- 阅读型 / 商务型参考每页信息密度 +- 不超过 2 页连续的 dense 类型 + +#### f) 配色方案 + +> **详细规范见 `04-design-system.md`** + +提供 **3 个候选方案**,每个包含: +- 背景色 / 主色 / 辅色 / 强调色 / 文字色 / 弱化色 +- 适用场景一句话 +- 候选方案必须差异化(如:科技蓝 vs 温暖橙 vs 沉静墨绿) + +**60-30-10 原则**:主色占视觉面积 60%,辅色 30%,强调色 10%。 + +#### g) 图标策略 + +三选一:Emoji / AI 生成 / 内置图标库 +推荐策略:内容页用图标库 + 装饰页按需 AI 生成 + +#### h) 字体方案 + +字体配对示例: + +| 场景 | 标题字体 | 正文字体 | +|------|---------|---------| +| 中文商务 | 微软雅黑 Bold | 微软雅黑 | +| 中文正式 | 思源黑体 Bold | 宋体 | +| 英文现代 | Calibri Bold | Calibri | +| 中英混排 | 微软雅黑 Bold | Arial | + +**字号层级(基于 1280x720)**: +- Title: 42-48px | Subtitle: 24-28px | Body: 16-18px | Annotation: 12-14px +- 整份 PPT 中同一角色字号锁定不变 + +--- + +### Stage 3 — 图片/内容确认 + +#### h) 图片使用(用户提供) + +图片全部由用户提供。在此阶段需要确认: + +- **哪些页面需要图片**:逐页确认(封面、内容页等) +- **每张图片的用途**:背景 / 插图 / 图表 / Logo / 头像 +- **图片尺寸建议**:根据页面布局给出推荐尺寸(如封面背景 1280×720、卡片配图 540×300 等) +- **图片格式**:jpg / png / webp(推荐 png 以保证透明通道可用) + +#### 无图片时的处理 + +若用户无需图片,则全部采用纯文字排版 + 装饰图形(色块、线条、图标)。 + +--- + +## 输出规范 + +### design_spec.md(人工可读) + +必须包含 11 个章节: +1. 项目信息 +2. 画布格式 +3. 关键信息(受众/场合/核心信息/交付目的) +4. 页数与页码分配 +5. 模式与视觉风格 +6. 配色方案(HEX 色板) +7. 字体方案 +8. 图标策略 +9. 图片资源清单 +10. 内容大纲(每页:页面类型 + 标题 + 核心消息 + 关键要点) +11. 技术约束 + +### spec_lock.md(机器可读) + +精简的 YAML 格式锁定数据,Contains only machine-consumable values: + +```yaml +canvas: ppt169 +palette: + bg: '#FAFBFC' + primary: '#1A73E8' + accent: '#EA4335' + text: '#1F2937' + muted: '#6B7280' +typography: + title_size: 42 + subtitle_size: 24 + body_size: 16 + annotation_size: 12 + font_family: 'Microsoft YaHei, sans-serif' +mode: pyramid +visual_style: swiss-minimal +page_rhythm: + - { page: 1, rhythm: anchor } + - { page: 2, rhythm: dense } + ... +pages: + - { index: 1, title: '封面', core_message: '...', rhythm: anchor } + - { index: 2, title: '背景', core_message: '...', rhythm: dense } + ... +``` + +--- + +## 确认 UI 规则 + +1. **每阶段一次呈现**:将该阶段所有待确认项打包为一个结构化选项清单 +2. **必须提供候选**:配色 ≥ 3 个候选、字体 ≥ 3 个候选 +3. **默认推荐**:每个选项标注推荐值(`(Recommended)`),基于受众和场合智能推荐 +4. **允许用户覆盖**:每个选项都可以被用户自定义值覆盖 +5. **禁止跳过**:每个项都必须显式确认或接受默认值 diff --git a/src/agent/skills/ppt_maker/attachments/03-modes-and-styles.md b/src/agent/skills/ppt_maker/attachments/03-modes-and-styles.md new file mode 100644 index 0000000..35e5fc7 --- /dev/null +++ b/src/agent/skills/ppt_maker/attachments/03-modes-and-styles.md @@ -0,0 +1,141 @@ +# 03 — 模式与视觉风格索引 + +> **角色**:设计参考速查 — 提供 5 种沟通模式 + 17 种视觉风格的快速决策表 +> **读取时机**:Strategist 阶段需要锁定 mode / visual_style 时 +> **关系**:mode = 如何论证(叙事骨架);visual_style = 长什么样(视觉外观)。二者相互独立,同时锁定。 + +--- + +## 一、沟通模式(Mode) + +模式定义 PPT 的**叙事 + 说服骨架**——论点如何被组织和推进。 + +### 1. `pyramid` — 金字塔模式 + +**结论先行论证**。先给答案,再用互斥且穷尽 (MECE) 的证据支撑。 + +| 特征 | 说明 | +|------|------| +| 开场 | SCQA 结构(情境→冲突→问题→答案) | +| 标题风格 | 断言式(每页标题即核心结论) | +| 数据规则 | 数据永不孤立,每个图表必须带比较维度 | +| 页面结构 | 严格逻辑层级,逐层展开 | +| 适用 | 决策支持、分析报告、策略、董事会/高管汇报 | + +### 2. `narrative` — 叙事模式 + +**故事弧线说服**。情境→冲突→解决,用悬念、转折和人性化框架让观点在逻辑之前先触动情感。 + +| 特征 | 说明 | +|------|------| +| 弧线结构 | 至少一个转折;标题读起来像推进弧线的节拍 | +| 情感策略 | 人性化框架、案例聚焦、代入感 | +| 视觉配合 | 图胜于表,情绪图像优先于数据图表 | +| 适用 | 路演、案例研究、品牌叙事、融资 | + +### 3. `instructional` — 教学模式 + +**教学引导式阐述**。将概念分解为有序、可消化的部分,逐步建立理解。 + +| 特征 | 说明 | +|------|------| +| 组织逻辑 | 每页一个概念、先展示后讲述 | +| 路标系统 | 设置进度路标(如 "第3/7步") | +| 语言风格 | 解释性、引导性,避免专业黑话 | +| 重复强化 | 关键概念跨页重复强调 | +| 适用 | 培训、教程、知识分享 | + +### 4. `showcase` — 展示模式 + +**视觉主导冲击**。让图像、比例和节奏承载信息——最小化文案、最大化存在感。 + +| 特征 | 说明 | +|------|------| +| 视觉策略 | 每页一个主导视觉元素 | +| 情绪结构 | 按情感节奏推进(预告→揭示→高潮→回味) | +| 文案策略 | 极少文字,每页不超过 30 字 | +| 转场节奏 | 快节奏,每页停留不超过 30 秒 | +| 适用 | 发布会、品牌揭示、活动/推广 | + +### 5. `briefing` — 简报模式 + +**中立信息传递**。平实、完整地列出事实,按可扫描和查找的方式组织——无论点、无故事、无课程。 + +| 特征 | 说明 | +|------|------| +| 标题风格 | 主题标签而非断言 | +| core_message | 描述覆盖范围而非主张 | +| 组织原则 | 按主题/时间/部门分类,便于按需查阅 | +| 适用 | 状态更新、参考资料、目录、会议材料 | + +### 模式自动选择参考 + +| 场景信号 | 推荐模式 | +|----------|----------| +| 需要决策 > 数据分析 > 数据含对比 | `pyramid` | +| 有故事/案例 > 需情感共鸣 > 品牌/融资 | `narrative` | +| 教学目标 > 步骤清晰 > 知识传递 | `instructional` | +| 视觉 > 文字 > 冲击力优先 | `showcase` | +| 信息罗列 > 无论点 > 后续查阅 | `briefing` | + +--- + +## 二、视觉风格(Visual Style) + +视觉风格定义 PPT 的**美学外观**——形状语言、装饰密度、留白节奏、字体个性、纹理/层级。整份 PPT 只锁定一种风格。 + +### 风格速查表 + +#### 企业 / 产品类 + +| 风格 | 关键词 | 适合场景 | 插画倾向 | +|------|--------|----------|----------| +| `swiss-minimal` | 瑞士网格、极简、大量留白、零装饰 | 科技、咨询、金融 | sparse | +| `soft-rounded` | 圆角卡片 `rx:12-16`、柔和阴影、友好 | SaaS、产品、培训 | supportive | +| `glassmorphism` | 毛玻璃、半透明层叠、渐变光流、暗底 | 科技/设计感发布 | sparse | +| `dark-tech` | 暗色画布、发光强调、几何精密 | AI/科技产品发布 | sparse | +| `blueprint` | 工程绘图纸、等轴测、数据标注语言 | 架构讲解、技术简报 | supportive | + +#### 编辑 / 出版类 + +| 风格 | 关键词 | 适合场景 | 插画倾向 | +|------|--------|----------|----------| +| `editorial` | 杂志层级、栏位、衬线/无衬线交替 | 行业报告、白皮书 | supportive | +| `photo-editorial` | 大图主导、满版摄影 | 品牌画册、摄影集 | sparse | +| `data-journalism` | 多媒体信息图、行内微图表 | 数据报告、新闻 | sparse | +| `brutalist` | 小字号满版、粗框线、原始结构 | 独立出版物 | supportive | + +#### 表现力 / 印刷类 + +| 风格 | 关键词 | 适合场景 | 插画倾向 | +|------|--------|----------|----------| +| `memphis` | 碰撞色块、几何碎屑、粗黑描边、80s | 创意/音乐/潮流 | core | +| `zine` | Riso印刷、颜色层错位、网点纹理 | 独立杂志/艺术 | core | +| `vintage-poster` | 中世纪印刷、平涂色块、网点/纸纹 | 文化活动/复古 | core | +| `paper-cut` | 剪纸层次、剪刀裁剪形状、层叠投影 | 传统文化/手工艺 | core | + +#### 手绘 / 画笔类 + +| 风格 | 关键词 | 适合场景 | 插画倾向 | +|------|--------|----------|----------| +| `sketch-notes` | 暖纸底、黑色墨线涂鸦、柔和色块 | 培训/笔记/复盘 | core | +| `ink-notes` | 白板墨水极简、黑白为主、少量语义色 | 白板/头脑风暴 | supportive | +| `chalkboard` | 深色石板、粉笔线条、粉彩点缀 | 教学/怀旧 | core | +| `ink-wash` | 宣纸底、文人留白、单色印章、水墨 | 文化/艺术/中式 | supportive | + +#### 特殊 + +| 风格 | 关键词 | 适合场景 | 插画倾向 | +|------|--------|----------|----------| +| `pixel-art` | 8-bit 复古、严格像素网格、有限调色板 | 游戏/怀旧/趣味 | core | + +### 风格自动选择参考 + +| 场景信号 | 推荐风格 | +|----------|----------| +| 科技 > 数据 > 分析 | `swiss-minimal` 或 `dark-tech` | +| 产品 > 用户 > 易用性 | `soft-rounded` | +| 文化 > 艺术 > 手工 | `zine` 或 `memphis` 或 `ink-wash` | +| 教育 > 培训 > 亲和力 | `sketch-notes` | +| 建筑 > 工程 > 精确 | `blueprint` 或 `editorial` | +| 金融 > 正式 > 信任 | `swiss-minimal` 或 `editorial` | diff --git a/src/agent/skills/ppt_maker/attachments/04-design-system.md b/src/agent/skills/ppt_maker/attachments/04-design-system.md new file mode 100644 index 0000000..49102f1 --- /dev/null +++ b/src/agent/skills/ppt_maker/attachments/04-design-system.md @@ -0,0 +1,131 @@ +# 04 — 设计系统:配色 / 字体 / 画布 / 图标规范 + +> **角色**:设计参考手册 — 规范所有视觉参数的取值空间与决策依据 +> **读取时机**:Strategist 阶段 Stage 2 定调设计时,Executor 阶段生成 SVG 前 + +--- + +## 一、配色方案 + +### 60-30-10 规则 + +| 角色 | 占比 | 用途 | +|------|------|------| +| 主色 (Primary) | 60% | 背景大面积区域、卡片填充 | +| 辅色 (Secondary) | 30% | 标题栏、装饰元素、图标底色 | +| 强调色 (Accent) | 10% | 关键数据、CTA、高亮文本 | + +### 色板结构 + +每套配色需提供 6 个色值: + +| 色值 | 角色 | 说明 | +|------|------|------| +| `bg` | 背景色 | 页面底色,浅色方案建议 #FFFFFF ~ #F5F7FA | +| `primary` | 主色 | 标题栏、主按钮、强调区域 | +| `secondary` | 辅色 | 次要区域、图表副系列 | +| `accent` | 强调色 | 高亮标记、关键数字、CTA | +| `text` | 正文色 | 深色背景用浅色文字,浅色背景用深色文字 | +| `muted` | 弱化色 | 副文本、脚注、辅助线 | + +### 行业色参考(仅作启发,非硬性绑定) + +| 行业 | 倾向色调 | +|------|----------| +| 科技/AI | 蓝 #1A73E8、深蓝 #0D47A1、青 #00BCD4 | +| 金融/商务 | 深蓝 #1B3A5C、金 #C8A951、深灰 | +| 医疗/健康 | 绿 #2E7D32、蓝绿 #009688、白 | +| 教育/培训 | 橙 #F57C00、蓝 #1976D2、黄 | +| 环保/能源 | 绿 #4CAF50、大地色 | +| 政府/政务 | 红 #C62828、深蓝、金 | + +### 候选方案要求 + +提供 ≥ 3 个候选方案,必须差异化: +- 至少跨越 2 个不同色相(如:冷蓝 vs 暖橙 vs 中性灰绿) +- 每个候选附带一句话适用场景描述 + +### 中性色层级 + +| 层级 | 日间模式 | 用途 | +|------|----------|------| +| 背景 | `#FFFFFF` ~ `#FAFBFC` | 页面背景 | +| 表面 | `#F3F4F6` ~ `#E5E7EB` | 卡片、面板 | +| 分割线 | `#D1D5DB` | 分隔线 | +| 禁用文本 | `#9CA3AF` | 占位符 | +| 弱化文本 | `#6B7280` | 副文本 | +| 正文 | `#1F2937` | 正文 | +| 加粗 | `#111827` | 标题 | + +--- + +## 二、字体方案 + +### 系统预装字体(无需嵌入) + +| 分类 | 中文字体 | 英文字体 | +|------|----------|----------| +| 黑体/无衬线 | 微软雅黑、黑体 | Arial、Calibri、Segoe UI | +| 宋体/衬线 | 宋体、仿宋 | Times New Roman、Georgia | +| 等宽 | 微软雅黑 | Consolas、Courier New | + +### 推荐字体配对 + +| 场景 | 标题 | 正文 | 数据/代码 | +|------|------|------|-----------| +| 中文商务 | 微软雅黑 Bold | 微软雅黑 | Consolas | +| 中文正式 | 黑体 | 宋体 | — | +| 英文现代 | Calibri Bold | Calibri | Consolas | +| 中英混排 | 微软雅黑 Bold | Arial/Calibri | Consolas | +| 科技/极简 | Segoe UI Bold | Segoe UI | Consolas | + +### 字号层级(16:9 / 1280x720 基准) + +| 角色 | 字号 | 说明 | +|------|------|------| +| **Hero Title** | 56-64px | 封面专用超大标题 | +| **Title** | 42-48px | 封面/章节标题 | +| **Section Title** | 32-36px | 内容页标题 | +| **Subtitle** | 24-28px | 副标题 | +| **Body** | 16-18px | 正文 | +| **Caption** | 14px | 图表标注 | +| **Annotation** | 12px | 脚注/来源 | + +**硬规则**:整份 PPT 中同一角色的字号锁定不变。 + +--- + +## 三、画布格式 + +| ID | viewBox | 宽高比 | 用途 | +|----|---------|--------|------| +| `ppt169` | `0 0 1280 720` | 16:9 | **默认** — 演示/会议 | +| `ppt43` | `0 0 1024 768` | 4:3 | 传统投影 | + +**硬规则**:viewBox 是唯一尺寸来源,SVG 上不写 `width`/`height` 属性。 + +--- + +## 四、图标策略 + +### 三选一 + +| 选项 | 优点 | 缺点 | 建议 | +|------|------|------|------| +| **Emoji** | 零成本、跨平台一致 | 风格局限、无法自定义 | 小项目/快速原型 | +| **AI 生成** | 完全定制 | 质量不稳定、需反复调整 | 核心装饰元素 | +| **内置图标库** | 质量稳定、SVG 原生 | 选择有限 | 内容页常规图标 | + +**推荐组合**:内容页用内置图标库,关键装饰页按需 AI 生成。 + +--- + +## 五、页面节奏(Page Rhythm) + +| 标签 | 策略 | 单页信息量 | +|------|------|-----------| +| `anchor` | 封面/章节/目录/结尾 — 高冲击力、标题主导、大量留白 | ≤ 30 字 | +| `dense` | 数据/分析页 — 卡片网格、图表、KPI、多栏 | ≤ 80 字 | +| `breathing` | 过渡/强调页 — 单句金句、关键结论,不堆砌内容 | ≤ 20 字 | + +**原则**:不要连续两页 dense,用 breathing 做节奏换气。 diff --git a/src/agent/skills/ppt_maker/attachments/05-executor.md b/src/agent/skills/ppt_maker/attachments/05-executor.md new file mode 100644 index 0000000..00a1a6a --- /dev/null +++ b/src/agent/skills/ppt_maker/attachments/05-executor.md @@ -0,0 +1,176 @@ +# 05 — Executor(执行师):SVG 页面生成规范 + +> **角色**:执行师 — 将 design_spec + spec_lock 转化为逐页 SVG +> **读取时机**:Step 3 (Executor) 进入执行阶段前 +> **前置依赖**:`06-svg-standards.md`(SVG 技术约束)、`08-layout-patterns.md`(布局模式)、`07-image-embedding.md`(图片嵌入,有图片时) + +--- + +## 核心使命 + +读取 `spec_lock.md` → 逐页手写 SVG → 质量检查 → 生成演讲备注 + +--- + +## 生成前强制步骤 + +### 1. 批量预读 + +在生成第一页前,一次性加载: +- `spec_lock.md` — 设计合约 +- `design_spec.md` — 设计叙事(了解完整上下文) +- 如果使用模板:全部模板 SVG 文件 + +### 2. 每页生成前 + +**每页生成前必须重新读取 `spec_lock.md`**。禁止在锁之外取值(颜色、图标、字体、字号、图片路径)。 + +--- + +## 每页布局节奏 + +从 `spec_lock.md` 获取 `page_rhythm` 字段: + +| rhythm | 策略 | 每页文字量 | +|--------|------|-----------| +| `anchor` | 封面/章节页 — 标题主导、高冲击力、大量留白 | ≤ 30 字 | +| `dense` | 内容页 — 卡片网格、图表、KPI、多栏 | ≤ 80 字 | +| `breathing` | 过渡页 — 单句金句、关键结论 | ≤ 20 字 | + +**强制规则**:不连续两页 dense。 + +--- + +## 封面设计 `anchor` + +- **Cover impact**:一句抓人的主标题 + 一句补充副标题 +- **视觉层次**:主标题 >> 副标题 >> 作者/日期 +- **常用布局**: + - 大标题居中偏上 + 底部装饰色块/图形 + - 左对齐大标题 + 右侧装饰图像 + - 全幅背景 + 居中标题 + +```xml + + + + + 主标题最多两行 + + + 副标题补充说明 + + + 作者 / 日期 + + +``` + +--- + +## 内容页 `dense` + +- **一行一个核心消息**:每页提炼一个 <15 字的标题 +- **内容组织**:优先用卡片网格(2×2 或 3×2),其次用编号列表 +- **文字量控制**:每页正文不超过 80 字 +- **文本高亮**:关键数值、对比结论、核心名词用加粗或强调色 + +### 双卡片模板 + +```xml + + + + 页面主标题 + + + + 卡片一标题 + + + 卡片一要点 + + + + 卡片二标题 + + + 卡片二要点 + + + + + + + +``` + +--- + +## 过渡页 `breathing` + +- 单句金句或关键结论 +- 极少元素,大量留白 +- 可配一个装饰图形或图标 + +--- + +## 结构规范 + +### 语义分组(强制) + +每页 3-8 个顶层 `` 分组: + +```xml +... +... +... +... +... +``` + +### 文本规范 + +- **一行一个 ``**:同行不同样式用 `` 内联 +- 禁止多个 `` 并排模拟一行 + +```xml + + 正常文字 加粗高亮 继续正常 + +``` + +--- + +## 图片处理 + +> **详细规范见 `07-image-embedding.md`** + +图片全部由用户提供。嵌入方式:将用户图片转为 Base64 直接写在 SVG 中: + +```xml + +``` + +**关键规则**: +- 背景图叠加半透明遮罩(`fill="#000000" opacity="0.25-0.45"`),保证文字可读 +- 图片上的文字用 `#FFFFFF` +- 根据图片宽高比选择左右分割或上下分割布局 + +--- + +## 演讲备注生成 + +为每页生成 200-500 字的中文演讲备注。格式要求: +- 口语化、可在演讲时自然朗读 +- 关键数字可读性("三百五十万"而非"350万") +- 每页独立成段,不跨页引用 + +--- + +## 禁止事项 + +- 禁止子代理生成 SVG +- 禁止脚本批量生成 +- 禁止在 spec_lock 之外取值 +- 禁止跳过任何一页 diff --git a/src/agent/skills/ppt_maker/attachments/06-svg-standards.md b/src/agent/skills/ppt_maker/attachments/06-svg-standards.md new file mode 100644 index 0000000..2672ef5 --- /dev/null +++ b/src/agent/skills/ppt_maker/attachments/06-svg-standards.md @@ -0,0 +1,151 @@ +# 06 — SVG 技术约束与 PPT 兼容性 + +> **角色**:技术规范手册 — 定义所有 SVG 编写时必须遵守的硬约束 +> **读取时机**:Executor 阶段写 SVG 前查阅 + +--- + +## 一、画布 + +| 格式 | viewBox | 用途 | +|------|---------|------| +| `ppt169` | `0 0 1280 720` | 16:9 演示(默认) | +| `ppt43` | `0 0 1024 768` | 4:3 传统 | + +**硬规则**:viewBox 是唯一尺寸来源,不要写 `width`/`height` 属性。 + +--- + +## 二、禁止特性黑名单 + +### 禁止使用 + +| 类别 | 禁止项 | 替代方案 | +|------|--------|----------| +| **字符** | HTML 命名实体 (` ` `—` `©`) | 原始 Unicode(` ` `—` `©`) | +| **元素** | `` | 用 `` + `` | +| **元素** | `