1+ from collections .abc import Generator
2+ from typing import Any
3+ import requests
4+ import openpyxl
5+ from openpyxl .utils import get_column_letter
6+ from io import BytesIO
7+
8+ from dify_plugin import Tool
9+ from dify_plugin .entities .tool import ToolInvokeMessage
10+
11+ def get_cell_styles (cell ) -> dict :
12+ """セルのすべてのスタイル情報を取得"""
13+ styles = {}
14+
15+ # セル結合情報の取得
16+ if cell .parent .merged_cells :
17+ for merged_range in cell .parent .merged_cells .ranges :
18+ if cell .coordinate in merged_range :
19+ styles ['merge' ] = {
20+ 'start' : merged_range .coord .split (':' )[0 ],
21+ 'end' : merged_range .coord .split (':' )[1 ]
22+ }
23+ break
24+
25+ # 罫線情報(すべての罫線情報を取得)
26+ if cell .border :
27+ borders = {}
28+ for side in ['top' , 'bottom' , 'left' , 'right' ]:
29+ border = getattr (cell .border , side )
30+ if border and border .style :
31+ borders [side ] = border .style
32+ if borders :
33+ styles ['borders' ] = borders
34+
35+ # 背景色(パターンタイプと色情報を取得)
36+ if cell .fill :
37+ if hasattr (cell .fill , 'patternType' ) and cell .fill .patternType :
38+ if cell .fill .patternType == 'solid' :
39+ if (cell .fill .start_color and
40+ cell .fill .start_color .rgb and
41+ isinstance (cell .fill .start_color .rgb , str )):
42+ # 透明の場合は除外
43+ if cell .fill .start_color .rgb != '00000000' :
44+ styles ['background' ] = cell .fill .start_color .rgb
45+
46+ # 文字色(すべての文字色を取得)
47+ if (cell .font and cell .font .color and
48+ cell .font .color .rgb and
49+ isinstance (cell .font .color .rgb , str )):
50+ styles ['color' ] = cell .font .color .rgb
51+
52+ return styles
53+
54+ def create_xml_with_styles (wb ) -> str :
55+ """ワークシートの内容とすべてのスタイル情報をXMLに変換"""
56+ ws = wb .active
57+
58+ # 結合セルの範囲を保存
59+ merged_cells_ranges = ws .merged_cells .ranges .copy ()
60+
61+ # 結合セルを一時的に解除
62+ for merged_cell_range in merged_cells_ranges :
63+ ws .unmerge_cells (str (merged_cell_range ))
64+
65+ xml_parts = ['<?xml version="1.0" encoding="UTF-8"?>\n <workbook>' ]
66+ xml_parts .append (' <worksheet>' )
67+
68+ # すべての列幅情報を追加
69+ for col_letter , col in ws .column_dimensions .items ():
70+ if hasattr (col , 'width' ) and col .width is not None :
71+ width = float (col .width )
72+ xml_parts .append (f' <column letter="{ col_letter } " width="{ width :.2f} "/>' )
73+
74+ # 結合セルを再設定
75+ for merged_cell_range in merged_cells_ranges :
76+ ws .merge_cells (str (merged_cell_range ))
77+
78+ # すべての行高さ情報を追加
79+ for row_idx , row in ws .row_dimensions .items ():
80+ if row .height :
81+ xml_parts .append (f' <row index="{ row_idx } " height="{ row .height :.2f} "/>' )
82+
83+ # データ範囲を取得
84+ data_rows = list (ws .rows )
85+ if not data_rows :
86+ return "<workbook></workbook>"
87+
88+ # セルの値をXMLエスケープする関数
89+ def escape_xml (text ):
90+ if not isinstance (text , str ):
91+ text = str (text )
92+ return (text .replace ('&' , '&' )
93+ .replace ('<' , '<' )
94+ .replace ('>' , '>' )
95+ .replace ('"' , '"' )
96+ .replace ("'" , ''' ))
97+
98+ # すべてのセル情報を処理
99+ for row in data_rows :
100+ row_idx = row [0 ].row
101+ xml_parts .append (f' <row index="{ row_idx } ">' )
102+
103+ for cell in row :
104+ # 値の有無に関わらずすべてのセルを出力
105+ xml_parts .append (f' <cell ref="{ cell .coordinate } ">' )
106+
107+ if cell .value is not None :
108+ value = escape_xml (cell .value )
109+ xml_parts .append (f' <value>{ value } </value>' )
110+
111+ # スタイル情報を追加
112+ styles = get_cell_styles (cell )
113+ if styles :
114+ for style_name , style_value in styles .items ():
115+ if style_name == 'merge' :
116+ xml_parts .append (' <merge>' )
117+ xml_parts .append (f' <start>{ style_value ["start" ]} </start>' )
118+ xml_parts .append (f' <end>{ style_value ["end" ]} </end>' )
119+ xml_parts .append (' </merge>' )
120+ elif style_name == 'borders' :
121+ xml_parts .append (' <borders>' )
122+ for border_side , border_style in style_value .items ():
123+ xml_parts .append (f' <border side="{ border_side } " style="{ border_style } "/>' )
124+ xml_parts .append (' </borders>' )
125+ else :
126+ xml_parts .append (f' <{ style_name } >{ style_value } </{ style_name } >' )
127+
128+ xml_parts .append (' </cell>' )
129+
130+ xml_parts .append (' </row>' )
131+
132+ xml_parts .append (' </worksheet>' )
133+ xml_parts .append ('</workbook>' )
134+ return '\n ' .join (xml_parts )
135+
136+ def get_url_from_file_data (file_data : Any ) -> str :
137+ """ファイルデータからURLを抽出する"""
138+ if isinstance (file_data , str ):
139+ # 直接URLが渡された場合
140+ return file_data
141+ elif isinstance (file_data , list ) and len (file_data ) > 0 :
142+ # 配列形式で渡された場合
143+ first_item = file_data [0 ]
144+ if isinstance (first_item , dict ) and 'url' in first_item :
145+ return first_item ['url' ]
146+ elif isinstance (file_data , dict ) and 'url' in file_data :
147+ # 辞書形式で渡された場合
148+ return file_data ['url' ]
149+ elif hasattr (file_data , 'url' ):
150+ # Fileオブジェクトの場合
151+ return file_data .url
152+ return None
153+
154+ class DifyPluginExcelToXMLTool (Tool ):
155+ def _invoke (self , tool_parameters : dict [str , Any ]) -> Generator [ToolInvokeMessage , None , None ]:
156+
157+ # ファイルオブジェクトを取得
158+ file_data = tool_parameters .get ("file_url" )
159+
160+ if not file_data :
161+ yield ToolInvokeMessage (
162+ type = "text" ,
163+ message = {"text" : "ファイルが提供されていません。" }
164+ )
165+ return
166+
167+ try :
168+ # ファイルのURLを取得
169+ file_url = get_url_from_file_data (file_data )
170+
171+ if not file_url :
172+ yield ToolInvokeMessage (
173+ type = "text" ,
174+ message = {"text" : "ファイルのURLが見つかりません。" }
175+ )
176+ return
177+
178+ # ファイルをダウンロード
179+ try :
180+ response = requests .get (file_url )
181+ response .raise_for_status ()
182+ file_content = response .content
183+ except Exception as e :
184+ yield ToolInvokeMessage (
185+ type = "text" ,
186+ message = {"text" : f"ファイルのダウンロードに失敗しました: { str (e )} " }
187+ )
188+ return
189+
190+ # ExcelファイルをOpenpyxlで読み込む
191+ excel_data = BytesIO (file_content )
192+ wb = openpyxl .load_workbook (excel_data )
193+
194+ # XMLに変換(罫線情報を含む)
195+ xml_data = create_xml_with_styles (wb )
196+
197+ # XML形式のテキストを返す
198+ yield ToolInvokeMessage (
199+ type = "text" ,
200+ message = {"text" : xml_data }
201+ )
202+
203+ except Exception as e :
204+ yield ToolInvokeMessage (
205+ type = "text" ,
206+ message = {"text" : f"エラーが発生しました: { str (e )} " }
207+ )
208+ return
0 commit comments