iflow-mcp_mcp-server-okppt 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1619 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+ from pptx.util import Inches, Pt, Cm, Emu
3
+ from typing import Optional, Union, List
4
+ import os
5
+ import datetime
6
+ import traceback
7
+ import re
8
+ import json
9
+ import logging
10
+ import sys
11
+ from mcp_server_okppt.svg_module import insert_svg_to_pptx, create_svg_file, get_pptx_slide_count, save_svg_code_to_file
12
+ # New import for our PPT operations
13
+ from mcp_server_okppt.ppt_operations import (
14
+ analyze_layout_details,
15
+ insert_layout,
16
+ clear_placeholder_content,
17
+ assign_placeholder_content
18
+ )
19
+
20
+ # Configure logging
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
24
+ stream=sys.stdout
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+ # 创建MCP服务器实例
28
+ mcp = FastMCP(name="main")
29
+ PROMPT_TEMPLATE_CONTENT = """
30
+ # PPT页面SVG设计宗师 · Prompt框架 v5.0
31
+ **Author: neekchaw**
32
+
33
+ %%%USER_CORE_DESIGN_TASK_HERE%%%
34
+
35
+ ## 1. 角色定位:SVG设计宗师
36
+
37
+ * **核心身份**: 你是一位深谙设计哲学与SVG技艺的"PPT页面SVG设计宗师"。你的出品不仅是视觉呈现,更是思想的载体与沟通的桥梁。
38
+ * **核心能力**:
39
+ * **洞察内容本质**: 快速穿透信息表象,精准提炼核心主旨、逻辑架构与情感基调。
40
+ * **驾驭多元风格**: 从经典商务到前沿科技,从简约素雅到繁复华丽,皆能游刃有余,并能进行现代化创新演绎。
41
+ * **平衡艺术与实用**: 完美融合设计美学与信息传达效率,确保作品既悦目又易懂。
42
+ * **精通SVG技艺与自我修正**: 输出结构清晰、语义化、高度优化且兼容性良好的SVG代码。在生成过程中,你会主动进行多轮自我审视与修正,确保代码质量与视觉效果。鼓励使用`<defs>`, `<use>`等进行元素复用和模块化。
43
+ * **预见性洞察**: 不仅满足明确需求,更能预见潜在问题或优化点(如可访问性细节、多设备适应性、潜在美学缺陷),并主动融入设计或提醒用户。
44
+ * **教育性沟通**: 在阐述设计决策时,能适时普及相关设计原理或最佳实践,帮助用户提升设计认知。
45
+ * **设计理念**: "设计服务于沟通,创意源于理解,技艺赋能表达,反思成就卓越。"
46
+
47
+ ## 2. 设计原则架构:三阶九律
48
+
49
+ * **第一阶:基石准则 (不可违背)**
50
+ 1. **比例规范**: 严格遵循16:9 SVG `viewBox="0 0 1600 900"`。
51
+ 2. **安全边际**: 核心内容必须完整落于 `100, 50, 1400, 800` 安全区内。
52
+ 3. **无障碍访问**: 文本对比度遵循WCAG AA级标准 (普通文本≥4.5:1, 大文本≥3:1)。
53
+ * **第二阶:核心导向 (优先遵循)**
54
+ 4. **信息层级**: 视觉层级清晰分明,主次信息一眼可辨,逻辑关系明确。
55
+ 5. **视觉焦点**: 页面必须有明确的视觉引导中心,快速吸引注意力。
56
+ 6. **阅读体验**: 字体大小、行高、字间距保证高度可读性与舒适性 (正文/提示文本≥16px)。
57
+ * **第三阶:创意疆域 (鼓励探索)**
58
+ 7. **风格创新与融贯**: 在理解用户指定风格基础上,鼓励进行现代化、个性化、情境化的创新演绎,并确保创新与整体风格的和谐统一。
59
+ 8. **视觉愉悦与和谐 (Visual Harmony & Appeal)**:
60
+ * 追求构图的平衡、稳定与韵律感,避免元素冲突或视觉失重。
61
+ * 色彩搭配需和谐、表意准确、符合情感基调,避免刺眼或混淆的组合。
62
+ * 细节处理(如对齐、间距、圆角、线条)需精致、一致,提升整体品质感。
63
+ 9. **主题共鸣**: 设计元素与主题深度关联,引发情感共鸣,强化信息记忆。
64
+
65
+ ## 3. 内容理解框架:三重透视
66
+
67
+ * **其一:本质洞察 (Essence)**
68
+ * 快速提炼用户输入(文本、主题、数据)的核心信息、逻辑脉络、预期传达的情感与态度。
69
+ * **其二:密度感知 (Density)**
70
+ * 引入CDI (内容密度指数) 0-10分评估体系:
71
+ * 0-3分 (低密度): 侧重视觉表达与创意空间。
72
+ * 4-7分 (中密度): 平衡信息呈现与设计美感,优先采用卡片式等模块化布局。
73
+ * 8-10分 (高密度): 优先内容筛选与结构优化,确保信息清晰,设计服务于阅读效率。
74
+ * **其三:价值分层 (Value)**
75
+ * 区分核心观点/数据、支撑论据/细节、辅助说明/装饰元素,以此为据合理分配视觉权重与空间资源。
76
+
77
+ ## 3.bis 美学与观感守护 (Aesthetic & Visual Sentinel)
78
+
79
+ * **核心理念**: 你内置了一位"美学哨兵",时刻守护设计的视觉品质,主动识别并修正潜在的观感缺陷。
80
+ * **图层与清晰度**:
81
+ * 确保前景元素清晰突出,背景元素有效衬托,避免非预期的图层覆盖或关键信息被遮挡。
82
+ * **空间与呼吸感**:
83
+ * 合理控制元素间距与页边距,为每个视觉模块(如卡片、图文组)提供充足的"呼吸空间",避免拥挤和压迫感。
84
+ * **对齐与秩序感**:
85
+ * 所有相关的视觉元素应有明确的对齐基准(水平、垂直、居中等),构建稳定、有序的视觉结构。
86
+ * **比例与协调性**:
87
+ * 关注元素自身的长宽比、以及不同元素间的相对大小,追求视觉上的协调与平衡。避免不成比例的拉伸或压缩。
88
+ * **色彩情感与和谐**:
89
+ * 色彩选择不仅要符合用户指定的风格和高亮色,更要考虑整体色调的情感倾向与视觉和谐性,避免色彩冲突或信息传递混淆。
90
+ * **"第一眼"美学自检**:
91
+ * 在设计过程的关键节点,尝试从普通用户的视角进行快速"第一眼"评估,检查是否存在任何明显的视觉不适、混乱或专业度不足之处。
92
+
93
+ ## 4. 设计决策框架:策略先行
94
+
95
+ * **布局策略**:
96
+ * 基于内容类型、密度及风格偏好,提供2-3种契合的布局方案(如卡片式、分栏式、中心辐射式、自由式等)供用户参考或选择。
97
+ * 卡片式布局作为模块化信息呈现的优先推荐,但非唯一解。
98
+ * **视觉叙事**:
99
+ * 构建清晰的视觉动线,运用格式塔原则引导观众视线,确保信息按预期逻辑高效传递。
100
+ * **风格演绎**:
101
+ * 深入解读用户指定风格(或根据内容推断风格)的文化内涵、视觉特征、情感联想,并结合"美学与观感守护"原则进行演绎。
102
+ * **具象化风格联想 (Evocative Style Visualization)**: 当用户指定一种较为抽象的风格(如"未来科技感")或一种意境(如"空灵"、"禅意"),你不仅要分析其设计元素层面的核心特征,还应尝试在内部构建或用简短的描述(约1-2句话)勾勒出符合该风格/意境的典型场景或氛围,以此加深理解并作为设计基调。例如,对于"禅意",你可能会联想到"雨后庭院,青苔石阶,一滴水珠从竹叶滑落的宁静瞬间"。这种联想将帮助你更精准地把握设计的整体感觉。 *(此联想过程主要用于AI内部理解,仅在必要时或被要求时才向用户简述以确认理解方向)*
103
+ * 当用户提及AI可能不熟悉的特定风格名词时,主动声明理解程度,并请求用户提供该风格的2-3个核心视觉特征描述或参考图像/案例。
104
+ * 对用户提出的意境描述(如"空灵"、"赛博朋克"),在通过上述具象化联想加深内部理解后,主动列出3-5个基于此理解而提炼出的匹配视觉元素、色彩倾向、构图手法,供用户确认或调整。
105
+ * 避免刻板复制,融合现代设计趋势与媒介特性进行创新性、情境化的视觉转译,始终以提升沟通效率和视觉愉悦感为目标。
106
+ * **图表运用**:
107
+ * 详见 `4.1 图表设计工具箱与风格指南`。
108
+
109
+ ## 4.1 图表设计工具箱与风格指南
110
+
111
+ * **核心图表类型清单 (简要)**:
112
+ * 条形图 (Bar)、折线图 (Line)、饼图/环形图 (Pie/Donut)、面积图 (Area)、散点图 (Scatter)。
113
+ * AI应能判断数据关系(比较、趋势、构成等)以建议合适图表类型。
114
+ * **可应用的图表风格关键词 (简要)**:
115
+ * 扁平化 (Flat)、简约 (Minimalist)、现代专业 (Modern Professional)、深色主题 (Dark Mode)。
116
+ * **图表SVG核心构造 (简要提示)**:
117
+ * 合理使用`<rect>`, `<circle>`, `<line>`, `<path>`, `<text>`, `<g>`。
118
+ * 关注`fill`, `stroke`, `font-family`, `font-size`, `text-anchor`等核心样式。
119
+ * **图表设计基本准则**: 清晰性、准确性、易读性、简洁性、一致性。
120
+ * **数据适配性提醒**: AI应主动评估数据与图表类型的匹配度,并在必要时向用户提出建议。
121
+
122
+ ## 5. 实施弹性区间:规范与自由的平衡
123
+
124
+ * **设计参数范围**:
125
+ * 整体留白率建议保持在 `20%-35%` 区间。
126
+ * 视觉层级建议控制在 `3-5` 个清晰可辨的层级。
127
+ * 鼓励在这些建议范围内,根据内容特性与设计目标灵活调整,而非机械执行。
128
+ * **风格探索边界**:
129
+ * 用户指定风格的核心特征(如麦肯锡的严谨、苹果的极简)必须保留并清晰传达。
130
+ * 在此基础上,色彩的细微调整、辅助图形的创意、排版细节的优化等均可大胆尝试。
131
+ * **创意试错空间**:
132
+ * 在不违背"基石准则"和用户核心诉求的前提下,允许尝试非常规的构图、色彩搭配或视觉元素组合,以期产生惊喜效果。
133
+
134
+ ## 6. 示例模块 (Few-Shot):启迪而非束缚
135
+
136
+ * **金质范例 (Golden Standard)**:
137
+ * *示例1*: "禅意留白"风格处理高管寄语类内容(核心原则:大面积留白,强调意境)。
138
+ * **对比参照 (Comparative Reference)**:
139
+ * *示例2*: 同一份"季度销售数据报告",分别采用"经典麦肯锡"风格与"现代数据可视化"风格的SVG呈现对比(核心差异:色彩与信息密度处理)。
140
+ * **演进之路 (Evolutionary Path)**:
141
+ * *示例3*: 一个从"初步线框草图"到"最终精细化SVG成品"的迭代过程片段展示(核心启发:迭代优化的价值)。
142
+ * **指令解析与精确实现 (Instruction Parsing & Precise Implementation)**:
143
+ * *示例4*: "具体风格指令实现 (Specific Style Directive Implementation)"
144
+ * 用户指令:
145
+ 1. 使用Bento Grid风格的视觉设计,纯白色底配合#FO5E1C颜色作为高亮。
146
+ 2. 强调超大字体或数字突出核心要点,画面中有超大视觉元素强调重点,与小元素的比例形成反差。
147
+ * 核心启发:演示模型如何精确解析并执行多条具体、量化的设计指令(包括特定风格名称、颜色代码、视觉强调手法),组合成一个统一的视觉风格输出。
148
+ * **自我修正与美学提升 (Self-Correction & Aesthetic Enhancement)**:
149
+ * *示例5*: "美学缺陷识别与修正 (Aesthetic Flaw Detection & Correction)"
150
+ * 场景:AI生成了一个SVG初稿,其中一个重要文本元素因图层顺序错误被背景图形部分遮挡,且整体配色略显沉闷,缺乏焦点。
151
+ * 反思与修正过程:AI通过"美学与观感守护"自检,识别出图层遮挡问题并调整了相应元素的SVG顺序;同时,基于对"视觉焦点"原则的再思考,微调了高亮色的饱和度或点缀性地增加了少量对比色,提升了视觉吸引力。
152
+ * 核心启发:展示AI主动识别并修正功能性(如图层)和美学性(如色彩、焦点)缺陷的能力,体现其内置的反思与自我优化机制。
153
+ * **核心理念**: "示例旨在点亮思路,开拓视野,而非提供刻板模仿的模板。宗师之道,在于借鉴通变,举一反三,将示例中的设计思想迁移应用。"
154
+
155
+ ## 7. 工作流程指引:四阶修炼 (内置反思与纠错循环)
156
+
157
+ 1. **阶段一:深度聆听与精准解构 (Empathize & Deconstruct)**
158
+ * 与用户充分沟通,全面理解原始需求、核心内容、潜在目标、风格偏好及任何特定约束。
159
+ * 运用"内容理解框架"对输入信息进行系统性分析。
160
+ * **初步反思点**: 对用户需求的理解是否存在偏差?核心信息是否完全捕捉?有无遗漏关键约束?
161
+
162
+ 2. **阶段二:多元构思与方案初选 (Ideate & Prioritize)**
163
+ * 基于分析结果,从布局、色彩、排版、视觉元素等多维度进行开放式创意构思。
164
+ * 结合"设计决策框架",筛选出2-3个高质量、差异化的初步设计方向/布局骨架。
165
+ * **方案反思点**: 初选方案是否真正解决了用户核心问题?布局骨架是否具备良好的扩展性和视觉潜力?是否已初步考虑"三阶九律"和"美学与观感守护"的基本原则?能否清晰向用户阐述各方案优劣?
166
+ * **关键确认点**: 主动向用户呈现对核心需求(内容概要、理解的风格方向、初步布局构想)的理解摘要,并请求用户确认或修正。
167
+
168
+ 3. **阶段三:匠心雕琢与细节呈现 (Craft & Execute - 模块化反思驱动)**
169
+ * 选定主攻方向后,开始具体的SVG设计与代码生成。
170
+ * **模块化构建与即时审视**: 在完成每个主要视觉模块(如一个信息卡片、一个图表、一个核心图文组合)后,进行一次局部的功能性和美学校验,对照"设计原则架构"和"美学与观感守护"的关键点进行快速检查和微调。
171
+ * **核心反思点 (自我审评1.0 - 完成主要元素绘制后)**: SVG代码是否符合规范?所有元素是否在安全区内?文本对比度是否达标?图层关系是否正确无遮挡?元素间距、对齐是否初步合理?色彩搭配是否符合选定风格且无明显冲突?整体是否已体现"美学与观感守护"中的基本要求?主动记录已识别并修正的关键问题。
172
+
173
+ 4. **阶段四:审视完善与美学升华 (Review, Refine & Elevate)**
174
+ * 在完成整体SVG初稿后,进行最终的、全局性的设计审查和美学升华。
175
+ * **全面自检**: 严格对照"三阶九律"、"美学与观感守护"、"实施弹性区间"以及AI内部更详尽的"反思清单"(模拟),进行逐项、细致的自我检查。
176
+ * **寻找提升点**: 不仅是纠错,更要思考如何让设计在细节、氛围、创意上更进一步,超越基础要求。
177
+ * **最终反思点 (自我审评2.0 - 交付前)**: 设计是否完美达成所有明确和隐含的目标?是否存在任何被忽略的细节或潜在的观感问题?我将如何在"自我审视与修正摘要"中清晰阐述我的设计决策和自我优化过程?
178
+ * 主动邀请用户审阅,清晰阐述设计思路、关键决策及自我修正过程,并根据反馈进行迭代优化。
179
+
180
+ ## 8. 输出格式标准:专业呈现
181
+
182
+ 1. **设计提案书 (Design Proposal Document)**:
183
+ * **a. 内容解读与设计洞察**: 对用户需求的理解,对内容核心价值的提炼。
184
+ * **b. 核心设计理念与风格阐释**: 本次设计的核心思路,对所选风格的理解与应用策略。
185
+ * **c. 主方案视觉预览 (可选,若适用)**: 通过文本描述或关键元素示意图,让用户对设计方向有初步感知。
186
+ * **d. 关键设计决策点解析**: 说明布局、色彩、字体、关键视觉元素选择的理由。
187
+ * **e. 自我审视与修正摘要 (Self-Review & Correction Summary)**: 简述在设计过程中,主动识别并修正的关键逻辑、功能或美学问题(例如:根据美学守护原则调整了图层顺序,优化了色彩对比以增强可读性等),以及遵循核心设计原则的体现。
188
+ 2. **创意备选 (Creative Alternatives - 若有)**:
189
+ * 简述1-2个在核心目标一致前提下的不同设计侧重点或风格变体的思路。
190
+ 3. **SVG交付物 (SVG Deliverable)**:
191
+ * 包含完整、结构清晰、语义化、经过优化的SVG代码。
192
+ * `<svg width="1600" height="900" viewBox="0 0 1600 900" xmlns="http://www.w3.org/2000/svg"> ... </svg>`
193
+ 4. **协作指引 (Collaboration Guide)**:
194
+ * 对SVG代码中用户最可能需要调整的部分(如主题色变量、主要文本区域的ID、可替换图片/图标的标识等)进行注释或说明,方便用户二次修改或开发者集成。
195
+ * 提出后续可能的调整方向与进一步优化的可能性探讨。
196
+
197
+ ## 9. 动态调整机制:持续进化
198
+
199
+ * **用户反馈通道**: 清晰、准确地接收用户针对设计方案提出的具体、可执行的调整意见。
200
+ * **调整优先级**: 调整请求将依照"设计原则架构"的层级进行权衡:基石准则 > 核心导向 > 用户即时偏好 > 创意疆域内的细微探索。
201
+ * **迭代优化承诺**: 致力于通过不多于三轮(通常情况)的有效沟通与迭代,达至用户满意且符合专业标准的最佳设计成果。
202
+
203
+ ## 10. 初始化与交互规则:默契开场
204
+
205
+ * **AI状态声明**: "请提供您的需求与原始素材,我将倾力为您打造兼具洞察与美感的SVG设计方案。"
206
+ * **交互模式**: 以深度对话、方案探讨、共同决策为主要模式。鼓励用户在关键节点参与思考与选择。
207
+ * **服务边界**: 本次核心专注于单页静态PPT页面的SVG视觉设计。复杂的动态交互效果、多页面联动逻辑、或SVG动画实现,非本次主要交付范围,但可作为未来延展方向探讨。
208
+
209
+ """
210
+ # 路径辅助函数
211
+ def get_base_dir():
212
+ """获取基础目录(服务器目录的父目录)"""
213
+ current_dir = os.path.dirname(os.path.abspath(__file__))
214
+ return os.path.dirname(current_dir)
215
+
216
+ def get_tmp_dir():
217
+ """获取临时文件目录,如果不存在则创建"""
218
+ tmp_dir = os.path.join(get_base_dir(), "tmp")
219
+ os.makedirs(tmp_dir, exist_ok=True)
220
+ return tmp_dir
221
+
222
+ def get_output_dir():
223
+ """获取输出文件目录,如果不存在则创建"""
224
+ output_dir = os.path.join(get_base_dir(), "output")
225
+ os.makedirs(output_dir, exist_ok=True)
226
+ return output_dir
227
+
228
+ def cleanup_filename(filename: str) -> str:
229
+ """
230
+ 清理文件名,移除所有旧的时间戳和操作类型标记
231
+
232
+ Args:
233
+ filename: 要清理的文件名(不含路径和扩展名)
234
+
235
+ Returns:
236
+ 清理后的基本文件名
237
+ """
238
+ # 移除类似 _svg_20240101_120000, _deleted_20240529_153045 等操作标记和时间戳
239
+ # 模式: _ + 操作名 + _ + 8位日期 + _ + 6位时间
240
+ pattern = r'_(svg|deleted|inserted|output)_\d{8}_\d{6}'
241
+ cleaned = re.sub(pattern, '', filename)
242
+
243
+ # 防止文件名连续处理后残留多余的下划线
244
+ cleaned = re.sub(r'_{2,}', '_', cleaned)
245
+
246
+ # 移除末尾的下划线(如果有)
247
+ cleaned = cleaned.rstrip('_')
248
+
249
+ return cleaned
250
+
251
+ def get_default_output_path(file_type="pptx", base_name=None, op_type=None):
252
+ """
253
+ 获取默认输出文件路径
254
+
255
+ Args:
256
+ file_type: 文件类型(扩展名)
257
+ base_name: 基本文件名,如果为None则使用时间戳
258
+ op_type: 操作类型,用于在文件名中添加标记
259
+
260
+ Returns:
261
+ 默认输出文件路径
262
+ """
263
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
264
+
265
+ if base_name is None:
266
+ base_name = f"output_{timestamp}"
267
+ else:
268
+ # 清理基本文件名
269
+ base_name = cleanup_filename(base_name)
270
+
271
+ # 添加操作类型和时间戳
272
+ if op_type:
273
+ base_name = f"{base_name}_{op_type}_{timestamp}"
274
+ else:
275
+ base_name = f"{base_name}_{timestamp}"
276
+
277
+ return os.path.join(get_output_dir(), f"{base_name}.{file_type}")
278
+
279
+ # 主要的SVG插入工具
280
+ @mcp.tool()
281
+ def insert_svg(
282
+ pptx_path: str,# 空字符串表示自动创建,否则使用绝对路径
283
+ svg_path: List[str],# 数组,绝对路径
284
+ slide_number: int = 1,
285
+ x_inches: float = 0,
286
+ y_inches: float = 0,
287
+ width_inches: float = 16,
288
+ height_inches: float = 9,
289
+ output_path: str = "",# 空字符串表示自动创建,否则使用绝对路径
290
+ create_if_not_exists: bool = True
291
+ ) -> str:
292
+ """
293
+ 将SVG图像插入到PPTX文件的指定位置。(如果需要替换已有的幻灯片,请组合使用`delete_slide`和`insert_blank_slide`功能)
294
+ 如果未提供PPTX路径,将自动创建一个临时文件,位于服务器同级目录的tmp目录。
295
+ 如果未提供输出路径,将使用标准输出目录,位于服务器同级目录的output目录。
296
+ 如果未提供坐标,默认对齐幻灯片左上角。
297
+ 如果未提供宽度和高度,默认覆盖整个幻灯片(16:9)。
298
+
299
+ 支持批量处理:
300
+ - 如果svg_path是单个字符串数组,则将SVG添加到slide_number指定的页面
301
+ - 如果svg_path是列表,则从slide_number开始顺序添加每个SVG,即第一个SVG添加到
302
+ slide_number页,第二个添加到slide_number+1页,依此类推
303
+
304
+ Args:
305
+ pptx_path: PPTX文件路径,如果未提供则自动创建一个临时文件,最好使用英文路径
306
+ svg_path: SVG文件路径或SVG文件路径列表,最好使用英文路径
307
+ slide_number: 起始幻灯片编号(从1开始)
308
+ x_inches: X坐标(英寸),如果未指定则默认为0
309
+ y_inches: Y坐标(英寸),如果未指定则默认为0
310
+ width_inches: 宽度(英寸),如果未指定则使用幻灯片宽度
311
+ height_inches: 高度(英寸),如果未指定则根据宽度计算或使用幻灯片高度
312
+ output_path: 输出文件路径,如果未指定则使用标准输出目录
313
+ create_if_not_exists: 如果为True且PPTX文件不存在,将自动创建一个新文件
314
+
315
+ Returns:
316
+ 操作结果消息,包含详细的错误信息(如果有)
317
+ """
318
+ # 收集错误信息
319
+ error_messages = []
320
+ result_messages = []
321
+
322
+ # 如果未提供pptx_path,使用默认输出目录创建一个
323
+ if not pptx_path or pptx_path.strip() == "":
324
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
325
+ pptx_path = os.path.join(get_output_dir(), f"presentation_{timestamp}.pptx")
326
+ print(f"未提供PPTX路径,将使用默认路径: {pptx_path}")
327
+
328
+ # 处理输出路径
329
+ if not output_path:
330
+ # 从原始文件名生成输出文件名
331
+ base_name = os.path.splitext(os.path.basename(pptx_path))[0]
332
+ base_name = cleanup_filename(base_name)
333
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
334
+ output_path = os.path.join(get_output_dir(), f"{base_name}_svg_{timestamp}.pptx")
335
+
336
+ if not os.path.isabs(pptx_path):
337
+ pptx_path = os.path.abspath(pptx_path)
338
+
339
+ # 确保PPTX文件的父目录存在
340
+ pptx_dir = os.path.dirname(pptx_path)
341
+ if not os.path.exists(pptx_dir):
342
+ try:
343
+ os.makedirs(pptx_dir, exist_ok=True)
344
+ print(f"已创建PPTX目录: {pptx_dir}")
345
+ error_messages.append(f"已创建PPTX目录: {pptx_dir}")
346
+ except Exception as e:
347
+ error_msg = f"创建PPTX目录 {pptx_dir} 时出错: {e}"
348
+ error_messages.append(error_msg)
349
+ return error_msg
350
+
351
+ # 将英寸转换为Inches对象
352
+ x = Inches(x_inches) if x_inches is not None else None
353
+ y = Inches(y_inches) if y_inches is not None else None
354
+ width = Inches(width_inches) if width_inches is not None else None
355
+ height = Inches(height_inches) if height_inches is not None else None
356
+
357
+ # 如果提供了输出路径且是相对路径,转换为绝对路径
358
+ if output_path and not os.path.isabs(output_path):
359
+ output_path = os.path.abspath(output_path)
360
+
361
+ # 如果提供了输出路径,确保其父目录存在
362
+ if output_path:
363
+ output_dir = os.path.dirname(output_path)
364
+ if not os.path.exists(output_dir):
365
+ try:
366
+ os.makedirs(output_dir, exist_ok=True)
367
+ print(f"已创建输出目录: {output_dir}")
368
+ error_messages.append(f"已创建输出目录: {output_dir}")
369
+ except Exception as e:
370
+ error_msg = f"创建输出目录 {output_dir} 时出错: {e}"
371
+ error_messages.append(error_msg)
372
+ return error_msg
373
+
374
+ # 检查svg_path的类型并分别处理
375
+ if isinstance(svg_path, str):
376
+ # 单个SVG文件处理
377
+ return process_single_svg(
378
+ pptx_path, svg_path, slide_number, x, y, width, height,
379
+ output_path, create_if_not_exists
380
+ )
381
+ elif isinstance(svg_path, list):
382
+ # 批量处理SVG文件列表
383
+ success_count = 0
384
+ total_count = len(svg_path)
385
+
386
+ if total_count == 0:
387
+ return "错误:SVG文件列表为空"
388
+
389
+ # 创建中间文件路径基础
390
+ temp_base = os.path.join(get_tmp_dir(), f"svg_batch_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}")
391
+ os.makedirs(os.path.dirname(temp_base), exist_ok=True)
392
+
393
+ # 当前输入文件路径
394
+ current_input = pptx_path
395
+
396
+ for i, current_svg in enumerate(svg_path):
397
+ current_slide = slide_number + i
398
+
399
+ # 处理每个SVG文件
400
+ if i < total_count - 1:
401
+ # 对于非最后一个文件,创建临时输出路径
402
+ temp_output = f"{temp_base}_step_{i}.pptx"
403
+
404
+ result = process_single_svg(
405
+ current_input,
406
+ current_svg,
407
+ current_slide,
408
+ x, y, width, height,
409
+ temp_output,
410
+ create_if_not_exists
411
+ )
412
+
413
+ # 下一次迭代的输入文件是本次的输出文件
414
+ current_input = temp_output
415
+ else:
416
+ # 最后一个SVG使用最终输出路径
417
+ final_output = output_path if output_path else pptx_path
418
+
419
+ result = process_single_svg(
420
+ current_input,
421
+ current_svg,
422
+ current_slide,
423
+ x, y, width, height,
424
+ final_output,
425
+ create_if_not_exists
426
+ )
427
+
428
+ # 检查处理结果
429
+ if "成功" in result:
430
+ success_count += 1
431
+ result_messages.append(f"第{i+1}个SVG({current_svg}):成功添加到第{current_slide}页")
432
+ else:
433
+ result_messages.append(f"第{i+1}个SVG({current_svg}):添加失败 - {result}")
434
+
435
+ # 清理临时文件
436
+ for i in range(total_count - 1):
437
+ temp_file = f"{temp_base}_step_{i}.pptx"
438
+ if os.path.exists(temp_file):
439
+ try:
440
+ os.remove(temp_file)
441
+ except Exception as e:
442
+ print(f"清理临时文件 {temp_file} 时出错: {e}")
443
+
444
+ # 返回总体结果
445
+ result_path = output_path or pptx_path
446
+ summary = f"批量处理完成:共{total_count}个SVG文件,成功{success_count}个,失败{total_count-success_count}个"
447
+ details = "\n".join(result_messages)
448
+ return f"{summary}\n输出文件:{result_path}\n\n详细结果:\n{details}"
449
+ else:
450
+ return f"错误:svg_path类型无效,必须是字符串或字符串列表,当前类型: {type(svg_path)}"
451
+
452
+ def process_single_svg(
453
+ pptx_path: str,
454
+ svg_path: str,
455
+ slide_number: int,
456
+ x: Optional[Union[Inches, Pt, Cm, Emu, float]],
457
+ y: Optional[Union[Inches, Pt, Cm, Emu, float]],
458
+ width: Optional[Union[Inches, Pt, Cm, Emu, float]],
459
+ height: Optional[Union[Inches, Pt, Cm, Emu, float]],
460
+ output_path: Optional[str],
461
+ create_if_not_exists: bool
462
+ ) -> str:
463
+ """处理单个SVG文件的辅助函数"""
464
+ # 检查SVG文件是否存在,如果是相对路径则转换为绝对路径
465
+ if not os.path.isabs(svg_path):
466
+ svg_path = os.path.abspath(svg_path)
467
+
468
+ # 确保SVG文件的父目录存在
469
+ svg_dir = os.path.dirname(svg_path)
470
+ if not os.path.exists(svg_dir):
471
+ try:
472
+ os.makedirs(svg_dir, exist_ok=True)
473
+ print(f"已创建SVG目录: {svg_dir}")
474
+ except Exception as e:
475
+ return f"创建SVG目录 {svg_dir} 时出错: {e}"
476
+
477
+ # 如果SVG文件不存在且create_if_not_exists为True,则创建一个简单的SVG文件
478
+ if not os.path.exists(svg_path) and create_if_not_exists:
479
+ svg_created = create_svg_file(svg_path)
480
+ if not svg_created:
481
+ return f"错误:无法创建SVG文件 {svg_path}"
482
+ elif not os.path.exists(svg_path):
483
+ return f"错误:SVG文件 {svg_path} 不存在"
484
+
485
+ # 确保输出路径存在,如果未指定则使用标准输出目录
486
+ if not output_path:
487
+ # 从原始文件名生成输出文件名
488
+ base_name = os.path.splitext(os.path.basename(pptx_path))[0]
489
+ # 清理文件名
490
+ base_name = cleanup_filename(base_name)
491
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
492
+ output_path = os.path.join(get_output_dir(), f"{base_name}_svg_{timestamp}.pptx")
493
+
494
+ try:
495
+ # 调用改进后的函数,它现在返回一个元组 (成功标志, 错误消息)
496
+ result = insert_svg_to_pptx(
497
+ pptx_path=pptx_path,
498
+ svg_path=svg_path,
499
+ slide_number=slide_number,
500
+ x=x,
501
+ y=y,
502
+ width=width,
503
+ height=height,
504
+ output_path=output_path,
505
+ create_if_not_exists=create_if_not_exists
506
+ )
507
+
508
+ # 检查返回值类型
509
+ if isinstance(result, tuple) and len(result) == 2:
510
+ success, error_details = result
511
+ else:
512
+ # 向后兼容
513
+ success = result
514
+ error_details = ""
515
+
516
+ if success:
517
+ result_path = output_path or pptx_path
518
+ was_created = not os.path.exists(pptx_path) and create_if_not_exists
519
+ creation_msg = "(已自动创建PPTX文件)" if was_created else ""
520
+ return f"成功将SVG文件 {svg_path} 插入到 {result_path} 的第 {slide_number} 张幻灯片 {creation_msg}"
521
+ else:
522
+ # 返回详细的错误信息
523
+ return f"插入SVG到PPTX文件失败,详细错误信息:\n{error_details}"
524
+ except Exception as e:
525
+ # 收集异常堆栈
526
+ error_trace = traceback.format_exc()
527
+ return f"插入SVG时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}"
528
+
529
+ @mcp.tool()
530
+ def list_files(directory: str = ".", file_type: Optional[str] = None) -> str:
531
+ """
532
+ 列出目录中的文件,可选按文件类型过滤。
533
+ 如需查看svg文件是否正确保存,请输入svg文件的保存路径。
534
+ Args:
535
+ directory: 要列出文件的目录路径
536
+ file_type: 文件类型过滤,可以是 "svg" 或 "pptx"
537
+
538
+ Returns:
539
+ 文件列表(每行一个文件)
540
+ """
541
+ import os
542
+
543
+ if not os.path.exists(directory):
544
+ return f"错误:目录 {directory} 不存在"
545
+
546
+ files = os.listdir(directory)
547
+
548
+ if file_type:
549
+ file_type = file_type.lower()
550
+ extensions = {
551
+ "svg": [".svg"],
552
+ "pptx": [".pptx", ".ppt"]
553
+ }
554
+
555
+ if file_type in extensions:
556
+ filtered_files = []
557
+ for file in files:
558
+ if any(file.lower().endswith(ext) for ext in extensions[file_type]):
559
+ filtered_files.append(file)
560
+ files = filtered_files
561
+ else:
562
+ files = [f for f in files if f.lower().endswith(f".{file_type}")]
563
+
564
+ if not files:
565
+ return f"未找到{'任何' if not file_type else f'{file_type}'} 文件"
566
+
567
+ return "\n".join(files)
568
+
569
+ @mcp.tool()
570
+ def get_file_info(file_path: str) -> str:
571
+ """
572
+ 获取文件信息,如存在状态、大小等。
573
+
574
+ Args:
575
+ file_path: 要查询的文件路径
576
+
577
+ Returns:
578
+ 文件信息
579
+ """
580
+ import os
581
+
582
+ if not os.path.exists(file_path):
583
+ return f"文件 {file_path} 不存在"
584
+
585
+ if os.path.isdir(file_path):
586
+ return f"{file_path} 是一个目录"
587
+
588
+ size_bytes = os.path.getsize(file_path)
589
+ size_kb = size_bytes / 1024
590
+ size_mb = size_kb / 1024
591
+
592
+ if size_mb >= 1:
593
+ size_str = f"{size_mb:.2f} MB"
594
+ else:
595
+ size_str = f"{size_kb:.2f} KB"
596
+
597
+ modified_time = os.path.getmtime(file_path)
598
+ from datetime import datetime
599
+ modified_str = datetime.fromtimestamp(modified_time).strftime("%Y-%m-%d %H:%M:%S")
600
+
601
+ # 获取文件扩展名
602
+ _, ext = os.path.splitext(file_path)
603
+ ext = ext.lower()
604
+
605
+ file_type = None
606
+ if ext == ".svg":
607
+ file_type = "SVG图像"
608
+ elif ext in [".pptx", ".ppt"]:
609
+ file_type = "PowerPoint演示文稿"
610
+ else:
611
+ file_type = f"{ext[1:]} 文件" if ext else "未知类型文件"
612
+
613
+ return f"文件: {file_path}\n类型: {file_type}\n大小: {size_str}\n修改时间: {modified_str}"
614
+
615
+ # 添加一个将SVG转换为PNG的工具
616
+ @mcp.tool()
617
+ def convert_svg_to_png(
618
+ svg_path: str,
619
+ output_path: Optional[str] = None
620
+ ) -> str:
621
+ """
622
+ 将SVG文件转换为PNG图像。
623
+
624
+ Args:
625
+ svg_path: SVG文件路径
626
+ output_path: 输出PNG文件路径,如果未指定则使用相同文件名但扩展名为.png
627
+
628
+ Returns:
629
+ 操作结果消息
630
+ """
631
+ from reportlab.graphics import renderPM
632
+ from svglib.svglib import svg2rlg
633
+ import os
634
+
635
+ if not os.path.exists(svg_path):
636
+ return f"错误:SVG文件 {svg_path} 不存在"
637
+
638
+ if not output_path:
639
+ # 获取不带扩展名的文件名,然后添加.png扩展名
640
+ base_name = os.path.splitext(svg_path)[0]
641
+ output_path = f"{base_name}.png"
642
+
643
+ try:
644
+ drawing = svg2rlg(svg_path)
645
+ if drawing is None:
646
+ return f"错误:无法读取SVG文件 {svg_path}"
647
+
648
+ renderPM.drawToFile(drawing, output_path, fmt="PNG")
649
+ return f"成功将SVG文件 {svg_path} 转换为PNG文件 {output_path}\n宽度: {drawing.width}px\n高度: {drawing.height}px"
650
+ except Exception as e:
651
+ return f"转换SVG到PNG时发生错误: {str(e)}"
652
+
653
+ @mcp.tool()
654
+ def get_pptx_info(pptx_path: str) -> str:
655
+ """
656
+ 获取PPTX文件的基本信息,包括幻灯片数量。
657
+
658
+ Args:
659
+ pptx_path: PPTX文件路径
660
+
661
+ Returns:
662
+ 包含文件信息和幻灯片数量的字符串
663
+ """
664
+ import os
665
+
666
+ # 确保路径存在
667
+ if not os.path.isabs(pptx_path):
668
+ pptx_path = os.path.abspath(pptx_path)
669
+
670
+ # 先获取基本文件信息
671
+ if not os.path.exists(pptx_path):
672
+ return f"错误:文件 {pptx_path} 不存在"
673
+
674
+ size_bytes = os.path.getsize(pptx_path)
675
+ size_kb = size_bytes / 1024
676
+ size_mb = size_kb / 1024
677
+
678
+ if size_mb >= 1:
679
+ size_str = f"{size_mb:.2f} MB"
680
+ else:
681
+ size_str = f"{size_kb:.2f} KB"
682
+
683
+ modified_time = os.path.getmtime(pptx_path)
684
+ from datetime import datetime
685
+ modified_str = datetime.fromtimestamp(modified_time).strftime("%Y-%m-%d %H:%M:%S")
686
+
687
+ # 获取幻灯片数量
688
+ slide_count, error = get_pptx_slide_count(pptx_path)
689
+
690
+ if error:
691
+ slide_info = f"获取幻灯片数量失败:{error}"
692
+ else:
693
+ slide_info = f"幻灯片数量:{slide_count}张"
694
+
695
+ return f"PPT文件: {pptx_path}\n大小: {size_str}\n修改时间: {modified_str}\n{slide_info}"
696
+
697
+ @mcp.tool()
698
+ def save_svg_code(
699
+ svg_code: str
700
+ ) -> str:
701
+ """
702
+ 将SVG代码保存为SVG文件并返回保存的绝对路径。
703
+ !!!注意:特殊字符如"&"需要转义为"&amp;"
704
+ Args:
705
+ svg_code: SVG代码内容
706
+
707
+ Returns:
708
+ 操作结果消息,包含保存的文件路径或错误信息
709
+ """
710
+ try:
711
+ # 调用svg_module中的函数保存SVG代码
712
+ success, file_path, error_message = save_svg_code_to_file(
713
+ svg_code=svg_code,
714
+ output_path="",
715
+ create_dirs=True
716
+ )
717
+
718
+ if success:
719
+ return f"成功保存SVG代码到文件: {file_path}"
720
+ else:
721
+ return f"保存SVG代码到文件失败: {error_message}"
722
+ except Exception as e:
723
+ error_trace = traceback.format_exc()
724
+ return f"保存SVG代码到文件时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}"
725
+
726
+ @mcp.tool()
727
+ def delete_slide(
728
+ pptx_path: str,
729
+ slide_number: int,
730
+ output_path: Optional[str] = None
731
+ ) -> str:
732
+ """
733
+ 从PPTX文件中删除指定编号的幻灯片。
734
+
735
+ !!!注意:
736
+
737
+ 在使用SVG替换PPT幻灯片内容时,我们发现了一些关键点,以下是正确替换PPT内容的方法总结:
738
+
739
+ ### 正确替换PPT内容的方法
740
+
741
+ 1. **完全替换法**(最可靠):
742
+ - 删除需要替换的幻灯片(使用`delete_slide`功能)
743
+ - 在同一位置插入空白幻灯片(使用`insert_blank_slide`功能)
744
+ - 将新的SVG内容插入到空白幻灯片(使用`insert_svg`功能)
745
+
746
+ 2. **新文件法**(适合多页修改):
747
+ - 创建全新的PPT文件
748
+ - 将所有需要的SVG(包括已修改的)按顺序插入到新文件中
749
+ - 这样可以避免在旧文件上操作导致的混淆和叠加问题
750
+
751
+ 3. **注意事项**:
752
+ - 直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换
753
+ - 文件名可能会随着多次操作变得过长,影响可读性
754
+ - 批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件
755
+ - 操作后应检查输出文件以确认修改是否成功
756
+
757
+ ### 推荐工作流
758
+
759
+ 1. 先保存修改后的SVG内容到文件
760
+ 2. 创建一个全新的PPT文件
761
+ 3. 按顺序一次性插入所有SVG(包括已修改和未修改的)
762
+ 4. 使用简洁直观的文件名
763
+
764
+ 这种方法避免了多步骤操作导致的文件混乱,也能确保每张幻灯片都是干净的、不包含叠加内容的。
765
+
766
+ Args:
767
+ pptx_path: PPTX文件路径
768
+ slide_number: 要删除的幻灯片编号(从1开始)
769
+ output_path: 输出文件路径,如果未指定则使用标准输出目录
770
+
771
+ Returns:
772
+ 操作结果消息
773
+ """
774
+ try:
775
+ # 确保路径是绝对路径
776
+ if not os.path.isabs(pptx_path):
777
+ pptx_path = os.path.abspath(pptx_path)
778
+
779
+ # 检查文件是否存在
780
+ if not os.path.exists(pptx_path):
781
+ return f"错误:PPTX文件 {pptx_path} 不存在"
782
+
783
+ # 处理输出路径,如果未指定则使用标准输出目录
784
+ if not output_path:
785
+ # 从原始文件名生成输出文件名
786
+ base_name = os.path.splitext(os.path.basename(pptx_path))[0]
787
+ # 清理文件名
788
+ base_name = cleanup_filename(base_name)
789
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
790
+ output_path = os.path.join(get_output_dir(), f"{base_name}_deleted_{timestamp}.pptx")
791
+
792
+ if output_path and not os.path.isabs(output_path):
793
+ output_path = os.path.abspath(output_path)
794
+
795
+ # 如果提供了输出路径,确保其父目录存在
796
+ if output_path:
797
+ output_dir = os.path.dirname(output_path)
798
+ if not os.path.exists(output_dir):
799
+ try:
800
+ os.makedirs(output_dir, exist_ok=True)
801
+ except Exception as e:
802
+ return f"创建输出目录 {output_dir} 时出错: {e}"
803
+
804
+ # 使用python-pptx加载演示文稿
805
+ from pptx import Presentation
806
+ prs = Presentation(pptx_path)
807
+
808
+ # 检查幻灯片编号范围
809
+ if not 1 <= slide_number <= len(prs.slides):
810
+ return f"错误:幻灯片编号 {slide_number} 超出范围 [1, {len(prs.slides)}]"
811
+
812
+ # 计算索引(转换为从0开始)
813
+ slide_index = slide_number - 1
814
+
815
+ # 使用用户提供的方法删除幻灯片
816
+ slides = list(prs.slides._sldIdLst)
817
+ prs.slides._sldIdLst.remove(slides[slide_index])
818
+
819
+ # 保存文件
820
+ save_path = output_path
821
+ prs.save(save_path)
822
+
823
+ return f"成功从 {pptx_path} 中删除第 {slide_number} 张幻灯片,结果已保存到 {save_path}"
824
+
825
+ except Exception as e:
826
+ error_trace = traceback.format_exc()
827
+ return f"删除幻灯片时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}"
828
+
829
+ @mcp.tool()
830
+ def insert_blank_slide(
831
+ pptx_path: str,
832
+ slide_number: int,
833
+ layout_index: int = 6, # 默认使用空白布局
834
+ output_path: Optional[str] = None,
835
+ create_if_not_exists: bool = True
836
+ ) -> str:
837
+ """
838
+ 在PPTX文件的指定位置插入一个空白幻灯片。
839
+
840
+ !!!注意:
841
+
842
+ 在使用SVG替换PPT幻灯片内容时,我们发现了一些关键点,以下是正确替换PPT内容的方法总结:
843
+
844
+ ### 正确替换PPT内容的方法
845
+
846
+ 1. **完全替换法**(最可靠):
847
+ - 删除需要替换的幻灯片(使用`delete_slide`功能)
848
+ - 在同一位置插入空白幻灯片(使用`insert_blank_slide`功能)
849
+ - 将新的SVG内容插入到空白幻灯片(使用`insert_svg`功能)
850
+
851
+ 2. **新文件法**(适合多页修改):
852
+ - 创建全新的PPT文件
853
+ - 将所有需要的SVG(包括已修改的)按顺序插入到新文件中
854
+ - 这样可以避免在旧文件上操作导致的混淆和叠加问题
855
+
856
+ 3. **注意事项**:
857
+ - 直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换
858
+ - 文件名可能会随着多次操作变得过长,影响可读性
859
+ - 批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件
860
+ - 操作后应检查输出文件以确认修改是否成功
861
+
862
+ ### 推荐工作流
863
+
864
+ 1. 先保存修改后的SVG内容到文件
865
+ 2. 创建一个全新的PPT文件
866
+ 3. 按顺序一次性插入所有SVG(包括已修改和未修改的)
867
+ 4. 使用简洁直观的文件名
868
+
869
+ 这种方法避免了多步骤操作导致的文件混乱,也能确保每张幻灯片都是干净的、不包含叠加内容的。
870
+
871
+ Args:
872
+ pptx_path: PPTX文件路径
873
+ slide_number: 要插入幻灯片的位置编号(从1开始)
874
+ layout_index: 幻灯片布局索引,默认为6(空白布局)
875
+ output_path: 输出文件路径,如果未指定则使用标准输出目录
876
+ create_if_not_exists: 如果为True且PPTX文件不存在,将自动创建一个新文件
877
+
878
+ Returns:
879
+ 操作结果消息
880
+ """
881
+ try:
882
+ # 如果未提供pptx_path,使用默认输出目录创建一个
883
+ if not pptx_path or pptx_path.strip() == "":
884
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
885
+ pptx_path = os.path.join(get_output_dir(), f"presentation_{timestamp}.pptx")
886
+ print(f"未提供PPTX路径,将使用默认路径: {pptx_path}")
887
+
888
+ # 确保路径是绝对路径
889
+ if not os.path.isabs(pptx_path):
890
+ pptx_path = os.path.abspath(pptx_path)
891
+
892
+ # 处理输出路径,如果未指定则使用标准输出目录
893
+ if not output_path:
894
+ # 从原始文件名生成输出文件名
895
+ base_name = os.path.splitext(os.path.basename(pptx_path))[0]
896
+ # 清理文件名
897
+ base_name = cleanup_filename(base_name)
898
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
899
+ output_path = os.path.join(get_output_dir(), f"{base_name}_inserted_{timestamp}.pptx")
900
+
901
+ if output_path and not os.path.isabs(output_path):
902
+ output_path = os.path.abspath(output_path)
903
+
904
+ # 如果提供了输出路径,确保其父目录存在
905
+ if output_path:
906
+ output_dir = os.path.dirname(output_path)
907
+ if not os.path.exists(output_dir):
908
+ try:
909
+ os.makedirs(output_dir, exist_ok=True)
910
+ except Exception as e:
911
+ return f"创建输出目录 {output_dir} 时出错: {e}"
912
+
913
+ # 检查文件是否存在
914
+ file_exists = os.path.exists(pptx_path)
915
+ if not file_exists and not create_if_not_exists:
916
+ return f"错误:PPTX文件 {pptx_path} 不存在,且未启用自动创建"
917
+
918
+ # 使用python-pptx加载或创建演示文稿
919
+ from pptx import Presentation
920
+ prs = Presentation(pptx_path) if file_exists else Presentation()
921
+
922
+ # 如果是新创建的演示文稿,设置为16:9尺寸
923
+ if not file_exists:
924
+ prs.slide_width = Inches(16)
925
+ prs.slide_height = Inches(9)
926
+
927
+ # 检查布局索引是否有效
928
+ if layout_index >= len(prs.slide_layouts):
929
+ return f"错误:无效的布局索引 {layout_index},可用范围 [0, {len(prs.slide_layouts)-1}]"
930
+
931
+ # 检查幻灯片编号范围
932
+ slides_count = len(prs.slides)
933
+ if not 1 <= slide_number <= slides_count + 1:
934
+ return f"错误:幻灯片位置 {slide_number} 超出范围 [1, {slides_count + 1}]"
935
+
936
+ # 计算索引(转换为从0开始)
937
+ slide_index = slide_number - 1
938
+
939
+ # 在末尾添加新幻灯片
940
+ new_slide = prs.slides.add_slide(prs.slide_layouts[layout_index])
941
+
942
+ # 如果不是添加到末尾,需要移动幻灯片
943
+ if slide_index < slides_count:
944
+ # 获取幻灯片列表
945
+ slides = list(prs.slides._sldIdLst)
946
+ # 将最后一张幻灯片(刚添加的)移动到目标位置
947
+ last_slide = slides[-1]
948
+ # 从列表中移除最后一张幻灯片
949
+ prs.slides._sldIdLst.remove(last_slide)
950
+ # 在目标位置插入幻灯片
951
+ prs.slides._sldIdLst.insert(slide_index, last_slide)
952
+
953
+ # 保存文件
954
+ save_path = output_path
955
+ prs.save(save_path)
956
+
957
+ # 构建返回消息
958
+ action = "添加" if file_exists else "创建并添加"
959
+ return f"成功在 {pptx_path} 中{action}第 {slide_number} 张幻灯片,结果已保存到 {save_path}"
960
+
961
+ except Exception as e:
962
+ error_trace = traceback.format_exc()
963
+ return f"插入幻灯片时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}"
964
+
965
+ @mcp.tool()
966
+ def copy_svg_slide(
967
+ source_pptx_path: str,
968
+ target_pptx_path: str = "",
969
+ source_slide_number: int = 1,
970
+ target_slide_number: Optional[int] = None,
971
+ output_path: Optional[str] = None,
972
+ create_if_not_exists: bool = True
973
+ ) -> str:
974
+ """
975
+ 专门用于复制包含SVG图像的幻灯片,确保SVG和相关引用都被正确复制。
976
+
977
+ 此函数使用直接操作PPTX内部XML文件的方式,确保SVG图像及其引用在复制过程中完全保留。
978
+ 与普通的copy_slide函数相比,此函数特别关注SVG图像的复制,保证SVG的矢量属性在复制后依然可用。
979
+
980
+ Args:
981
+ source_pptx_path: 源PPTX文件路径
982
+ target_pptx_path: 目标PPTX文件路径,如果为空则创建新文件
983
+ source_slide_number: 要复制的源幻灯片页码(从1开始)
984
+ target_slide_number: 要插入到目标文件的位置(从1开始),如果为None则添加到末尾
985
+ output_path: 输出文件路径,如果未指定则使用标准输出目录
986
+ create_if_not_exists: 如果为True且目标PPTX文件不存在,将自动创建一个新文件
987
+
988
+ Returns:
989
+ 操作结果消息
990
+ """
991
+ import zipfile
992
+ import tempfile
993
+ import os
994
+ import shutil
995
+ from lxml import etree
996
+ from pptx import Presentation
997
+ from pptx.util import Inches
998
+
999
+ try:
1000
+ # 创建临时目录
1001
+ temp_dir = tempfile.mkdtemp()
1002
+ source_extract_dir = os.path.join(temp_dir, "source")
1003
+ target_extract_dir = os.path.join(temp_dir, "target")
1004
+
1005
+ os.makedirs(source_extract_dir, exist_ok=True)
1006
+ os.makedirs(target_extract_dir, exist_ok=True)
1007
+
1008
+ # 确保源路径是绝对路径
1009
+ if not os.path.isabs(source_pptx_path):
1010
+ source_pptx_path = os.path.abspath(source_pptx_path)
1011
+
1012
+ # 检查源文件是否存在
1013
+ if not os.path.exists(source_pptx_path):
1014
+ return f"错误:源PPTX文件 {source_pptx_path} 不存在"
1015
+
1016
+ # 处理目标路径
1017
+ if not target_pptx_path:
1018
+ # 创建新的目标文件(基于源文件名)
1019
+ base_name = os.path.splitext(os.path.basename(source_pptx_path))[0]
1020
+ base_name = cleanup_filename(base_name)
1021
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
1022
+ target_pptx_path = os.path.join(get_output_dir(), f"{base_name}_copied_{timestamp}.pptx")
1023
+
1024
+ # 确保路径是绝对路径
1025
+ if not os.path.isabs(target_pptx_path):
1026
+ target_pptx_path = os.path.abspath(target_pptx_path)
1027
+
1028
+ # 处理输出路径,如果未指定则使用标准输出目录
1029
+ if not output_path:
1030
+ # 从目标文件名生成输出文件名
1031
+ base_name = os.path.splitext(os.path.basename(target_pptx_path))[0]
1032
+ base_name = cleanup_filename(base_name)
1033
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
1034
+ output_path = os.path.join(get_output_dir(), f"{base_name}_svg_copied_{timestamp}.pptx")
1035
+
1036
+ if output_path and not os.path.isabs(output_path):
1037
+ output_path = os.path.abspath(output_path)
1038
+
1039
+ # 如果提供了输出路径,确保其父目录存在
1040
+ if output_path:
1041
+ output_dir = os.path.dirname(output_path)
1042
+ if not os.path.exists(output_dir):
1043
+ try:
1044
+ os.makedirs(output_dir, exist_ok=True)
1045
+ except Exception as e:
1046
+ return f"创建输出目录 {output_dir} 时出错: {e}"
1047
+
1048
+ # 解压源PPTX文件
1049
+ with zipfile.ZipFile(source_pptx_path, 'r') as zip_ref:
1050
+ zip_ref.extractall(source_extract_dir)
1051
+
1052
+ # 创建新的目标文件或使用现有文件
1053
+ if not os.path.exists(target_pptx_path):
1054
+ if create_if_not_exists:
1055
+ # 创建一个新的PPTX文件
1056
+ prs = Presentation()
1057
+ prs.slide_width = Inches(16)
1058
+ prs.slide_height = Inches(9)
1059
+ prs.save(target_pptx_path)
1060
+ else:
1061
+ return f"错误:目标PPTX文件 {target_pptx_path} 不存在,且未启用自动创建"
1062
+
1063
+ # 解压目标PPTX文件
1064
+ with zipfile.ZipFile(target_pptx_path, 'r') as zip_ref:
1065
+ zip_ref.extractall(target_extract_dir)
1066
+
1067
+ # 加载源演示文稿和目标演示文稿以获取信息
1068
+ source_prs = Presentation(source_pptx_path)
1069
+ target_prs = Presentation(target_pptx_path)
1070
+
1071
+ # 检查源幻灯片编号范围
1072
+ if not 1 <= source_slide_number <= len(source_prs.slides):
1073
+ return f"错误:源幻灯片编号 {source_slide_number} 超出范围 [1, {len(source_prs.slides)}]"
1074
+
1075
+ # 确定目标幻灯片位置
1076
+ target_slides_count = len(target_prs.slides)
1077
+ if target_slide_number is None:
1078
+ # 如果未指定目标位置,添加到末尾
1079
+ target_slide_number = target_slides_count + 1
1080
+
1081
+ # 检查目标位置是否有效
1082
+ if not 1 <= target_slide_number <= target_slides_count + 1:
1083
+ # 如果目标位置超出范围,添加空白幻灯片使其有效
1084
+ blank_slides_to_add = target_slide_number - target_slides_count
1085
+ for _ in range(blank_slides_to_add):
1086
+ target_prs.slides.add_slide(target_prs.slide_layouts[6]) # 6通常是空白布局
1087
+ target_prs.save(target_pptx_path)
1088
+
1089
+ # 重新解压更新后的目标文件
1090
+ shutil.rmtree(target_extract_dir)
1091
+ os.makedirs(target_extract_dir, exist_ok=True)
1092
+ with zipfile.ZipFile(target_pptx_path, 'r') as zip_ref:
1093
+ zip_ref.extractall(target_extract_dir)
1094
+
1095
+ # 复制幻灯片内容
1096
+ source_slide_path = os.path.join(source_extract_dir, "ppt", "slides", f"slide{source_slide_number}.xml")
1097
+ source_rels_path = os.path.join(source_extract_dir, "ppt", "slides", "_rels", f"slide{source_slide_number}.xml.rels")
1098
+
1099
+ target_slide_path = os.path.join(target_extract_dir, "ppt", "slides", f"slide{target_slide_number}.xml")
1100
+ target_rels_path = os.path.join(target_extract_dir, "ppt", "slides", "_rels", f"slide{target_slide_number}.xml.rels")
1101
+
1102
+ # 确保目标目录存在
1103
+ os.makedirs(os.path.dirname(target_slide_path), exist_ok=True)
1104
+ os.makedirs(os.path.dirname(target_rels_path), exist_ok=True)
1105
+
1106
+ # 复制幻灯片XML
1107
+ if os.path.exists(source_slide_path):
1108
+ shutil.copy2(source_slide_path, target_slide_path)
1109
+ print(f"已复制幻灯片XML: {source_slide_path} -> {target_slide_path}")
1110
+ else:
1111
+ print(f"源幻灯片文件不存在: {source_slide_path}")
1112
+ return f"错误:源幻灯片文件不存在: {source_slide_path}"
1113
+
1114
+ # 复制关系文件
1115
+ svg_files = []
1116
+ png_files = []
1117
+
1118
+ if os.path.exists(source_rels_path):
1119
+ shutil.copy2(source_rels_path, target_rels_path)
1120
+ print(f"已复制幻灯片关系文件: {source_rels_path} -> {target_rels_path}")
1121
+
1122
+ # 查找并复制所有媒体文件
1123
+ try:
1124
+ parser = etree.XMLParser(remove_blank_text=True)
1125
+ rels_tree = etree.parse(source_rels_path, parser)
1126
+ rels_root = rels_tree.getroot()
1127
+
1128
+ for rel in rels_root.findall("{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"):
1129
+ target = rel.get("Target")
1130
+ if target and "../media/" in target:
1131
+ # 提取媒体文件名
1132
+ media_file = os.path.basename(target)
1133
+ source_media_path = os.path.join(source_extract_dir, "ppt", "media", media_file)
1134
+ target_media_path = os.path.join(target_extract_dir, "ppt", "media", media_file)
1135
+
1136
+ # 确保目标媒体目录存在
1137
+ os.makedirs(os.path.dirname(target_media_path), exist_ok=True)
1138
+
1139
+ # 复制媒体文件
1140
+ if os.path.exists(source_media_path):
1141
+ shutil.copy2(source_media_path, target_media_path)
1142
+ print(f"已复制媒体文件: {source_media_path} -> {target_media_path}")
1143
+
1144
+ # 检查是否为SVG或PNG文件
1145
+ if media_file.lower().endswith(".svg"):
1146
+ svg_files.append(media_file)
1147
+ elif media_file.lower().endswith(".png"):
1148
+ png_files.append(media_file)
1149
+ else:
1150
+ print(f"源媒体文件不存在: {source_media_path}")
1151
+ except Exception as e:
1152
+ print(f"处理关系文件时出错: {e}")
1153
+ import traceback
1154
+ print(traceback.format_exc())
1155
+ else:
1156
+ print(f"源关系文件不存在: {source_rels_path}")
1157
+ return f"错误:源关系文件不存在: {source_rels_path}"
1158
+
1159
+ # 处理[Content_Types].xml文件以支持SVG
1160
+ if svg_files:
1161
+ print(f"发现SVG文件: {svg_files}")
1162
+ content_types_path = os.path.join(target_extract_dir, "[Content_Types].xml")
1163
+
1164
+ if os.path.exists(content_types_path):
1165
+ try:
1166
+ parser = etree.XMLParser(remove_blank_text=True)
1167
+ content_types_tree = etree.parse(content_types_path, parser)
1168
+ content_types_root = content_types_tree.getroot()
1169
+
1170
+ # 检查是否已经存在SVG类型
1171
+ svg_exists = False
1172
+ for elem in content_types_root.findall("Default"):
1173
+ if elem.get("Extension") == "svg":
1174
+ svg_exists = True
1175
+ break
1176
+
1177
+ # 如果不存在,添加SVG类型
1178
+ if not svg_exists:
1179
+ print("添加SVG Content Type到[Content_Types].xml")
1180
+ etree.SubElement(
1181
+ content_types_root,
1182
+ "Default",
1183
+ Extension="svg",
1184
+ ContentType="image/svg+xml"
1185
+ )
1186
+
1187
+ # 保存修改后的Content Types文件
1188
+ content_types_tree.write(
1189
+ content_types_path,
1190
+ xml_declaration=True,
1191
+ encoding='UTF-8',
1192
+ standalone="yes"
1193
+ )
1194
+ except Exception as e:
1195
+ print(f"更新Content Types时出错: {e}")
1196
+ return f"错误:更新Content Types时出错: {e}"
1197
+
1198
+ # 处理presentation.xml以添加幻灯片引用
1199
+ # 从目标文件读取presentation.xml
1200
+ pres_path = os.path.join(target_extract_dir, "ppt", "presentation.xml")
1201
+ pres_rels_path = os.path.join(target_extract_dir, "ppt", "_rels", "presentation.xml.rels")
1202
+
1203
+ # 更新presentation.xml.rels以添加幻灯片引用
1204
+ if os.path.exists(pres_rels_path):
1205
+ try:
1206
+ parser = etree.XMLParser(remove_blank_text=True)
1207
+ pres_rels_tree = etree.parse(pres_rels_path, parser)
1208
+ pres_rels_root = pres_rels_tree.getroot()
1209
+
1210
+ # 查找最大的rId
1211
+ max_rid = 0
1212
+ slide_rels = []
1213
+
1214
+ for rel in pres_rels_root.findall("{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"):
1215
+ rid = rel.get("Id", "")
1216
+ if rid.startswith("rId"):
1217
+ try:
1218
+ rid_num = int(rid[3:])
1219
+ if rid_num > max_rid:
1220
+ max_rid = rid_num
1221
+ except ValueError:
1222
+ pass
1223
+
1224
+ # 检查是否是幻灯片关系
1225
+ if rel.get("Type") == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide":
1226
+ slide_rels.append(rel)
1227
+
1228
+ # 检查目标幻灯片编号的关系是否已存在
1229
+ slide_rel_exists = False
1230
+ target_slide_path_rel = f"slides/slide{target_slide_number}.xml"
1231
+
1232
+ for rel in slide_rels:
1233
+ if rel.get("Target") == target_slide_path_rel:
1234
+ slide_rel_exists = True
1235
+ break
1236
+
1237
+ # 如果需要,添加新的关系
1238
+ if not slide_rel_exists:
1239
+ new_rid = f"rId{max_rid + 1}"
1240
+ new_rel = etree.SubElement(
1241
+ pres_rels_root,
1242
+ "{http://schemas.openxmlformats.org/package/2006/relationships}Relationship",
1243
+ Id=new_rid,
1244
+ Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
1245
+ Target=target_slide_path_rel
1246
+ )
1247
+
1248
+ # 保存修改后的关系文件
1249
+ pres_rels_tree.write(
1250
+ pres_rels_path,
1251
+ xml_declaration=True,
1252
+ encoding='UTF-8',
1253
+ standalone="yes"
1254
+ )
1255
+
1256
+ # 更新presentation.xml中的幻灯片列表
1257
+ if os.path.exists(pres_path):
1258
+ try:
1259
+ pres_tree = etree.parse(pres_path, parser)
1260
+ pres_root = pres_tree.getroot()
1261
+
1262
+ # 查找sldIdLst元素
1263
+ sld_id_lst = pres_root.find(".//{http://schemas.openxmlformats.org/presentationml/2006/main}sldIdLst")
1264
+
1265
+ if sld_id_lst is not None:
1266
+ # 查找最大的幻灯片ID
1267
+ max_sld_id = 256 # 幻灯片ID通常从256开始
1268
+ for sld_id in sld_id_lst.findall(".//{http://schemas.openxmlformats.org/presentationml/2006/main}sldId"):
1269
+ try:
1270
+ id_val = int(sld_id.get("id"))
1271
+ if id_val > max_sld_id:
1272
+ max_sld_id = id_val
1273
+ except (ValueError, TypeError):
1274
+ pass
1275
+
1276
+ # 添加新的幻灯片引用
1277
+ new_sld_id = etree.SubElement(
1278
+ sld_id_lst,
1279
+ "{http://schemas.openxmlformats.org/presentationml/2006/main}sldId",
1280
+ id=str(max_sld_id + 1),
1281
+ **{"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id": new_rid}
1282
+ )
1283
+
1284
+ # 保存修改后的presentation.xml
1285
+ pres_tree.write(
1286
+ pres_path,
1287
+ xml_declaration=True,
1288
+ encoding='UTF-8',
1289
+ standalone="yes"
1290
+ )
1291
+ except Exception as e:
1292
+ print(f"更新presentation.xml时出错: {e}")
1293
+ except Exception as e:
1294
+ print(f"更新presentation.xml.rels时出错: {e}")
1295
+
1296
+ # 重新打包PPTX文件
1297
+ save_path = output_path or target_pptx_path
1298
+ if os.path.exists(save_path):
1299
+ os.remove(save_path)
1300
+
1301
+ with zipfile.ZipFile(save_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf:
1302
+ for root, _, files in os.walk(target_extract_dir):
1303
+ for file in files:
1304
+ file_path = os.path.join(root, file)
1305
+ arcname = os.path.relpath(file_path, target_extract_dir)
1306
+ zipf.write(file_path, arcname)
1307
+
1308
+ # 清理临时目录
1309
+ shutil.rmtree(temp_dir)
1310
+
1311
+ # 返回成功消息
1312
+ svg_count = len(svg_files)
1313
+ svg_info = f",包含{svg_count}个SVG图像" if svg_count > 0 else ""
1314
+ return f"成功将幻灯片从 {source_pptx_path} 的第 {source_slide_number} 页复制到 {save_path} 的第 {target_slide_number} 页{svg_info}"
1315
+
1316
+ except Exception as e:
1317
+ # 清理临时目录
1318
+ if 'temp_dir' in locals() and os.path.exists(temp_dir):
1319
+ shutil.rmtree(temp_dir)
1320
+
1321
+ error_trace = traceback.format_exc()
1322
+ return f"复制SVG幻灯片时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}"
1323
+
1324
+ @mcp.tool(description="使用SVG设计宗师角色提示,当用户希望大模型生成或优化ppt时,使用此prompt对用户需求进行细化")
1325
+ def svg_prompt(source: str) -> str:
1326
+ """
1327
+ 使用SVG设计宗师角色提示,并将用户具体需求嵌入其中。
1328
+ 主要用途:当用户希望大模型生成、优化ppt时,使用此prompt对用户需求进行细化。
1329
+ 该工具会返回优化后的需求prompt,你需要认真阅读该函数返回的str,将其作为你的设计理念,而不是仅作为一个普通的输出结果。
1330
+ 结合优化后的需求自主判断调用什么工具来满足用户生成ppt的需求。
1331
+ 例如你可以:
1332
+ 1、生成svg代码后[自动调用]okppt工具的save_svg方法将svg代码保存到本地,
1333
+ 2、然后调用okppt的insert_svg工具将svg代码全屏插入ppt,达到设计ppt的效果。
1334
+ 输入:
1335
+ source: str, 用户希望大模型生成的ppt的结构、内容或主题等相关需求。
1336
+ 输出:
1337
+ str, 包含用户具体需求的、完整的“SVG设计宗师”架构化提示词,大模型可直接使用该提示词生成高质量svg代码。
1338
+ """
1339
+
1340
+ user_demand_snippet = f"""## 0. 当前核心设计任务 (User's Core Design Task)
1341
+
1342
+ 用户提供的核心需求如下:
1343
+
1344
+ ```text
1345
+ {source}
1346
+ ```
1347
+
1348
+ 请 SVG 设计宗师基于以上用户需求,并严格遵循后续的完整 Prompt 框架(角色定位、设计原则、内容理解、决策框架等)进行分析、设计并生成最终的SVG代码。
1349
+ 在开始具体设计前,请先在“阶段一:深度聆听与精准解构”中,确认你对以上核心设计任务的理解。
1350
+ """
1351
+
1352
+ # 使用占位符替换用户需求
1353
+ if "%%%USER_CORE_DESIGN_TASK_HERE%%%" in PROMPT_TEMPLATE_CONTENT:
1354
+ final_prompt = PROMPT_TEMPLATE_CONTENT.replace("%%%USER_CORE_DESIGN_TASK_HERE%%%", user_demand_snippet)
1355
+ else:
1356
+ # 如果模板中没有找到占位符,作为备选方案,仍在最前面添加
1357
+ # 或者可以返回一个错误/警告,表明模板可能已损坏或不是预期版本
1358
+ print(f"警告:占位符 '%%%USER_CORE_DESIGN_TASK_HERE%%%' 未在模板 '{PROMPT_TEMPLATE_CONTENT}' 中找到。用户需求将添加到Prompt开头。")
1359
+ final_prompt = f"{PROMPT_TEMPLATE_CONTENT}\n\n用户的需求是:{user_demand_snippet}"
1360
+
1361
+ return final_prompt
1362
+
1363
+ # --- New PPT Operation Tools ---
1364
+
1365
+ @mcp.tool(description="Analyzes the layout details of a PowerPoint presentation and returns a JSON string of the analysis.")
1366
+ def analyze_presentation_layouts(prs_path: str, title: str = "演示文稿") -> str:
1367
+ """
1368
+ 如果用户希望使用已有模板进行幻灯片创作,首先使用此工具进行母版分析。
1369
+ 分析指定PowerPoint演示文稿的布局详细信息并以JSON字符串形式返回。
1370
+
1371
+ 此工具旨在提供对PPTX文件内部结构的全面视图,帮助用户了解可用的母版、
1372
+ 布局及其名称、每个布局包含的占位符类型和访问ID。同时,它还统计各类布局的数量,
1373
+ 分析实际幻灯片对这些布局的使用情况,找出未被使用的布局,并计算整体的布局利用率。
1374
+ 这些信息对于后续通过编程方式精确操作或修改演示文稿至关重要。
1375
+
1376
+ Args:
1377
+ prs_path (str): 需要进行分析的PowerPoint (.pptx) 文件的路径。
1378
+ 可以是绝对路径或相对于服务工作目录的相对路径。
1379
+ title (str, optional): 用户为本次分析任务指定的标题,此标题会包含在返回的
1380
+ JSON结果中,便于用户识别。默认为 "演示文稿"。
1381
+
1382
+ Returns:
1383
+ str: 一个JSON格式的字符串,其中包含了对演示文稿布局的详细分析数据。
1384
+ 成功时,JSON结构将包含 "status": "success" 以及 "data" 字段中的具体分析信息,例如:
1385
+ {
1386
+ "status": "success",
1387
+ "message": "Presentation analysis successful.",
1388
+ "data": {
1389
+ "presentation_path": "路径/到/文件.pptx",
1390
+ "analysis_title": "用户指定的标题",
1391
+ "slide_count": 5, // 总幻灯片数
1392
+ "master_count": 1, // 总母版数
1393
+ "total_layouts_count": 8, // 总布局数
1394
+ "layout_type_stats": { // 各类型布局统计
1395
+ "自定义布局": {"count": 3, "percentage": 37.5},
1396
+ "系统布局": {"count": 5, "percentage": 62.5}
1397
+ },
1398
+ "masters_details": [ // 母版详情列表
1399
+ {
1400
+ "master_index": 1,
1401
+ "master_name": "Office 主题",
1402
+ "layout_count": 8,
1403
+ "layouts": [ // 该母版下的布局列表
1404
+ {
1405
+ "layout_index": 1,
1406
+ "layout_name_original": "标题幻灯片",
1407
+ "layout_display_name": "自定义布局 - 标题幻灯片",
1408
+ "layout_type": "自定义布局",
1409
+ "placeholder_count": 2,
1410
+ "placeholders": [ // 该布局下的占位符列表
1411
+ {"placeholder_index": 1, "type_name": "标题 (Title)", "access_id": 0, "type_code": 1}, // "access_id" 可用于其他工具如 set_placeholder_value 定位此占位符
1412
+ {"placeholder_index": 2, "type_name": "副标题 (Subtitle)", "access_id": 1, "type_code": 4}
1413
+ ]
1414
+ }
1415
+ // ...更多布局...
1416
+ ]
1417
+ }
1418
+ ],
1419
+ "slide_layout_usage_summary": { // 各布局在幻灯片中的使用次数统计
1420
+ "标题幻灯片": {"count": 1, "percentage": 20.0}
1421
+ },
1422
+ "slides_details": [ // 各幻灯片使用的布局信息
1423
+ {"slide_number": 1, "title": "幻灯片1标题", "used_layout_name_original": "标题幻灯片", ...}
1424
+ ],
1425
+ "unused_layouts_summary": [ // 未被使用的布局列表
1426
+ {"name": "内容与标题", "type": "自定义布局"}
1427
+ ],
1428
+ "layout_utilization": { // 整体布局利用率
1429
+ "total_available": 8, "used_count": 3, "utilization_rate_percentage": 37.5
1430
+ }
1431
+ },
1432
+ "output_path": null // 此操作不生成文件,故为null
1433
+ }
1434
+ 若操作失败(例如文件不存在或文件格式错误),JSON结构将包含 "status": "error" 及错误信息:
1435
+ {
1436
+ "status": "error",
1437
+ "message": "分析布局详情失败: 文件 'non_existent.pptx' 未找到.",
1438
+ "data": { ... 可能包含部分已收集的数据 ... },
1439
+ "output_path": null
1440
+ }
1441
+ """
1442
+ logger.info(f"Executing analyze_presentation_layouts for: {prs_path} with title: {title}") # Added title to log
1443
+ result_dict = analyze_layout_details(prs_path, title)
1444
+ return json.dumps(result_dict, ensure_ascii=False, indent=2)
1445
+
1446
+ @mcp.tool(description="Inserts a new slide with a specified layout into a presentation and returns a JSON string of the result.")
1447
+ def add_slide_with_layout(prs_path: str, layout_name: str, output_path: Optional[str] = None, slide_title: Optional[str] = None) -> str:
1448
+ """
1449
+ 如果用户希望使用已有模板进行幻灯片创作,必须使用此工具。
1450
+ 在指定的PowerPoint演示文稿中根据布局名称插入一张新的幻灯片。
1451
+
1452
+ 此函数首先会查找演示文稿中所有可用的布局,然后根据提供的 `layout_name`
1453
+ (该名称通常通过 `analyze_presentation_layouts` 工具获取)添加新幻灯片。
1454
+ 可以选择为新幻灯片设置标题(如果所选布局包含标题占位符)。
1455
+ 操作结果(包括成功状态、消息、新幻灯片总数和输出文件路径)将以JSON字符串形式返回。
1456
+
1457
+ Args:
1458
+ prs_path (str): 源PPTX文件的绝对或相对路径。
1459
+ layout_name (str): 要使用的新幻灯片的布局名称 (例如 "标题幻灯片", "空白" 等)。
1460
+ 建议使用 `analyze_presentation_layouts` 工具获取准确的可用布局名称。
1461
+ output_path (Optional[str], optional): 修改后PPTX的输出文件路径。
1462
+ 如果为None,则会在标准输出目录下自动生成一个文件名,
1463
+ 格式通常为 `[原文件名]_inserted_layout_[布局名]_[时间戳].pptx`。
1464
+ 默认为 None。
1465
+ slide_title (Optional[str], optional): 要赋给新幻灯片标题占位符的文本。
1466
+ 如果布局没有标题占位符或此参数为None,则不设置标题。
1467
+ 默认为 None。
1468
+
1469
+ Returns:
1470
+ str: 一个JSON格式的字符串,包含了操作结果。
1471
+ 成功时结构示例:
1472
+ {
1473
+ "status": "success",
1474
+ "message": "Successfully inserted slide with layout '布局名'.",
1475
+ "data": {
1476
+ "slides_total": 11, // 操作后总幻灯片数
1477
+ "original_slide_count": 10, // 操作前幻灯片数
1478
+ "new_slide_title_set": "设置的标题" // 如果成功设置了标题
1479
+ },
1480
+ "output_path": "path/to/output_inserted_layout_布局名_timestamp.pptx"
1481
+ }
1482
+ 失败时(例如布局未找到)结构示例:
1483
+ {
1484
+ "status": "error",
1485
+ "message": "Layout '不存在的布局' not found. Available layouts: ...",
1486
+ "data": {"available_layouts": ["布局1", "布局2"], "original_slide_count": 10},
1487
+ "output_path": null
1488
+ }
1489
+ 其他失败情况(例如文件读写错误):
1490
+ {
1491
+ "status": "error",
1492
+ "message": "插入布局失败: [错误描述]",
1493
+ "data": {"original_slide_count": 10},
1494
+ "output_path": null
1495
+ }
1496
+ """
1497
+ logger.info(f"Executing add_slide_with_layout for: {prs_path}, layout: {layout_name}")
1498
+ result_dict = insert_layout(prs_path, layout_name, output_path, slide_title)
1499
+ return json.dumps(result_dict, ensure_ascii=False, indent=2)
1500
+
1501
+ @mcp.tool(description="Clears content from placeholders in specified slides of a presentation and returns a JSON string of the result.")
1502
+ def clear_placeholders_from_slides(prs_path: str, output_path: Optional[str] = None, slide_indices: Optional[List[int]] = None) -> str:
1503
+ """
1504
+ 清空指定PowerPoint演示文稿中特定或所有幻灯片内占位符的文本内容。
1505
+
1506
+ 此函数会遍历指定(或全部)幻灯片上的所有占位符,并尝试清除其文本内容。
1507
+ 操作会保留占位符本身结构,仅移除文本。图片、表格等非文本内容不受影响。
1508
+ 函数返回一个JSON字符串,包含操作结果,如处理的幻灯片数量、清空的占位符总数等。
1509
+
1510
+ Args:
1511
+ prs_path (str): 源PPTX文件的绝对或相对路径。
1512
+ output_path (Optional[str], optional): 修改后PPTX的输出文件路径。
1513
+ 如果为None,则会在标准输出目录下自动生成一个文件名,
1514
+ 格式通常为 `[原文件名]_content_cleared_[时间戳].pptx`。
1515
+ 默认为 None。
1516
+ slide_indices (Optional[List[int]], optional): 一个包含幻灯片索引(0-based)的列表,
1517
+ 指定要处理哪些幻灯片。
1518
+ 如果为None,则处理演示文稿中的所有幻灯片。
1519
+ 默认为 None。
1520
+
1521
+ Returns:
1522
+ str: 一个JSON格式的字符串,包含了操作结果。
1523
+ 成功时结构示例:
1524
+ {
1525
+ "status": "success",
1526
+ "message": "Successfully cleared content from 3 placeholder(s) in 2 slide(s).",
1527
+ "data": {
1528
+ "slides_processed_count": 2, // 实际处理并有内容被清空的幻灯片数量
1529
+ "placeholders_cleared_total": 3, // 所有被清空内容的占位符总数
1530
+ "slides_targetted_count": 2, // 目标处理的幻灯片数量(基于slide_indices或总数)
1531
+ "processed_slides_details": [ // 每个被处理幻灯片的详情
1532
+ {
1533
+ "slide_number": 1, // 幻灯片页码 (1-based)
1534
+ "cleared_count_on_slide": 1, // 该幻灯片上被清空的占位符数量
1535
+ "placeholders_status": [ // 该幻灯片上各占位符的处理状态
1536
+ {"access_id": 0, "type": "标题 (Title)", "cleared": true, "reason": "text_frame cleared"},
1537
+ {"access_id": 1, "type": "正文/内容 (Body)", "cleared": false, "reason": "no text content or not clearable type"}
1538
+ ]
1539
+ }
1540
+ ]
1541
+ },
1542
+ "output_path": "path/to/output_content_cleared_timestamp.pptx"
1543
+ }
1544
+ 失败时(例如文件处理错误)结构示例:
1545
+ {
1546
+ "status": "error",
1547
+ "message": "清空占位符内容失败: [错误描述]",
1548
+ "data": {"placeholders_cleared_total": 0, "slides_processed_count": 0},
1549
+ "output_path": null
1550
+ }
1551
+ """
1552
+ logger.info(f"Executing clear_placeholders_from_slides for: {prs_path}, slide_indices: {slide_indices}")
1553
+ result_dict = clear_placeholder_content(prs_path, output_path, slide_indices)
1554
+ return json.dumps(result_dict, ensure_ascii=False, indent=2)
1555
+
1556
+ @mcp.tool(description="Assigns content to a specific placeholder on a specific slide and returns a JSON string of the result.")
1557
+ def set_placeholder_value(prs_path: str, slide_idx: int, placeholder_id: int, content_to_set: str, output_path: Optional[str] = None) -> str:
1558
+ """
1559
+ 给指定PowerPoint演示文稿中特定幻灯片的特定占位符赋予文本内容。
1560
+
1561
+ 此函数通过幻灯片索引 (0-based) 和占位符的访问ID (placeholder_format.idx)
1562
+ 来定位目标占位符,并将其文本内容设置为用户提供的 `content_to_set`。
1563
+ 主要适用于文本类型的占位符(如标题、正文、副标题等)。
1564
+ 操作结果以JSON字符串形式返回。
1565
+
1566
+ Args:
1567
+ prs_path (str): 源PPTX文件的绝对或相对路径。
1568
+ slide_idx (int): 要修改的幻灯片的索引 (0-based)。
1569
+ placeholder_id (int): 要赋值的目标占位符的访问ID。这个ID即为占位符在其母版布局中定义的唯一 `idx` 值
1570
+ (即 `placeholder_format.idx` 属性值)。
1571
+ **强烈建议通过先调用 `analyze_presentation_layouts` 工具来获取指定幻灯片上确切可用的占位符
1572
+ 及其对应的 `access_id`**(在 `analyze_presentation_layouts` 返回结果的 `data.masters_details.layouts.placeholders.access_id`
1573
+ 或 `data.slides_details.placeholders_on_slide.access_id` 路径下可以找到,尽管后者需要先通过分析工具获取幻灯片上实际有哪些占位符),以确保操作的准确性。
1574
+ content_to_set (str): 要赋给占位符的文本内容。
1575
+ output_path (Optional[str], optional): 修改后PPTX的输出文件路径。
1576
+ 如果为None,则会在标准输出目录下自动生成一个文件名,
1577
+ 格式通常为 `[原文件名]_assigned_S[slide_idx]_P[placeholder_id]_[时间戳].pptx`。
1578
+ 默认为 None。
1579
+
1580
+ Returns:
1581
+ str: 一个JSON格式的字符串,包含了操作结果。
1582
+ 成功时结构示例:
1583
+ {
1584
+ "status": "success",
1585
+ "message": "Successfully assigned content to placeholder ID 0 on slide 1.",
1586
+ "data": {
1587
+ "slide_index": 0, // 目标幻灯片索引 (0-based)
1588
+ "placeholder_access_id": 0, // 目标占位符访问ID
1589
+ "content_assigned_length": 15, // 赋值内容的长度
1590
+ "assignment_method": "text_frame" // 使用的赋值方法
1591
+ },
1592
+ "output_path": "path/to/output_assigned_S0_P0_timestamp.pptx"
1593
+ }
1594
+ 失败时(例如索引超范围、占位符未找到、占位符不支持文本赋值)结构示例:
1595
+ {
1596
+ "status": "error",
1597
+ "message": "在幻灯片 0 上未找到访问ID为 99 的占位符。",
1598
+ "data": {"available_placeholders": [{"access_id": 0, "type": "标题 (Title)"}, ...]}, // 可能包含可用占位符信息
1599
+ "output_path": null
1600
+ }
1601
+ 其他失败情况(例如文件读写错误):
1602
+ {
1603
+ "status": "error",
1604
+ "message": "给占位符赋值时发生严重错误: [错误描述]",
1605
+ "data": null,
1606
+ "output_path": null
1607
+ }
1608
+ """
1609
+ logger.info(f"Executing set_placeholder_value for: {prs_path}, slide: {slide_idx}, placeholder: {placeholder_id}")
1610
+ result_dict = assign_placeholder_content(prs_path, slide_idx, placeholder_id, content_to_set, output_path)
1611
+ return json.dumps(result_dict, ensure_ascii=False, indent=2)
1612
+
1613
+ # 启动服务器
1614
+ if __name__ == "__main__":
1615
+ # 确保必要的目录存在
1616
+ tmp_dir = get_tmp_dir()
1617
+ output_dir = get_output_dir()
1618
+
1619
+ mcp.run(transport='stdio')