Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
ask_data_ai_admin
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
linyangyang
ask_data_ai_admin
Commits
234f9289
Commit
234f9289
authored
Jul 21, 2025
by
林洋洋
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
添加使用工具接口
parent
725a962d
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
242 additions
and
267 deletions
+242
-267
pom.xml
ask-data-ai/ask-data-ai-biz/pom.xml
+6
-1
ChatController.java
...-biz/src/main/java/com/ask/controller/ChatController.java
+1
-15
ExcelTools.java
...k-data-ai-biz/src/main/java/com/ask/tools/ExcelTools.java
+58
-4
ConvertUtils.java
...data-ai-biz/src/main/java/com/ask/utils/ConvertUtils.java
+78
-0
SpringContextUtils.java
...i-biz/src/main/java/com/ask/utils/SpringContextUtils.java
+85
-0
WordUtil.java
...ask-data-ai-biz/src/main/java/com/ask/utils/WordUtil.java
+0
-246
FileTemplate.java
...ommon/src/main/java/com/ask/common/core/FileTemplate.java
+5
-0
LocalFileTemplate.java
...src/main/java/com/ask/common/local/LocalFileTemplate.java
+6
-0
pom.xml
ask-data-ai/pom.xml
+3
-1
No files found.
ask-data-ai/ask-data-ai-biz/pom.xml
View file @
234f9289
...
...
@@ -119,7 +119,12 @@
<dependency>
<groupId>
org.apache.poi
</groupId>
<artifactId>
poi-ooxml
</artifactId>
<version>
5.2.5
</version>
<version>
5.2.2
</version>
</dependency>
<dependency>
<groupId>
com.deepoove
</groupId>
<artifactId>
poi-tl
</artifactId>
<version>
1.12.1
</version>
</dependency>
</dependencies>
<repositories>
...
...
ask-data-ai/ask-data-ai-biz/src/main/java/com/ask/controller/ChatController.java
View file @
234f9289
...
...
@@ -169,24 +169,10 @@ public class ChatController {
Message
userMessage
=
new
UserMessage
(
"问题:"
+
message
+
"\n回答要求:请使用markdown格式输出"
);
Prompt
prompt
=
new
Prompt
(
List
.
of
(
systemMessage
,
userMessage
));
// ChatResponse chatResponse = openAiChatClient.prompt(prompt)
// .advisors(messageChatMemoryAdvisor)
// .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
// .tools(new ExcelTools())
// // 关闭内部自动执行
// .options(
// ToolCallingChatOptions.builder()
// .internalToolExecutionEnabled(false) // ← 关键
// .build()
// )
// .call().chatResponse();
return
openAiChatClient
.
prompt
(
prompt
)
.
advisors
(
messageChatMemoryAdvisor
)
.
advisors
(
a
->
a
.
param
(
ChatMemory
.
CONVERSATION_ID
,
conversationId
))
.
tools
(
new
ExcelTools
()
)
.
tools
(
excelTools
)
.
call
()
.
content
();
...
...
ask-data-ai/ask-data-ai-biz/src/main/java/com/ask/tools/ExcelTools.java
View file @
234f9289
package
com
.
ask
.
tools
;
import
cn.hutool.json.JSONObject
;
import
com.ask.utils.ConvertUtils
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.ai.tool.annotation.Tool
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.context.i18n.LocaleContextHolder
;
import
org.springframework.stereotype.Component
;
import
java.sql.Date
;
import
java.text.SimpleDateFormat
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
import
java.time.format.DateTimeFormatter
;
import
java.time.format.DateTimeFormatterBuilder
;
import
java.time.format.SignStyle
;
import
java.time.temporal.ChronoField
;
import
java.util.HashMap
;
import
java.util.Map
;
@Component
@Slf4j
public
class
ExcelTools
{
@Tool
(
description
=
"Get the current date and time in the user's timezone"
)
String
getCurrentDateTime
()
{
log
.
info
(
"调用getCurrentDateTime"
);
return
LocalDateTime
.
now
().
atZone
(
LocaleContextHolder
.
getTimeZone
().
toZoneId
()).
toString
();
@Autowired
private
ConvertUtils
convertUtils
;
private
String
formatLocalDate
(
LocalDate
date
)
{
// 创建自定义格式器:2位年份.单/双位月.单/双位日
DateTimeFormatter
formatter
=
new
DateTimeFormatterBuilder
()
// 年份取后两位(2025 -> 25)
.
appendValueReduced
(
ChronoField
.
YEAR
,
2
,
2
,
2000
)
.
appendLiteral
(
'.'
)
// 月份不补零(1-12)
.
appendValue
(
ChronoField
.
MONTH_OF_YEAR
,
1
,
2
,
SignStyle
.
NEVER
)
.
appendLiteral
(
'.'
)
// 日期不补零(1-31)
.
appendValue
(
ChronoField
.
DAY_OF_MONTH
,
1
,
2
,
SignStyle
.
NEVER
)
.
toFormatter
();
return
date
.
format
(
formatter
);
}
@Tool
(
description
=
"Get the production records word"
)
public
String
getProductionFile
(
String
dateStr
)
{
log
.
info
(
"getProductionFile{}"
,
dateStr
);
DateTimeFormatter
formatter
=
DateTimeFormatter
.
ofPattern
(
"yyyy-M-d"
);
LocalDate
date
=
LocalDate
.
parse
(
dateStr
,
formatter
);
String
dateFileStr
=
formatLocalDate
(
date
);
// 动态生成文件名
String
fileName
=
String
.
format
(
"科环集团电力运营日报(%s).docx"
,
dateFileStr
);
Map
<
String
,
Object
>
paramMap
=
new
HashMap
<
String
,
Object
>();
paramMap
.
put
(
"year"
,
date
.
getYear
());
paramMap
.
put
(
"month"
,
date
.
getMonthValue
());
paramMap
.
put
(
"day"
,
date
.
getDayOfMonth
());
String
bucketName
=
"report/production"
;
boolean
result
=
convertUtils
.
fillWordLoop
(
fileName
,
"科环集团电力运营日报模板.docx"
,
paramMap
,
bucketName
);
JSONObject
jsonObject
=
new
JSONObject
();
if
(
result
){
jsonObject
.
set
(
"success"
,
true
);
jsonObject
.
set
(
"fileName"
,
fileName
);
jsonObject
.
set
(
"filePath"
,
"/admin/"
+
bucketName
+
"/"
+
fileName
);
}
else
{
jsonObject
.
set
(
"success"
,
false
);
jsonObject
.
set
(
"fileName"
,
""
);
jsonObject
.
set
(
"filePath"
,
""
);
}
return
jsonObject
.
toString
();
}
}
ask-data-ai/ask-data-ai-biz/src/main/java/com/ask/utils/ConvertUtils.java
0 → 100644
View file @
234f9289
package
com
.
ask
.
utils
;
import
cn.hutool.core.io.FileUtil
;
import
cn.hutool.core.util.IdUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.ask.common.core.FileTemplate
;
import
com.deepoove.poi.XWPFTemplate
;
import
com.deepoove.poi.config.Configure
;
import
lombok.Data
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.core.io.Resource
;
import
org.springframework.core.io.ResourceLoader
;
import
org.springframework.stereotype.Component
;
import
java.io.ByteArrayInputStream
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.util.Date
;
import
java.util.Map
;
@Component
@Slf4j
public
class
ConvertUtils
{
@Autowired
private
ResourceLoader
resourceLoader
;
@Autowired
private
FileTemplate
fileTemplate
;
/**
* 填充word并上传到本地OSS
*
* @param templatePath 模板路径
* @param paramMap 参数映射
* @return {@link String} 返回OSS中的文件路径
*/
private
boolean
fillWord
(
String
fileName
,
String
templatePath
,
Map
<
String
,
Object
>
paramMap
,
String
bucketName
)
{
// 载入Word文档
if
(
fileTemplate
.
isFileExists
(
bucketName
,
fileName
))
{
return
true
;
}
Resource
resource
=
resourceLoader
.
getResource
(
"classpath:"
+
templatePath
);
// OSS中的文件路径
try
(
InputStream
resourceStream
=
resource
.
getInputStream
();
XWPFTemplate
template
=
XWPFTemplate
.
compile
(
resourceStream
).
render
(
paramMap
);
ByteArrayOutputStream
byteArrayOutputStream
=
new
ByteArrayOutputStream
())
{
template
.
write
(
byteArrayOutputStream
);
byteArrayOutputStream
.
flush
();
// 直接使用字节数组上传,避免额外的ByteArrayInputStream
byte
[]
content
=
byteArrayOutputStream
.
toByteArray
();
fileTemplate
.
putObject
(
bucketName
,
fileName
,
new
ByteArrayInputStream
(
content
),
""
);
}
catch
(
IOException
e
)
{
// 添加具体异常处理逻辑
log
.
error
(
"文档生成或上传失败: {}"
,
e
.
getMessage
(),
e
);
return
false
;
}
catch
(
Exception
e
)
{
return
false
;
}
return
true
;
}
/**
* 填充单词循环
* 填充
*
* @param templatePath 模板路径
* @param paramMap 参数映射
* @return {@link String}
*/
public
boolean
fillWordLoop
(
String
fileName
,
String
templatePath
,
Map
<
String
,
Object
>
paramMap
,
String
bucketName
)
{
return
fillWord
(
fileName
,
templatePath
,
paramMap
,
bucketName
);
}
}
ask-data-ai/ask-data-ai-biz/src/main/java/com/ask/utils/SpringContextUtils.java
0 → 100644
View file @
234f9289
package
com
.
ask
.
utils
;
import
jakarta.servlet.http.HttpServletRequest
;
import
org.springframework.beans.BeansException
;
import
org.springframework.context.ApplicationContext
;
import
org.springframework.context.ApplicationContextAware
;
import
org.springframework.stereotype.Component
;
import
org.springframework.web.context.request.RequestContextHolder
;
import
org.springframework.web.context.request.ServletRequestAttributes
;
@Component
public
class
SpringContextUtils
implements
ApplicationContextAware
{
/**
* 上下文对象实例
*/
private
static
ApplicationContext
applicationContext
;
@Override
public
void
setApplicationContext
(
ApplicationContext
applicationContext
)
throws
BeansException
{
SpringContextUtils
.
applicationContext
=
applicationContext
;
}
/**
* 获取applicationContext
*
* @return
*/
public
static
ApplicationContext
getApplicationContext
()
{
return
applicationContext
;
}
/**
* 获取HttpServletRequest
*/
public
static
HttpServletRequest
getHttpServletRequest
()
{
return
((
ServletRequestAttributes
)
RequestContextHolder
.
getRequestAttributes
()).
getRequest
();
}
public
static
String
getDomain
(){
HttpServletRequest
request
=
getHttpServletRequest
();
StringBuffer
url
=
request
.
getRequestURL
();
return
url
.
delete
(
url
.
length
()
-
request
.
getRequestURI
().
length
(),
url
.
length
()).
toString
();
}
public
static
String
getOrigin
(){
HttpServletRequest
request
=
getHttpServletRequest
();
return
request
.
getHeader
(
"Origin"
);
}
/**
* 通过name获取 Bean.
*
* @param name
* @return
*/
public
static
Object
getBean
(
String
name
)
{
return
getApplicationContext
().
getBean
(
name
);
}
/**
* 通过class获取Bean.
*
* @param clazz
* @param <T>
* @return
*/
public
static
<
T
>
T
getBean
(
Class
<
T
>
clazz
)
{
return
getApplicationContext
().
getBean
(
clazz
);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public
static
<
T
>
T
getBean
(
String
name
,
Class
<
T
>
clazz
)
{
return
getApplicationContext
().
getBean
(
name
,
clazz
);
}
}
ask-data-ai/ask-data-ai-biz/src/main/java/com/ask/utils/WordUtil.java
deleted
100644 → 0
View file @
725a962d
package
com
.
ask
.
utils
;
import
java.io.File
;
import
java.io.FileOutputStream
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.HashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Map.Entry
;
import
java.util.Set
;
import
org.apache.poi.ooxml.POIXMLDocument
;
import
org.apache.poi.xwpf.usermodel.XWPFDocument
;
import
org.apache.poi.xwpf.usermodel.XWPFParagraph
;
import
org.apache.poi.xwpf.usermodel.XWPFRun
;
import
org.apache.poi.xwpf.usermodel.XWPFTable
;
import
org.apache.poi.xwpf.usermodel.XWPFTableCell
;
import
org.apache.poi.xwpf.usermodel.XWPFTableRow
;
public
class
WordUtil
{
/**
* 根据模板生成新word文档
* 对表格是进行替换以及插入
* @param inputUrl 模板存放地址
* @param outputUrl 新文档存放地址
* @param textMap 需要替换的信息集合
* @param tableList 需要插入的表格信息集合
* @return 成功返回true,失败返回false
*/
public
static
boolean
changWord
(
String
inputUrl
,
String
outputUrl
,
Map
<
String
,
String
>
textMap
,
List
<
String
[]>
tableList
)
{
//模板转换默认成功
boolean
changeFlag
=
true
;
try
{
//获取docx解析对象
XWPFDocument
document
=
new
XWPFDocument
(
POIXMLDocument
.
openPackage
(
inputUrl
));
//解析替换文本段落对象
WordUtil
.
changeText
(
document
,
textMap
);
//解析替换表格对象
WordUtil
.
changeTable
(
document
,
textMap
,
tableList
);
//生成新的word
File
file
=
new
File
(
outputUrl
);
FileOutputStream
stream
=
new
FileOutputStream
(
file
);
document
.
write
(
stream
);
stream
.
close
();
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
changeFlag
=
false
;
}
return
changeFlag
;
}
/**
* 替换段落文本
* @param document docx解析对象
* @param textMap 需要替换的信息集合
*/
public
static
void
changeText
(
XWPFDocument
document
,
Map
<
String
,
String
>
textMap
){
//获取段落集合
List
<
XWPFParagraph
>
paragraphs
=
document
.
getParagraphs
();
for
(
XWPFParagraph
paragraph
:
paragraphs
)
{
//判断此段落时候需要进行替换
String
text
=
paragraph
.
getText
();
System
.
out
.
println
(
text
);
if
(
checkText
(
text
)){
List
<
XWPFRun
>
runs
=
paragraph
.
getRuns
();
for
(
XWPFRun
run
:
runs
)
{
//替换模板原来位置
run
.
setText
(
changeValue
(
run
.
toString
(),
textMap
),
0
);
}
}
}
}
/**
* 替换表格对象方法
* @param document docx解析对象
* @param textMap 需要替换的信息集合
* @param tableList 需要插入的表格信息集合
*/
public
static
void
changeTable
(
XWPFDocument
document
,
Map
<
String
,
String
>
textMap
,
List
<
String
[]>
tableList
){
//获取表格对象集合
List
<
XWPFTable
>
tables
=
document
.
getTables
();
//获取第一个表格 根据实际模板情况 决定去第几个word中的表格
XWPFTable
table
=
tables
.
get
(
0
);
//表格中的内容替换
List
<
XWPFTableRow
>
rows
=
table
.
getRows
();
eachTable
(
rows
,
textMap
);
//遍历表格,并替换模板
//向特定位置 行插入数据
insertTable
(
table
,
tableList
);
}
/**
* 遍历表格
* @param rows 表格行对象
* @param textMap 需要替换的信息集合
*/
public
static
void
eachTable
(
List
<
XWPFTableRow
>
rows
,
Map
<
String
,
String
>
textMap
){
for
(
XWPFTableRow
row
:
rows
)
{
List
<
XWPFTableCell
>
cells
=
row
.
getTableCells
();
for
(
XWPFTableCell
cell
:
cells
)
{
//判断单元格是否需要替换
if
(
checkText
(
cell
.
getText
())){
List
<
XWPFParagraph
>
paragraphs
=
cell
.
getParagraphs
();
for
(
XWPFParagraph
paragraph
:
paragraphs
)
{
List
<
XWPFRun
>
runs
=
paragraph
.
getRuns
();
for
(
XWPFRun
run
:
runs
)
{
run
.
setText
(
changeValue
(
run
.
toString
(),
textMap
),
0
);
}
}
}
}
}
}
/**
* 为表格插入数据,行数不够添加新行
* @param table 需要插入数据的表格
* @param tableList 插入数据集合
*/
public
static
void
insertTable
(
XWPFTable
table
,
List
<
String
[]>
tableList
){
//创建行,根据需要插入的数据添加新行,不处理表头
//判断需要插入的数量
if
(
tableList
.
size
()
>
5
)
{
//我的模板预留了五行
for
(
int
i
=
0
;
i
<
tableList
.
size
()-
4
;
i
++)
{
insertRow
(
table
,
10
,
14
);
}
}
//遍历表格插入数据
List
<
XWPFTableRow
>
rows
=
table
.
getRows
();
for
(
int
i
=
9
;
i
<
tableList
.
size
()
+
9
;
i
++){
XWPFTableRow
newRow
=
table
.
getRow
(
i
);
List
<
XWPFTableCell
>
cells
=
newRow
.
getTableCells
();
for
(
int
j
=
0
;
j
<
cells
.
size
();
j
++){
XWPFTableCell
cell
=
cells
.
get
(
j
);
cell
.
setText
(
tableList
.
get
(
i
-
9
)[
j
]);
}
}
}
/**
* 判断文本中时候包含$
* @param text 文本
* @return 包含返回true,不包含返回false
*/
public
static
boolean
checkText
(
String
text
){
boolean
check
=
false
;
if
(
text
.
indexOf
(
"$"
)!=
-
1
){
check
=
true
;
}
return
check
;
}
/**
* insertRow 在word表格中指定位置插入一行,并将某一行的样式复制到新增行
* @param copyrowIndex 需要复制的行位置
* @param newrowIndex 需要新增一行的位置
* */
public
static
void
insertRow
(
XWPFTable
table
,
int
copyrowIndex
,
int
newrowIndex
)
{
// 在表格中指定的位置新增一行
XWPFTableRow
targetRow
=
table
.
insertNewTableRow
(
newrowIndex
);
// 获取需要复制行对象
XWPFTableRow
copyRow
=
table
.
getRow
(
copyrowIndex
);
//复制行对象
targetRow
.
getCtRow
().
setTrPr
(
copyRow
.
getCtRow
().
getTrPr
());
//或许需要复制的行的列
List
<
XWPFTableCell
>
copyCells
=
copyRow
.
getTableCells
();
//复制列对象
XWPFTableCell
targetCell
=
null
;
for
(
int
i
=
0
;
i
<
copyCells
.
size
();
i
++)
{
XWPFTableCell
copyCell
=
copyCells
.
get
(
i
);
targetCell
=
targetRow
.
addNewTableCell
();
targetCell
.
getCTTc
().
setTcPr
(
copyCell
.
getCTTc
().
getTcPr
());
if
(
copyCell
.
getParagraphs
()
!=
null
&&
copyCell
.
getParagraphs
().
size
()
>
0
)
{
targetCell
.
getParagraphs
().
get
(
0
).
getCTP
().
setPPr
(
copyCell
.
getParagraphs
().
get
(
0
).
getCTP
().
getPPr
());
if
(
copyCell
.
getParagraphs
().
get
(
0
).
getRuns
()
!=
null
&&
copyCell
.
getParagraphs
().
get
(
0
).
getRuns
().
size
()
>
0
)
{
XWPFRun
cellR
=
targetCell
.
getParagraphs
().
get
(
0
).
createRun
();
cellR
.
setBold
(
copyCell
.
getParagraphs
().
get
(
0
).
getRuns
().
get
(
0
).
isBold
());
}
}
}
}
/**
* 匹配传入信息集合与模板
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public
static
String
changeValue
(
String
value
,
Map
<
String
,
String
>
textMap
){
Set
<
Entry
<
String
,
String
>>
textSets
=
textMap
.
entrySet
();
for
(
Entry
<
String
,
String
>
textSet
:
textSets
)
{
//匹配模板与替换值 格式${key}
String
key
=
"${"
+
textSet
.
getKey
()+
"}"
;
if
(
value
.
indexOf
(
key
)!=
-
1
){
value
=
textSet
.
getValue
();
}
}
//模板未匹配到区域替换为空
if
(
checkText
(
value
)){
value
=
""
;
}
return
value
;
}
public
static
void
main
(
String
[]
args
)
{
//模板文件地址
String
inputUrl
=
"D:/科环集团电力运营日报模板.docx"
;
//新生产的模板文件
String
outputUrl
=
"D:/科环集团电力运营日报模板1.docx"
;
Map
<
String
,
String
>
testMap
=
new
HashMap
<
String
,
String
>();
testMap
.
put
(
"year"
,
"2025"
);
testMap
.
put
(
"month"
,
"1"
);
testMap
.
put
(
"day"
,
"1"
);
List
<
String
[]>
testList
=
new
ArrayList
<
String
[]>();
// testList.add(new String[]{"1","Q1","W1","E1","R1","T1"});
// testList.add(new String[]{"2","Q2","W2","E2","R2","T2"});
// testList.add(new String[]{"3","Q3","W3","E3","R3","T3"});
// testList.add(new String[]{"4","Q4","W4","E4","R4","T4"});
// testList.add(new String[]{"5","Q5","W5","E5","R5","T5"});
// testList.add(new String[]{"6","Q6","W6","E6","R6","T6"});
// testList.add(new String[]{"7","Q7","W7","E7","R7","T7"});
WordUtil
.
changWord
(
inputUrl
,
outputUrl
,
testMap
,
testList
);
}
}
ask-data-ai/ask-data-ai-common/src/main/java/com/ask/common/core/FileTemplate.java
View file @
234f9289
...
...
@@ -18,6 +18,11 @@ public interface FileTemplate extends InitializingBean {
* @param bucketName bucket名称
*/
void
createBucket
(
String
bucketName
);
/**
* 创建bucket
* @param bucketName bucket名称
*/
boolean
isFileExists
(
String
bucketName
,
String
fileName
);
/**
* 获取全部bucket
...
...
ask-data-ai/ask-data-ai-common/src/main/java/com/ask/common/local/LocalFileTemplate.java
View file @
234f9289
...
...
@@ -31,6 +31,12 @@ public class LocalFileTemplate implements FileTemplate {
FileUtil
.
mkdir
(
basePath
+
FileUtil
.
FILE_SEPARATOR
+
bucketName
);
}
@Override
public
boolean
isFileExists
(
String
bucketName
,
String
fileName
)
{
String
dir
=
basePath
+
FileUtil
.
FILE_SEPARATOR
+
bucketName
+
FileUtil
.
FILE_SEPARATOR
+
fileName
;
return
FileUtil
.
exist
(
dir
);
}
/**
* 获取全部bucket
* <p>
...
...
ask-data-ai/pom.xml
View file @
234f9289
...
...
@@ -64,7 +64,9 @@
<resources>
<resource>
<directory>
src/main/resources
</directory>
<filtering>
true
</filtering>
<includes>
<include>
**/*
</include>
</includes>
</resource>
</resources>
<plugins>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment