提交 3bd89fc5 authored 作者: 陈泽健's avatar 陈泽健

Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	.gitignore
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/analyze_options.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/build_agent_correct.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/build_agent_in_container.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/build_with_temp_container.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/check_nginx_modules.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/check_nginx_real_config.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/correct_https_test.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/create_jar.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/get_kkfile_nginx.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/implement_agent.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/nginx_url_rewrite_test.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/parse_nginx_single_line.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/plan_agent.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/read_from_host.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/read_nginx_direct.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/real_browser_test.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/rebuild_agent.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/rebuild_clean.py
#	AuxiliaryTool/ScriptTool/问题处理/kkfile_https调试脚本/view_kkfile_block.py
......@@ -387,6 +387,19 @@ def generate_report_main(
print(f" [X] 项目资料上传失败,请查看日志")
else:
print(f" [!] 跳过项目资料上传")
# 询问是否上传到协作文档(创建OnlyOffice在线协作文档)
from src.erp_uploader import ask_cooperation_upload_confirmation_cli, upload_file_to_cooperation
coop_confirmed, coop_project_id, coop_file_name = ask_cooperation_upload_confirmation_cli(report_path)
if coop_confirmed:
print(f"\n正在上传报告到ERP协作文档(项目ID: {coop_project_id})...")
coop_info = upload_file_to_cooperation(report_path, coop_project_id, coop_file_name, logger)
if coop_info:
print(f" [OK] 报告已成功上传到ERP协作文档(协作文档ID: {coop_info.get('id')})")
else:
print(f" [X] 协作文档上传失败,请查看日志")
else:
print(f" [!] 跳过协作文档上传")
else:
print(f" [X] 报告上传失败,请查看日志")
else:
......
......@@ -333,7 +333,8 @@ ERP_MAX_RETRIES = 3
ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口路径
ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口路径
ERP_STUFF_URL = "/openclaw/stuff" # 获取人员列表接口路径
ERP_UPLOAD_PROJECT_URL = "/openclaw/upload/project" # 项目资料上传接口路径
ERP_UPLOAD_PROJECT_URL = "/openclaw/upload/project" # 项目资料上传接口路径(fileGroup_id=4,仅归档,不创建协作)
ERP_UPLOAD_COOPERATION_URL = "/openclaw/upload/cooperation" # 协作文档上传接口路径(fileGroup_id=1,创建OnlyOffice在线协作文档)
# 请求超时时间(秒)
ERP_REQUEST_TIMEOUT = 30
......
......@@ -29,6 +29,7 @@ from src.config import (
ERP_CREATE_REPORT_URL,
ERP_STUFF_URL,
ERP_UPLOAD_PROJECT_URL,
ERP_UPLOAD_COOPERATION_URL,
ERP_REQUEST_TIMEOUT,
ERP_SAVE_IMAGES_TEMP,
ERP_VALIDATE_IMAGES,
......@@ -669,6 +670,196 @@ def upload_file_to_project(file_path: str, project_id: int, name: str, logger: l
return None
def upload_file_to_cooperation(file_path: str, project_id: int, name: str, logger: logging.Logger = None, group_id: Optional[int] = None) -> Optional[dict]:
"""
上传文件到ERP协作文档(创建OnlyOffice在线协作文档),含重试机制
与项目资料上传不同,协作文档上传会在ERP中创建OnlyOffice在线协作文档,
返回结果中包含 fileKey/filePath/sharePath 等协作信息,支持多人在线协作编辑。
Args:
file_path: 要上传的文件路径(推荐 .docx/.xlsx/.pptx/.pdf)
project_id: 关联的项目ID
name: 文件名称(留空使用原文件名)
logger: 日志记录器(可选,默认创建新的logger)
group_id: 分组ID(可选,可将文件归类到指定分组)
Returns:
上传成功返回协作文档信息字典,失败返回None
文件信息格式: {'id': int, 'file_id': int, 'name': str, 'fileKey': str,
'filePath': str, 'fileType': str, 'fileSize': int,
'sharePath': str, 'project_id': int}
"""
# 如果没有提供logger,创建新的logger
if logger is None:
logger = setup_logger()
url = f'{ERP_BASE_URL}{ERP_UPLOAD_COOPERATION_URL}'
logger.info("=" * 50)
logger.info("协作文档上传流程开始")
logger.info(f"文件路径: {file_path}")
logger.info(f"项目ID: {project_id}")
logger.info(f"文件名称: {name}")
if group_id is not None:
logger.info(f"分组ID: {group_id}")
logger.info("=" * 50)
# 验证文件存在
if not os.path.exists(file_path):
logger.error(f"✗ 文件不存在: {file_path}")
return None
# 获取文件大小
file_size = os.path.getsize(file_path)
logger.info(f"文件大小: {file_size} bytes ({file_size / 1024:.2f} KB)")
for attempt in range(ERP_MAX_RETRIES):
try:
# 准备请求参数(multipart/form-data)
data = {
'project_id': project_id,
'name': name
}
# 分组ID为可选参数,仅在传入时提交
if group_id is not None:
data['group_id'] = group_id
headers = {'X-Api-Key': ERP_API_KEY}
# 打印请求信息
logger.info(f"=== 协作文档上传请求 ===")
logger.info(f"请求URL: {url}")
logger.info(f"请求Headers: X-Api-Key={ERP_API_KEY[:10]}...")
logger.info(f"请求参数:")
logger.info(f" - project_id: {project_id}")
logger.info(f" - name: {name}")
if group_id is not None:
logger.info(f" - group_id: {group_id}")
logger.info(f" - file: {file_path}")
# 读取文件并上传
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f, 'application/octet-stream')}
# 发送请求
resp = requests.post(
url,
headers=headers,
files=files,
data=data,
timeout=ERP_REQUEST_TIMEOUT
)
# 打印响应信息
logger.info(f"响应状态码: {resp.status_code}")
logger.info(f"响应内容: {resp.text}")
result = resp.json()
if result.get('success') == 1:
file_info = result['data']
logger.info(f"✓ 协作文档上传成功!")
logger.info(f" - 协作文档ID: {file_info.get('id')}")
logger.info(f" - 源文件ID: {file_info.get('file_id')}")
logger.info(f" - 文件名称: {file_info.get('name')}")
logger.info(f" - 协作密钥(fileKey): {file_info.get('fileKey')}")
logger.info(f" - 协作路径(filePath): {file_info.get('filePath')}")
logger.info(f" - 文件类型: {file_info.get('fileType')}")
logger.info(f" - 文件大小: {file_info.get('fileSize')} bytes")
logger.info(f" - 分享路径(sharePath): {file_info.get('sharePath')}")
logger.info(f" - 项目ID: {file_info.get('project_id')}")
return file_info
else:
error_code = result.get('error', 0)
# 4xx错误(客户端错误,如权限不足、Key无效)不重试
if 400 <= error_code < 500:
logger.error(f"✗ 协作文档上传失败(客户端错误 {error_code}): {result.get('msg')}")
# 针对权限不足(40000015)给出明确提示
if error_code == 40000015:
logger.error(" → 提示:请确认 API Key 已授权 upload_cooperation 权限")
return None
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"协作文档上传失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"✗ 协作文档上传失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout:
logger.warning(f"协作文档上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
if attempt < ERP_MAX_RETRIES - 1:
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"✗ 协作文档上传超时(已达最大重试次数)")
except Exception as e:
logger.error(f"✗ 协作文档上传异常: {str(e)}")
break
return None
def ask_cooperation_upload_confirmation_cli(report_path: str) -> Tuple[bool, Optional[int], Optional[str]]:
"""
控制台交互:询问用户是否上传报告到ERP协作文档
Args:
report_path: 报告文件路径
Returns:
元组 (用户选择, 项目ID, 文件名称)
- (True, int, str) 用户确认上传
- (False, None, None) 用户选择不上传
"""
while True:
print(f"\n{'='*50}")
print(f"是否将报告上传到ERP协作文档?(将创建OnlyOffice在线协作文档)")
print(f"{'='*50}")
print(f"报告文件: {report_path}")
choice = input("请输入 Y/N (默认=N): ").strip().upper()
if not choice:
choice = 'N'
if choice == 'Y':
# 输入项目ID
project_id = None
while True:
id_input = input("请输入项目ID: ").strip()
if not id_input:
print("项目ID不能为空,请重新输入")
continue
try:
project_id = int(id_input)
if project_id > 0:
break
else:
print("项目ID必须为正整数,请重新输入")
except ValueError:
print("输入无效,请输入数字")
# 输入文件名称
file_name = None
while True:
name_input = input("请输入文件名称(直接回车使用原文件名): ").strip()
if not name_input:
# 使用原文件名(去掉扩展名)
import os
file_name = os.path.splitext(os.path.basename(report_path))[0]
print(f"使用原文件名: {file_name}")
break
else:
file_name = name_input
break
return True, project_id, file_name
elif choice == 'N':
return False, None, None
else:
print("输入错误,请输入 Y 或 N")
def ask_upload_confirmation_cli(report_path: str) -> Tuple[bool, Optional[int], Optional[list], Optional[int]]:
"""
控制台交互:询问用户是否上传报告到ERP,输入测试单ID、抄送人和创建人
......
......@@ -500,16 +500,16 @@ class ReportGeneratorGUI:
def _ask_upload_to_erp(self, report_path: str):
"""
询问用户是否上传报告到ERP,并获取测试单ID、抄送人、创建人和项目资料上传信息
询问用户是否上传报告到ERP,并获取测试单ID、抄送人、创建人和项目资料/协作文档上传信息
Args:
report_path: 报告文件路径
"""
# 使用自定义对话框替代messagebox,确保按钮正常显示
confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name = self._show_upload_dialog(report_path)
confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name, upload_cooperation = self._show_upload_dialog(report_path)
if confirmed:
# 用户选择上传,透传测试单ID、抄送人列表和创建人ID,以及项目资料上传信息
# 用户选择上传,透传测试单ID、抄送人列表和创建人ID,以及项目资料/协作文档上传信息
self._upload_to_erp(
report_path,
developtesting_id=testing_id,
......@@ -517,22 +517,23 @@ class ReportGeneratorGUI:
createuser_id=creator_id,
upload_project=upload_project,
project_id=project_id,
file_name=file_name
file_name=file_name,
upload_cooperation=upload_cooperation
)
else:
self._log("[!] 跳过上传到ERP", "warning")
def _show_upload_dialog(self, report_path: str) -> tuple:
"""
显示自定义上传确认对话框(含测试单ID输入框、抄送人输入框、创建人输入框和项目资料上传选项)
显示自定义上传确认对话框(含测试单ID输入框、抄送人输入框、创建人输入框和项目资料/协作文档上传选项)
Args:
report_path: 报告文件路径
Returns:
元组 (用户选择, 测试单ID, 抄送人ID列表, 创建人ID, 是否上传项目资料, 项目ID, 文件名称)
- (True, int, list, int, bool, int, str) 用户确认上传
- (False, None, None, None, False, None, None) 用户选择不上传
元组 (用户选择, 测试单ID, 抄送人ID列表, 创建人ID, 是否上传项目资料, 项目ID, 文件名称, 是否上传协作文档)
- (True, int, list, int, bool, int, str, bool) 用户确认上传
- (False, None, None, None, False, None, None, False) 用户选择不上传
"""
# 导入ERP配置
from src.config import ERP_DEVELOPTESTING_ID
......@@ -685,7 +686,7 @@ class ReportGeneratorGUI:
padx=10
).pack(side=tk.LEFT, padx=(0, 5))
# 项目资料上传区域
# 项目资料/协作文档上传区域(共享项目ID和文件名称输入)
project_frame = tk.Frame(message_frame)
project_frame.pack(anchor=tk.W, pady=(15, 0))
......@@ -696,17 +697,28 @@ class ReportGeneratorGUI:
text="同时上传到项目资料",
variable=project_var,
font=("微软雅黑", 10),
command=lambda: on_project_check_changed()
command=lambda: on_upload_option_changed()
)
project_check.pack(anchor=tk.W)
# 项目资料输入容器(初始隐藏)
# 协作文档上传复选框(创建OnlyOffice在线协作文档)
cooperation_var = tk.BooleanVar()
cooperation_check = tk.Checkbutton(
project_frame,
text="同时上传到协作文档(创建OnlyOffice在线协作文档)",
variable=cooperation_var,
font=("微软雅黑", 10),
command=lambda: on_upload_option_changed()
)
cooperation_check.pack(anchor=tk.W)
# 项目资料/协作文档输入容器(初始隐藏,任一勾选时显示)
project_inputs_frame = tk.Frame(project_frame)
# project_inputs_frame.pack() # 根据复选框状态显示/隐藏
def on_project_check_changed():
"""复选框状态变化时的处理"""
if project_var.get():
def on_upload_option_changed():
"""复选框状态变化时的处理:任一勾选时显示项目ID和文件名称输入"""
if project_var.get() or cooperation_var.get():
project_inputs_frame.pack(anchor=tk.W, pady=(10, 0), padx=(20, 0))
else:
project_inputs_frame.pack_forget()
......@@ -748,8 +760,8 @@ class ReportGeneratorGUI:
button_frame = tk.Frame(dialog, bg="#f0f0f0", height=60)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)
# 结果存储: [confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name]
result = [False, None, None, None, False, None, None]
# 结果存储: [confirmed, testing_id, copyuser_ids, creator_id, upload_project, project_id, file_name, upload_cooperation]
result = [False, None, None, None, False, None, None, False]
def on_yes():
"""确认上传"""
......@@ -807,11 +819,13 @@ class ReportGeneratorGUI:
except Exception:
pass
# 获取项目资料上传信息
# 获取项目资料/协作文档上传信息
upload_project = project_var.get()
upload_cooperation = cooperation_var.get()
project_id = None
file_name = None
if upload_project:
# 任一勾选时都需要项目ID和文件名称
if upload_project or upload_cooperation:
# 验证项目ID
project_id_input = project_id_entry.get().strip()
if not project_id_input:
......@@ -845,6 +859,7 @@ class ReportGeneratorGUI:
result[4] = upload_project
result[5] = project_id
result[6] = file_name
result[7] = upload_cooperation
dialog.destroy()
def on_no():
......@@ -856,6 +871,7 @@ class ReportGeneratorGUI:
result[4] = False
result[5] = None
result[6] = None
result[7] = False
dialog.destroy()
tk.Button(
......@@ -899,7 +915,7 @@ class ReportGeneratorGUI:
# 等待对话框关闭
dialog.wait_window()
return result[0], result[1], result[2], result[3], result[4], result[5], result[6]
return result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7]
def _show_info_dialog(self, title: str, message: str):
"""
......@@ -989,7 +1005,7 @@ class ReportGeneratorGUI:
# 等待对话框关闭
dialog.wait_window()
def _upload_to_erp(self, report_path: str, developtesting_id: int = None, copyuser_list: list = None, createuser_id: int = None, upload_project: bool = False, project_id: int = None, file_name: str = None):
def _upload_to_erp(self, report_path: str, developtesting_id: int = None, copyuser_list: list = None, createuser_id: int = None, upload_project: bool = False, project_id: int = None, file_name: str = None, upload_cooperation: bool = False):
"""
上传报告到ERP
......@@ -999,8 +1015,9 @@ class ReportGeneratorGUI:
copyuser_list: 抄送人ID列表(可选,默认为空列表)
createuser_id: 创建人ID(可选,用于指定报告创建者)
upload_project: 是否上传到项目资料(可选)
project_id: 项目ID(上传项目资料时需要)
file_name: 文件名称(上传项目资料时需要)
project_id: 项目ID(上传项目资料/协作文档时需要)
file_name: 文件名称(上传项目资料/协作文档时需要)
upload_cooperation: 是否上传到协作文档(可选,创建OnlyOffice在线协作文档)
"""
try:
from src.erp_uploader import upload_report_to_erp, setup_logger
......@@ -1014,7 +1031,7 @@ class ReportGeneratorGUI:
# 在后台线程中上传
thread = threading.Thread(
target=self._upload_to_erp_thread,
args=(report_path, logger, developtesting_id, copyuser_list, createuser_id, upload_project, project_id, file_name),
args=(report_path, logger, developtesting_id, copyuser_list, createuser_id, upload_project, project_id, file_name, upload_cooperation),
daemon=True
)
thread.start()
......@@ -1026,7 +1043,7 @@ class ReportGeneratorGUI:
self._log(f"[X] ERP上传功能异常: {str(e)}", "error")
self._show_error_dialog("错误", f"ERP上传功能异常:\n{str(e)}")
def _upload_to_erp_thread(self, report_path: str, logger, developtesting_id: int = None, copyuser_list: list = None, createuser_id: int = None, upload_project: bool = False, project_id: int = None, file_name: str = None):
def _upload_to_erp_thread(self, report_path: str, logger, developtesting_id: int = None, copyuser_list: list = None, createuser_id: int = None, upload_project: bool = False, project_id: int = None, file_name: str = None, upload_cooperation: bool = False):
"""
在后台线程中上传报告到ERP
......@@ -1037,8 +1054,9 @@ class ReportGeneratorGUI:
copyuser_list: 抄送人ID列表(可选,默认为空列表)
createuser_id: 创建人ID(可选,用于指定报告创建者)
upload_project: 是否上传到项目资料(可选)
project_id: 项目ID(上传项目资料时需要)
file_name: 文件名称(上传项目资料时需要)
project_id: 项目ID(上传项目资料/协作文档时需要)
file_name: 文件名称(上传项目资料/协作文档时需要)
upload_cooperation: 是否上传到协作文档(可选,创建OnlyOffice在线协作文档)
"""
try:
from src.erp_uploader import upload_report_to_erp
......@@ -1047,6 +1065,10 @@ class ReportGeneratorGUI:
if success:
self._log("[OK] 报告已成功上传到ERP", "success")
# 用于汇总各归档项的执行结果
summary_parts = ["报告已成功上传到ERP"]
has_partial_failure = False
# 如果需要上传到项目资料
if upload_project and project_id and file_name:
self._log(f"[INFO] 开始上传到项目资料(项目ID: {project_id})...", "info")
......@@ -1055,19 +1077,39 @@ class ReportGeneratorGUI:
file_info = upload_file_to_project(report_path, project_id, file_name, logger)
if file_info:
self._log(f"[OK] 报告已成功上传到ERP项目资料(文件ID: {file_info.get('id')})", "success")
# 在主线程中显示成功消息(包含项目资料上传)
self.root.after(0, lambda: self._show_info_dialog("成功", "报告已成功上传到ERP和项目资料!"))
summary_parts.append("项目资料")
else:
self._log("[X] 项目资料上传失败,请查看日志", "error")
# 在主线程中显示部分成功消息
self.root.after(0, lambda: self._show_info_dialog("部分成功", "报告已上传到ERP,但项目资料上传失败,请查看日志"))
has_partial_failure = True
except Exception as proj_e:
proj_err_msg = str(proj_e)
self._log(f"[X] 项目资料上传异常: {proj_err_msg}", "error")
self.root.after(0, lambda msg=proj_err_msg: self._show_info_dialog("部分成功", f"报告已上传到ERP,但项目资料上传异常:\n{msg}"))
has_partial_failure = True
# 如果需要上传到协作文档(创建OnlyOffice在线协作文档)
if upload_cooperation and project_id and file_name:
self._log(f"[INFO] 开始上传到协作文档(项目ID: {project_id})...", "info")
try:
from src.erp_uploader import upload_file_to_cooperation
coop_info = upload_file_to_cooperation(report_path, project_id, file_name, logger)
if coop_info:
self._log(f"[OK] 报告已成功上传到ERP协作文档(协作文档ID: {coop_info.get('id')})", "success")
summary_parts.append("协作文档")
else:
self._log("[X] 协作文档上传失败,请查看日志", "error")
has_partial_failure = True
except Exception as coop_e:
coop_err_msg = str(coop_e)
self._log(f"[X] 协作文档上传异常: {coop_err_msg}", "error")
has_partial_failure = True
# 根据归档结果汇总最终提示
if has_partial_failure:
final_msg = "、".join(summary_parts) + " 已上传,但部分归档项失败,请查看日志"
self.root.after(0, lambda msg=final_msg: self._show_info_dialog("部分成功", msg))
else:
# 在主线程中显示成功消息
self.root.after(0, lambda: self._show_info_dialog("成功", "报告已成功上传到ERP!"))
final_msg = "、".join(summary_parts) + "!"
self.root.after(0, lambda msg=final_msg: self._show_info_dialog("成功", msg))
else:
self._log("[X] 报告上传失败,请查看日志", "error")
# 在主线程中显示错误消息
......
#!/usr/bin/env python
import paramiko
def run(ssh, cmd):
stdin, stdout, stderr = ssh.exec_command(cmd)
return stdout.read().decode('utf-8'), stderr.read().decode('utf-8')
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("192.168.5.70", 22, "root", "Ubains@123", timeout=10)
# 直接读取配置文件并搜索kkfile相关行
out, _ = run(ssh, "docker exec unginx grep -n kkfile /data/middleware/nginx/config/unified443.conf")
print(f"kkfile相关行号:\n{out}")
# 用行号范围读取
print("\n完整配置内容(用grep定位):")
out, _ = run(ssh, "docker exec unginx cat /data/middleware/nginx/config/unified443.conf | grep -A30 'location /kkfile'")
print(out)
ssh.close()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""实施方案D:Java Agent禁用SSL主机名验证"""
import paramiko, time
def run(ssh, cmd):
stdin, stdout, stderr = ssh.exec_command(cmd)
return stdout.read().decode('utf-8'), stderr.read().decode('utf-8')
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("192.168.5.70", 22, "root", "Ubains@123", timeout=10)
print("1. 创建Java Agent源代码...")
agent_code = '''
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
import java.lang.instrument.Instrumentation;
public class SSLBypassAgent {
public static void premain(String args, Instrumentation inst) {
try {
// 创建信任所有证书的TrustManager
TrustManager[] trustAll = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
}
};
// 设置SSL上下文
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAll, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// 禁用主机名验证
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
System.out.println("[SSLBypassAgent] SSL hostname verification disabled");
} catch (Exception e) {
e.printStackTrace();
}
}
}
'''
manifest = '''Manifest-Version: 1.0
Premain-Class: SSLBypassAgent
Can-Redefine-Classes: false
'''
# 写入Java源码
run(ssh, f"cat > /tmp/SSLBypassAgent.java << 'JAVAEOF'\n{agent_code}\nJAVAEOF")
print("源码已写入 /tmp/SSLBypassAgent.java")
# 写入MANIFEST
run(ssh, f"mkdir -p /tmp/META-INF && cat > /tmp/META-INF/MANIFEST.MF << 'MFEOF'\n{manifest}\nMFEOF")
print("MANIFEST已创建")
print("\n2. 编译Java Agent...")
# 编译
out, err = run(ssh, "javac /tmp/SSLBypassAgent.java -d /tmp/")
if err and "error" in err.lower():
print(f"编译错误: {err}")
else:
print("编译成功")
print("\n3. 打包成JAR...")
out, err = run(ssh, "cd /tmp && jar cfm ssl-bypass-agent.jar META-INF/MANIFEST.MF SSLBypassAgent.class")
if err:
print(f"打包错误: {err}")
else:
print("打包成功: /tmp/ssl-bypass-agent.jar")
print("\n4. 复制到容器内...")
run(ssh, "docker cp /tmp/ssl-bypass-agent.jar kkfile:/opt/ssl-bypass-agent.jar")
print("已复制到容器: /opt/ssl-bypass-agent.jar")
print("\n5. 修改wrapper启动脚本...")
wrapper = '''#!/bin/bash
rm -rf /tmp/.jodconverter* /tmp/hsperfdata* 2>/dev/null
exec java -javaagent:/opt/ssl-bypass-agent.jar -Dfile.encoding=UTF-8 -Dspring.config.location=/opt/kkFileView-4.1.0/config/application.properties -jar /opt/kkFileView-4.1.0/bin/kkFileView-4.1.0.jar
'''
run(ssh, f"docker exec kkfile bash -c 'cat > /opt/start.sh << \"EOF\"\n{wrapper}\nEOF'")
run(ssh, "docker exec kkfile chmod +x /opt/start.sh")
print("wrapper已更新,添加了 -javaagent 参数")
print("\n6. 提交镜像...")
out, _ = run(ssh, "docker commit kkfile kkfile-with-agent:latest")
print(f"镜像: {out.strip()}")
print("\n7. 重启容器...")
run(ssh, "docker restart kkfile")
print("等待70秒...")
time.sleep(70)
print("\n8. 验证...")
out, _ = run(ssh, "docker logs kkfile 2>&1 | grep -E 'SSLBypassAgent|启动完成' | tail -2")
print(out if out else "检查日志...")
out, _ = run(ssh, "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8012/index")
print(f"HTTP状态: {out}")
ssh.close()
# 报告生成优化需求文档
## 代码路径
- 代码路径:[AuxiliaryTool/FunctionalTestReportGeneration]
## ERP对接文档
- 需求文档路径:[Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md]
## 功能需求
### 功能目标
**目标:** 对接ERP项目协作文档上传接口,实现创建完成ERP测试报告后,将测试报告文件上传至ERP项目协作文档上传中。
### 需求描述
- 上传项目资料接口:
- 根据[Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md]### 4.1 协作文档上传章节。
- 接口调用地址:https://office.ubainsyun.com:5082/api/uerp/openclaw/upload/cooperation
- X-Api-Key: 保持原有的key传入就行。
- 请求示例:
```bash
curl -X POST "https://office.ubainsyun.com:5082/uerp/openclaw/upload/cooperation" \
-H "X-Api-Key: your_api_key" \
-F "file=@/path/to/需求文档.pdf" \
-F "project_id=100" \
-F "name=项目需求文档"
```
- project_id和name通过用户输入。
- file就直接采用生产的测试报告word格式即可。
- 新增选项`上传协作文档`
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
\ No newline at end of file
# ERP对接优化文件归档至协作文档 - 计划执行文档
## 执行概述
### 项目背景
当前功能测试报告生成工具已实现将生成的报告上传到 ERP 测试报告模块,并支持归档到 ERP 项目资料(`/openclaw/upload/project`)。为进一步支持团队在线协作,需要对接 ERP 协作文档上传接口,在创建 ERP 测试报告后,将测试报告文件(Word 格式)上传至 ERP 项目协作文档中,自动创建 OnlyOffice 在线协作文档,便于多人在线协作编辑。
### 执行目标
1. 实现协作文档上传接口调用(`POST /openclaw/upload/cooperation`
2. 在测试报告创建成功后,新增"上传协作文档"选项,触发协作文档归档流程
3. 支持用户输入项目 ID(project_id)和文件名称(name)
4. 上传文件直接采用生产的测试报告 Word 格式(.docx),无需转 HTML
5. 集成到现有上传流程中(CLI 与 GUI 双模式)
### 涉及范围
- 代码路径:`AuxiliaryTool/FunctionalTestReportGeneration`
- 接口文档:`Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md`(参见第 4.1 协作文档上传章节)
- 规范文档:
- 代码规范:`Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 文档规范:`Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范:`Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
### 关键说明
> 协作文档上传(`/cooperation`)与项目资料上传(`/project`)的区别:
> - 协作文档会创建 OnlyOffice 在线协作文档(支持多人在线编辑),返回 `fileKey`、`filePath`、`sharePath` 等协作信息;
> - 项目资料仅作为只读归档文件,不创建协作会话。
> 本需求只新增"上传协作文档"选项,**不影响**已有的测试报告上传与项目资料归档功能。
---
## 任务分解与实施计划
### 任务1:新增协作文档上传接口配置
#### 1.1 任务详情
- **接口地址**`POST /openclaw/upload/cooperation`
- **接口目的**:上传测试报告文件到 ERP 协作文档,创建 OnlyOffice 在线协作文档
- **状态**`[ ]`
#### 1.2 实施步骤
- [ ] 1.2.1 在 `src/config.py` 中新增协作文档上传接口配置常量
- `ERP_UPLOAD_COOPERATION_URL` = "/openclaw/upload/cooperation"
- [ ] 1.2.2 确认现有 `ERP_BASE_URL``ERP_API_KEY``ERP_MAX_RETRIES``ERP_RETRY_INTERVAL``ERP_REQUEST_TIMEOUT` 配置可直接复用
- [ ] 1.2.3 添加配置说明注释(明确接口用途、api_name 权限要求、fileType 映射说明)
#### 1.3 输出产物
- `src/config.py` 中的新增配置常量 `ERP_UPLOAD_COOPERATION_URL`
- 配置说明注释(含 api_name=`upload_cooperation` 权限提示)
---
### 任务2:实现协作文档上传函数
#### 2.1 任务详情
- **功能目标**:在 `src/erp_uploader.py` 中新增协作文档上传函数(参考已实现的 `upload_file_to_project()`,保持代码风格一致)
- **函数签名**`upload_file_to_cooperation(file_path: str, project_id: int, name: str, logger: logging.Logger = None, group_id: Optional[int] = None) -> Optional[dict]`
- **状态**`[ ]`
#### 2.2 实施步骤
- [ ] 2.2.1 创建 `upload_file_to_cooperation()` 函数
- [ ] 2.2.2 实现 `multipart/form-data` 格式的文件上传逻辑
- 请求参数:`file`(必填)、`project_id`(必填)、`name`(可选,留空使用原文件名)
- 可选参数:`group_id`(分组 ID,默认不传)
- [ ] 2.2.3 复用现有重试机制(`ERP_MAX_RETRIES` / `ERP_RETRY_INTERVAL`)与 4xx 错误不重试策略
- [ ] 2.2.4 添加异常处理和完整日志记录(请求 URL、Headers、参数、响应状态码、响应内容)
- [ ] 2.2.5 解析并返回上传结果,字段包括:`id``file_id``name``fileKey``filePath``fileType``fileSize``sharePath``project_id`
- [ ] 2.2.6 在返回成功时,日志中输出 `fileKey``sharePath`(便于后续定位协作文档)
#### 2.3 输出产物
- `upload_file_to_cooperation()` 函数
- 完整的错误处理、重试机制与日志记录
- 协作文档信息字典返回(含 `fileKey` 等协作字段)
#### 2.4 参考代码片段
```python
# -*- coding: utf-8 -*-
# src/erp_uploader.py 中新增
def upload_file_to_cooperation(file_path: str, project_id: int, name: str, logger: logging.Logger = None, group_id: Optional[int] = None) -> Optional[dict]:
"""
上传文件到ERP协作文档(创建OnlyOffice在线协作文档),含重试机制
Args:
file_path: 要上传的文件路径(推荐 .docx/.xlsx/.pptx/.pdf)
project_id: 关联的项目ID
name: 文件名称(留空使用原文件名)
logger: 日志记录器(可选,默认创建新的logger)
group_id: 分组ID(可选,可将文件归类到指定分组)
Returns:
上传成功返回协作文档信息字典,失败返回None
字段: {'id', 'file_id', 'name', 'fileKey', 'filePath', 'fileType', 'fileSize', 'sharePath', 'project_id'}
"""
# 如果没有提供logger,创建新的logger
if logger is None:
logger = setup_logger()
url = f'{ERP_BASE_URL}{ERP_UPLOAD_COOPERATION_URL}'
logger.info("=" * 50)
logger.info("协作文档上传流程开始")
logger.info(f"文件路径: {file_path}")
logger.info(f"项目ID: {project_id}")
logger.info(f"文件名称: {name}")
logger.info("=" * 50)
# 验证文件存在
if not os.path.exists(file_path):
logger.error(f"✗ 文件不存在: {file_path}")
return None
# 获取文件大小
file_size = os.path.getsize(file_path)
logger.info(f"文件大小: {file_size} bytes ({file_size / 1024:.2f} KB)")
for attempt in range(ERP_MAX_RETRIES):
try:
# 准备请求参数(multipart/form-data)
data = {
'project_id': project_id,
'name': name
}
# 分组ID为可选参数
if group_id is not None:
data['group_id'] = group_id
headers = {'X-Api-Key': ERP_API_KEY}
# 打印请求信息
logger.info(f"=== 协作文档上传请求 ===")
logger.info(f"请求URL: {url}")
logger.info(f"请求Headers: X-Api-Key={ERP_API_KEY[:10]}...")
logger.info(f"请求参数:")
logger.info(f" - project_id: {project_id}")
logger.info(f" - name: {name}")
if group_id is not None:
logger.info(f" - group_id: {group_id}")
logger.info(f" - file: {file_path}")
# 读取文件并上传
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f, 'application/octet-stream')}
# 发送请求
resp = requests.post(
url,
headers=headers,
files=files,
data=data,
timeout=ERP_REQUEST_TIMEOUT
)
# 打印响应信息
logger.info(f"响应状态码: {resp.status_code}")
logger.info(f"响应内容: {resp.text}")
result = resp.json()
if result.get('success') == 1:
file_info = result['data']
logger.info(f"✓ 协作文档上传成功!")
logger.info(f" - 协作文档ID: {file_info.get('id')}")
logger.info(f" - 源文件ID: {file_info.get('file_id')}")
logger.info(f" - 文件名称: {file_info.get('name')}")
logger.info(f" - 协作密钥(fileKey): {file_info.get('fileKey')}")
logger.info(f" - 协作路径(filePath): {file_info.get('filePath')}")
logger.info(f" - 文件类型: {file_info.get('fileType')}")
logger.info(f" - 文件大小: {file_info.get('fileSize')} bytes")
logger.info(f" - 分享路径(sharePath): {file_info.get('sharePath')}")
logger.info(f" - 项目ID: {file_info.get('project_id')}")
return file_info
else:
error_code = result.get('error', 0)
# 4xx错误(客户端错误,如权限不足、Key无效)不重试
if 400 <= error_code < 500:
logger.error(f"✗ 协作文档上传失败(客户端错误 {error_code}): {result.get('msg')}")
# 针对权限不足(40000015)给出明确提示
if error_code == 40000015:
logger.error(" → 提示:请确认 API Key 已授权 upload_cooperation 权限")
return None
if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"协作文档上传失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"✗ 协作文档上传失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout:
logger.warning(f"协作文档上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
if attempt < ERP_MAX_RETRIES - 1:
time.sleep(ERP_RETRY_INTERVAL)
else:
logger.error(f"✗ 协作文档上传超时(已达最大重试次数)")
except Exception as e:
logger.error(f"✗ 协作文档上传异常: {str(e)}")
break
return None
```
---
### 任务3:集成到现有上传流程
#### 3.1 任务详情
- **功能目标**:在 ERP 测试报告创建成功后,新增"上传协作文档"选项
- **集成位置**
- CLI:`src/cli.py``generate_report_main()` 函数
- GUI:`src/gui.py``_show_upload_dialog()``_upload_to_erp_thread()` 函数
- **状态**`[ ]`
#### 3.2 实施步骤(CLI 模式)
- [ ] 3.2.1 在 `src/erp_uploader.py` 中新增 CLI 交互函数 `ask_cooperation_upload_confirmation_cli(report_path)`(参考已有 `ask_project_upload_confirmation_cli()`
- 询问是否上传到协作文档(Y/N)
- 获取用户输入的项目 ID(必填,正整数校验)
- 获取文件名称(留空使用原文件名)
- [ ] 3.2.2 在 `src/cli.py``generate_report_main()` 中,于测试单上传成功分支内,调用 `ask_cooperation_upload_confirmation_cli()`
- [ ] 3.2.3 用户确认后调用 `upload_file_to_cooperation()` 执行上传,打印成功/失败结果
#### 3.3 实施步骤(GUI 模式)
- [ ] 3.3.1 在 `src/gui.py``_show_upload_dialog()` 中新增"同时上传到协作文档"复选框
- 复用现有的项目 ID、文件名称输入框(协作文档与项目资料均需 project_id 与 name)
- 勾选后展示协作文档专属说明(提示将创建 OnlyOffice 在线协作文档)
- [ ] 3.3.2 扩展 `_show_upload_dialog()` 的返回值元组,新增 `upload_cooperation`(bool)字段
- [ ] 3.3.3 在 `_ask_upload_to_erp()``_upload_to_erp()` 中透传协作文档上传参数
- [ ] 3.3.4 在 `_upload_to_erp_thread()` 中,测试单上传成功后,根据 `upload_cooperation` 标志调用 `upload_file_to_cooperation()`,并展示协作文档信息(fileKey 等)
#### 3.4 输出产物
- CLI 模式下的协作文档上传交互(`ask_cooperation_upload_confirmation_cli()`
- GUI 模式下的"同时上传到协作文档"复选框及联动逻辑
- 测试单上传成功后的协作文档归档流程
---
### 任务4:优化交互体验与输入校验
#### 4.1 任务详情
- **功能目标**:提供友好的用户输入提示、输入校验和明确的成功/失败反馈
- **状态**`[ ]`
#### 4.2 实施步骤
- [ ] 4.2.1 添加项目 ID 输入验证(必须为正整数)
- [ ] 4.2.2 添加文件名称输入处理(留空时使用原文件名,去掉扩展名)
- [ ] 4.2.3 实现输入错误的友好提示,允许重新输入
- [ ] 4.2.4 上传成功时打印/展示协作文档关键信息(文档 ID、fileKey、fileType)
- [ ] 4.2.5 上传失败时给出明确错误原因(参考错误码:40000001 参数错误、40000014 Key 无效/过期、40000015 权限不足)
- [ ] 4.2.6 支持跳过协作文档上传的选项(不影响测试单与项目资料上传)
#### 4.3 输出产物
- 完善的输入验证逻辑
- 友好的错误提示与成功反馈信息
---
## 验收标准
### 功能验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 协作文档上传接口 | 成功调用 `/openclaw/upload/cooperation`,返回 `success=1` 及协作文档信息 | `[ ]` |
| 2 | 协作文档创建 | 返回结果包含 `fileKey``filePath``sharePath` 等协作字段,能在 ERP 页面打开 OnlyOffice 协作 | `[ ]` |
| 3 | 文件上传正确性 | 上传的 Word 文件内容与原测试报告一致 | `[ ]` |
| 4 | CLI 交互模式 | 能正常输入项目 ID 和文件名称,完成协作文档上传 | `[ ]` |
| 5 | GUI 交互模式 | 对话框新增"同时上传到协作文档"选项,勾选后能正常上传 | `[ ]` |
| 6 | 流程集成 | 测试单报告创建成功后,能正确触发协作文档上传询问 | `[ ]` |
| 7 | 跳过功能 | 用户可选择跳过协作文档上传,不影响其他流程 | `[ ]` |
| 8 | 异常处理 | 网络异常、接口失败、权限不足时有适当提示 | `[ ]` |
### 代码质量验收标准
| 序号 | 验收项 | 验收标准 | 状态 |
|------|--------|----------|------|
| 1 | 代码规范 | 符合 `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md` | `[ ]` |
| 2 | 中文注释 | 所有新增代码有完整的中文注释 | `[ ]` |
| 3 | 代码风格一致 | 与现有 `upload_file_to_project()` 保持一致的命名、日志、重试风格 | `[ ]` |
| 4 | 异常处理 | 所有可能的异常情况都有处理逻辑(超时、4xx/5xx、文件不存在等) | `[ ]` |
| 5 | 日志记录 | 关键操作有 INFO/WARN/ERROR 级别日志输出,便于审计排查 | `[ ]` |
| 6 | 兼容性 | 不影响现有测试单上传与项目资料归档功能的正常运行 | `[ ]` |
| 7 | 重试机制 | 复用现有重试配置,4xx 客户端错误不重试 | `[ ]` |
---
## 测试计划
### 单元测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| 接口调用 | 调用协作文档上传接口 | 返回 `success=1` 及完整协作文档信息 | `[ ]` |
| 参数验证 | 验证 file、project_id、name 参数 | 参数正确传入接口 | `[ ]` |
| 可选参数 | 传入/不传入 group_id | 均能正常上传,group_id 正确归类 | `[ ]` |
| 文件读取 | 读取并上传 Word 文件 | 文件内容完整上传,fileType 为 word | `[ ]` |
| 返回字段 | 解析响应字段 | 正确返回 id、fileKey、filePath、sharePath 等字段 | `[ ]` |
| 重试机制 | 模拟网络异常(5xx) | 能正确重试指定次数 | `[ ]` |
| 4xx 不重试 | 模拟权限不足(40000015) | 不重试,返回 None 并给出权限提示 | `[ ]` |
| 异常处理 | 模拟超时、文件不存在 | 有友好的错误提示 | `[ ]` |
### 集成测试
| 测试项 | 测试内容 | 预期结果 | 状态 |
|--------|----------|----------|------|
| CLI 完整流程 | CLI 模式下生成报告→测试单上传→协作文档上传 | 流程完整,协作文档上传成功 | `[ ]` |
| GUI 完整流程 | GUI 模式下勾选"同时上传到协作文档" | 对话框正常,上传成功,展示 fileKey | `[ ]` |
| 跳过上传 | 选择跳过协作文档上传 | 流程正常结束,不影响报告创建与项目资料上传 | `[ ]` |
| 输入验证 | 输入无效的项目 ID(非数字/非正整数) | 提示错误并允许重新输入 | `[ ]` |
| 网络异常测试 | 模拟网络异常情况 | 有友好的错误提示,不影响主流程 | `[ ]` |
| 权限不足测试 | API Key 未授权 upload_cooperation | 返回 40000015,提示检查权限 | `[ ]` |
| 兼容性测试 | 执行现有测试单上传、项目资料上传 | 现有功能正常运行不受影响 | `[ ]` |
| 并行归档测试 | 同时勾选"项目资料"和"协作文档" | 两者均上传成功,互不影响 | `[ ]` |
### 验证方法
1. 在 ERP 系统中准备一个有效项目(记录 project_id);
2. 确认 API Key 已授权 `upload_cooperation` 权限;
3. 运行工具生成测试报告(Word 格式);
4. 测试单上传成功后,选择上传协作文档,输入 project_id;
5. 到 ERP 项目管理 → 项目资料 → 协作文档 页签,确认文档已创建并能打开 OnlyOffice 在线协作。
---
## 风险评估
| 风险项 | 风险等级 | 影响范围 | 应对措施 |
|--------|----------|----------|----------|
| API Key 权限不足 | 中 | 接口返回 40000015,上传失败 | 提示用户检查 Key 是否授权 `upload_cooperation`;文档中明确权限要求 |
| 接口变更 | 中 | 协作文档上传失败 | 添加接口版本管理,做好兼容处理;参考最新接口文档 |
| 网络不稳定 | 中 | 上传超时失败 | 复用现有重试机制(3 次、间隔 5 秒)和超时处理 |
| 用户输入错误 | 低 | 参数无效导致上传失败 | 添加项目 ID 正整数校验和友好提示 |
| 现有功能影响 | 中 | 破坏现有测试单/项目资料上传流程 | 新增选项独立,充分回归测试,保持向后兼容 |
| 文件过大 | 低 | 上传时间过长或失败 | 复用现有文件大小日志,必要时提示 |
| OnlyOffice 服务异常 | 低 | 协作文档创建失败 | 依赖 ERP 侧服务,失败时明确提示并记录日志 |
| fileType 识别 | 低 | 非 .docx 文件协作体验不佳 | 测试报告固定为 .docx,fileType 默认 word,无需额外处理 |
---
## 实施记录
### 实施时间线
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| - | 任务1:新增协作文档上传接口配置 | `[ ]` | - |
| - | 任务2:实现协作文档上传函数 | `[ ]` | - |
| - | 任务3:集成到现有上传流程(CLI + GUI) | `[ ]` | - |
| - | 任务4:优化交互体验与输入校验 | `[ ]` | - |
| - | 单元测试 | `[ ]` | - |
| - | 集成测试 | `[ ]` | - |
| - | 验收测试 | `[ ]` | - |
### 问题记录
| 日期 | 问题描述 | 解决方案 | 状态 |
|------|----------|----------|------|
| - | - | - | `[ ]` |
---
## 后续工作
### 优化建议
- [ ] 支持从 ERP 项目列表中选择项目,而非手动输入项目 ID
- [ ] 添加项目 ID 记忆功能,方便重复使用
- [ ] 协作文档上传成功后,可选展示/复制 OnlyOffice 协作链接(基于 fileKey/sharePath)
- [ ] 支持 `group_id` 分组选择的交互配置(当前作为可选高级参数)
- [ ] 添加上传历史记录,便于追溯协作文档归档情况
### 待确认事项
- [ ] 确认 API Key 是否已授权 `upload_cooperation` 权限
- [ ] 确认用于联调测试的有效项目 ID
- [ ] 确认协作文档是否需要归类到特定分组(group_id)
- [ ] 确认协作文档与项目资料是否需要支持同时上传(默认支持并行)
---
## 附录
### 参考文档
- 代码规范:`Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结:`Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结:`Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范:`Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范:`Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
- ERP 接口文档:`Docs/PRD/自动化生成功能测试报告/ERP对接PRD/PRD_项目资料_上传接口_例子说明.md`(第 4.1 协作文档上传章节)
- 关联需求文档:`Docs/PRD/自动化生成功能测试报告/需求文档/_PRD_ERP对接优化文件归档至协作文档_需求文档.md`
### 代码路径
- 主目录:`AuxiliaryTool/FunctionalTestReportGeneration`
- 配置文件:`src/config.py`
- 上传模块:`src/erp_uploader.py`(新增 `upload_file_to_cooperation()``ask_cooperation_upload_confirmation_cli()`
- CLI 模块:`src/cli.py``generate_report_main()` 集成)
- GUI 模块:`src/gui.py``_show_upload_dialog()``_upload_to_erp_thread()` 集成)
### 接口信息
- **接口地址**`POST /openclaw/upload/cooperation`
- **完整 URL**`https://office.ubainsyun.com:5082/api/uerp/openclaw/upload/cooperation`
- **api_name**`upload_cooperation`(创建 API Key 时需勾选该权限)
- **认证方式**`X-Api-Key` 请求头(复用现有 ERP_API_KEY)
- **请求格式**`multipart/form-data`
- **请求参数**
- `file`:上传的文件(必填,推荐 .docx/.xlsx/.pptx/.pdf)
- `project_id`:关联的项目 ID(必填,int)
- `name`:文件名称(可选,默认使用原文件名)
- `group_id`:分组 ID(可选,可将文件归类到指定分组)
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 协作文档记录 ID(Personfile 表) |
| file_id | int | 源文件记录 ID(Producefile 表) |
| name | string | 文件名称 |
| fileKey | string | OnlyOffice 协作密钥,唯一标识协作会话 |
| filePath | string | OnlyOffice 协作文件相对路径 |
| fileType | string | 文件类型(word/excel/ppt/pdf) |
| fileSize | int | 文件大小(字节) |
| sharePath | string | 文件分享路径(与 fileKey 一致) |
| project_id | int | 关联的项目 ID |
### 错误码说明
| 错误码 | 说明 | 触发场景 | 解决方案 |
|--------|------|----------|----------|
| 40000001 | 参数错误或业务异常 | 缺少 file/project_id;项目不存在;上传异常 | 检查请求参数是否正确 |
| 40000014 | 无效或过期的 API Key | Key 不存在/已禁用/已过期 | 检查 Key 是否正确、是否启用、是否在有效期内 |
| 40000015 | 无权限访问该接口 | Key 未授权 `upload_cooperation` | 联系管理员添加接口权限 |
---
## 优化功能回填
> 此区域用于记录实施过程中的优化和改进
| 日期 | 优化内容 | 优化人 | 状态 |
|------|----------|--------|------|
| - | - | - | `[ ]` |
---
**文档版本**: 1.0
**创建日期**: 2026-06-18
**最后更新**: 2026-06-18
# 主服务端口切换需求文档
## 代码路径
- X86架构:
- 需调用代码路径:[自动化部署脚本/x86架构/新统一平台/auto_check_service_ports.sh]
- 主脚本:[自动化部署脚本/x86架构/新统一平台/new_auto.sh]
- ARM架构:
- 需调用代码路径:[自动化部署脚本/ARM架构/新统一平台/auto_check_service_ports.sh]
- 主脚本:[自动化部署脚本/ARM架构/新统一平台/new_auto.sh]
## 功能需求
### 功能目标
**目标:** middleware脚本
### 需求描述
-
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
\ No newline at end of file
# 服务部署脚本优化
## 代码路径
- deploy服务部署脚本:[自动化部署脚本/x86架构/新统一平台/auto_deploy_services.sh]
- 主脚本:[自动化部署脚本/x86架构/新统一平台/new_auto.sh]
- X86架构:
- deploy服务部署脚本:[自动化部署脚本/x86架构/新统一平台/auto_deploy_services.sh]
- 主脚本:[自动化部署脚本/x86架构/新统一平台/new_auto.sh]
- ARM架构:
- deploy服务部署脚本:[自动化部署脚本/arm架构/新统一平台/arm_auto_deploy_services.sh]
- 主脚本:[自动化部署脚本/arm架构/新统一平台/arm_new_auto.sh]
## 功能需求
### 功能目标
......
......@@ -614,6 +614,159 @@ configure_firewall_for_9990() {
return 0
}
# 配置电子桌牌色彩型号(三色/四色)
# 说明:桌牌的显示参数 color/epd_type 配置在 java-meeting(预定系统)的 sensitive-config.json 中,
# 配置修改即时生效,java-meeting 无需重启即可读取新参数。
# 色彩型号对应关系:
# 三色:color=3, epd_type=1(默认,保持不变)
# 四色:color=4, epd_type=9
# 环境变量 CARDTABLE_COLOR_MODE(由 deploy_services 在 "all" 非交互模式设置):
# 非空时直接采用该值("3" 或 "4"),跳过 whiptail 交互菜单。
# 容错策略:
# - 配置文件缺失 → 仅告警,不中断桌牌容器部署
# - 修改工具采用 jq → python3 → awk 降级链,适配统信 UOS / 欧拉 / 麒麟 等不同操作系统
function configure_tablecard_color() {
local config_file="/data/services/api/java-meeting/java-meeting2.0/config/sensitive-config.json"
log "INFO" "🎨 开始配置电子桌牌色彩型号..."
# 1. 校验配置文件是否存在(缺失仅告警,不中断部署)
if [ ! -f "$config_file" ]; then
log "WARN" "⚠️ 未找到桌牌配置文件: $config_file"
log "WARN" " 请确认已部署预定系统(java-meeting);本次跳过色彩配置,继续部署桌牌容器。"
return 0
fi
# 2. 确定色彩型号:非交互模式用 CARDTABLE_COLOR_MODE / 交互模式弹 whiptail 菜单
local color_choice=""
if [ -n "$CARDTABLE_COLOR_MODE" ]; then
# 非交互模式(all 自动部署),直接采用预设型号(默认三色)
color_choice="$CARDTABLE_COLOR_MODE"
log "INFO" "🤖 非交互模式,桌牌色彩采用预设型号:${color_choice} 色"
else
# 交互模式:弹出色彩选择菜单
color_choice=$(whiptail --title "🎨 电子桌牌色彩型号选择" \
--menu "\n请选择电子桌牌的色彩型号:" \
14 60 2 \
"3" "三色 (color=3, epd_type=1) 默认" \
"4" "四色 (color=4, epd_type=9)" \
3>&1 1>&2 2>&3)
local whiptail_rc=$?
# 用户取消或 ESC:默认三色
if [ "$whiptail_rc" -ne 0 ] || [ -z "$color_choice" ]; then
log "WARN" "⚠️ 用户未选择色彩型号,默认采用三色 (color=3, epd_type=1)"
color_choice="3"
fi
fi
# 3. 映射为目标参数值
local target_color="" target_epd=""
case "$color_choice" in
3)
target_color="3"
target_epd="1"
;;
4)
target_color="4"
target_epd="9"
;;
*)
log "WARN" "⚠️ 未知色彩选项: $color_choice,默认采用三色"
target_color="3"
target_epd="1"
;;
esac
log "INFO" "🎯 目标桌牌参数:color=${target_color}, epd_type=${target_epd}"
# 4. 修改前备份配置文件
local backup_file="${config_file}.bak"
if cp "$config_file" "$backup_file"; then
log "INFO" "💾 已备份配置文件: $backup_file"
else
log "WARN" "⚠️ 配置文件备份失败,将继续修改(建议手动备份)"
fi
# 5. 多工具降级链修改 tableCard.color / tableCard.epd_type(适配不同操作系统)
# 优先级:jq → python3 → awk(节点级/块内精确修改,避免误伤其它 color 字段)
local modify_ok=0
# 优先级 1:jq
if [ "$modify_ok" -eq 0 ] && command -v jq >/dev/null 2>&1; then
local jq_tmp="${config_file}.jq.tmp"
if jq --arg c "$target_color" --arg e "$target_epd" \
'(.tableCard.color)=($c|tonumber) | (.tableCard.epd_type)=($e|tonumber)' \
"$config_file" > "$jq_tmp" 2>/dev/null && [ -s "$jq_tmp" ]; then
mv "$jq_tmp" "$config_file"
modify_ok=1
log "INFO" "✅ 已通过 jq 修改桌牌色彩配置"
else
rm -f "$jq_tmp"
log "WARN" "⚠️ jq 不可用或修改失败,尝试下一工具"
fi
fi
# 优先级 2:python3(标准库 json,ensure_ascii=False 保持中文不转义)
if [ "$modify_ok" -eq 0 ] && command -v python3 >/dev/null 2>&1; then
if python3 - "$config_file" "$target_color" "$target_epd" <<'PYEOF' >/dev/null 2>&1
import json, sys
path, color, epd = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
tc = data.get("tableCard")
if not isinstance(tc, dict):
sys.exit(1)
tc["color"] = color
tc["epd_type"] = epd
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
PYEOF
then
modify_ok=1
log "INFO" "✅ 已通过 python3 修改桌牌色彩配置"
else
log "WARN" "⚠️ python3 不可用或修改失败,尝试下一工具"
fi
fi
# 优先级 3:awk 作用域替换(仅在 tableCard 块内替换 color/epd_type,所有 Linux 必有)
if [ "$modify_ok" -eq 0 ]; then
local awk_tmp="${config_file}.awk.tmp"
if awk -v tc="$target_color" -v te="$target_epd" '
BEGIN { in_block = 0 }
{
if ($0 ~ /"tableCard"[[:space:]]*:/) { in_block = 1 }
if (in_block == 1) {
if ($0 ~ /^[[:space:]]*"color"[[:space:]]*:/) {
sub(/:[[:space:]]*[0-9]+/, ": " tc)
} else if ($0 ~ /^[[:space:]]*"epd_type"[[:space:]]*:/) {
sub(/:[[:space:]]*[0-9]+/, ": " te)
}
if ($0 ~ /\}/) { in_block = 0 }
}
print
}' "$config_file" > "$awk_tmp" 2>/dev/null && [ -s "$awk_tmp" ]; then
mv "$awk_tmp" "$config_file"
modify_ok=1
log "INFO" "✅ 已通过 awk 修改桌牌色彩配置"
else
rm -f "$awk_tmp"
log "WARN" "⚠️ awk 修改失败"
fi
fi
# 6. 修改结果处理(失败仅告警,不中断桌牌容器部署)
if [ "$modify_ok" -eq 0 ]; then
log "ERROR" "❌ 桌牌色彩配置修改失败(jq/python3/awk 均不可用或失败)"
log "ERROR" " 请手动修改 $config_file:tableCard.color=${target_color}, tableCard.epd_type=${target_epd}"
return 0
fi
log "INFO" "✅ 桌牌色彩配置完成:color=${target_color}, epd_type=${target_epd}(即时生效,无需重启 java-meeting)"
return 0
}
function cardtable_arm() {
local service_name="电子桌牌服务 (cardtable)"
local container_name="cardtable"
......@@ -639,6 +792,10 @@ function cardtable_arm() {
return 1
fi
# 【新增步骤】选择并配置电子桌牌色彩型号(三色/四色)
# 修改的是预定系统 java-meeting 的桌牌显示参数,配置即时生效、无需重启。
configure_tablecard_color
# 1. 检查镜像压缩包是否存在
if [ ! -f "$image_tar" ]; then
log "ERROR" "❌ 镜像文件不存在: $image_tar"
......@@ -800,6 +957,10 @@ function deploy_services() {
log "INFO" "🤖 收到 'all' 参数,自动部署所有系统"
allowed_numbers=(1 2 3 4 5)
# 非交互模式:桌牌色彩采用默认三色(跳过 whiptail 交互菜单)
# 说明:bash 动态作用域下,该 local 变量对 cardtable_arm → configure_tablecard_color 可见
local CARDTABLE_COLOR_MODE="3"
# 直接跳到部署流程,跳过所有交互界面
log "INFO" "=================================================="
log "INFO" "🚀 开始自动部署所有系统"
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论