devlake-mcp 0.4.1__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,444 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 会话生命周期管理模块
5
+
6
+ 功能:
7
+ 1. 跟踪当前活跃的 session_id/conversation_id
8
+ 2. 检测会话切换(ID变化)
9
+ 3. 自动创建新会话(调用 API)
10
+ 4. 自动结束旧会话(调用 API 更新 session_end_time)
11
+ 5. 支持 Cursor 和 Claude Code 两种模式
12
+
13
+ 使用方式:
14
+ from devlake_mcp.session_manager import start_new_session, check_and_switch_session
15
+
16
+ # 1. SessionStart hook:强制开始新会话
17
+ start_new_session(
18
+ session_id=session_id,
19
+ cwd=cwd,
20
+ ide_type='claude_code'
21
+ )
22
+ # 无论如何都会创建新会话(结束旧的 + 创建新的)
23
+
24
+ # 2. UserPromptSubmit hook:智能判断
25
+ check_and_switch_session(
26
+ new_session_id=session_id,
27
+ cwd=cwd,
28
+ ide_type='claude_code'
29
+ )
30
+ # 会智能处理:
31
+ # - 首次会话:创建 session
32
+ # - 会话延续:什么都不做
33
+ # - 会话切换:结束旧的,创建新的
34
+ """
35
+
36
+ import json
37
+ import logging
38
+ from pathlib import Path
39
+ from datetime import datetime
40
+ from typing import Optional, Dict, Union
41
+
42
+ from .client import DevLakeClient
43
+ from .utils import get_data_dir
44
+ from .retry_queue import save_failed_upload
45
+ from .git_utils import get_git_info, get_git_repo_path
46
+ from .version_utils import detect_platform_info
47
+ from .enums import IDEType
48
+ import os
49
+
50
+ # 配置日志
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ # ============================================================================
55
+ # 辅助函数
56
+ # ============================================================================
57
+
58
+ def _get_current_pid() -> int:
59
+ """获取当前进程 PID"""
60
+ return os.getpid()
61
+
62
+
63
+ def _get_state_file(ide_type: Union[IDEType, str]) -> Path:
64
+ """
65
+ 获取当前进程的状态文件路径(进程级隔离)
66
+
67
+ Args:
68
+ ide_type: IDE 类型枚举或字符串
69
+
70
+ Returns:
71
+ 状态文件路径
72
+
73
+ 格式: ~/.devlake/session_<ide_type>_<pid>.json
74
+ 例如: ~/.devlake/session_claude_code_12345.json
75
+
76
+ 说明:
77
+ 使用进程级隔离 + 持久化存储确保:
78
+ 1. 不同 IDE 类型(Claude Code/Cursor)互不干扰
79
+ 2. 同一 IDE 的多个实例互不干扰
80
+ 3. 无需文件锁,每个进程有独立状态文件
81
+ 4. 状态文件不会被系统自动清理(使用 ~/.devlake 而非 /tmp)
82
+ """
83
+ pid = _get_current_pid()
84
+ ide_type_str = ide_type.value if isinstance(ide_type, IDEType) else str(ide_type)
85
+ filename = f'session_{ide_type_str}_{pid}.json'
86
+ return get_data_dir(persistent=True) / filename
87
+
88
+
89
+ # ============================================================================
90
+ # 状态文件管理
91
+ # ============================================================================
92
+
93
+ def _read_state(ide_type: Union[IDEType, str]) -> Optional[Dict]:
94
+ """
95
+ 从状态文件读取当前会话信息
96
+
97
+ Args:
98
+ ide_type: IDE 类型枚举
99
+
100
+ Returns:
101
+ 会话状态字典,如果文件不存在或损坏返回 None
102
+
103
+ 状态格式:
104
+ {
105
+ "session_id": "abc-123",
106
+ "ide_type": "cursor",
107
+ "started_at": "2025-01-08T10:00:00"
108
+ }
109
+ """
110
+ try:
111
+ state_file = _get_state_file(ide_type)
112
+ if state_file.exists():
113
+ with open(state_file, 'r', encoding='utf-8') as f:
114
+ return json.load(f)
115
+ except Exception as e:
116
+ logger.warning(f'读取会话状态文件失败: {e}')
117
+
118
+ return None
119
+
120
+
121
+ def _write_state(session_id: str, ide_type: Union[IDEType, str]):
122
+ """
123
+ 写入会话状态到文件
124
+
125
+ Args:
126
+ session_id: 会话 ID
127
+ ide_type: IDE 类型枚举
128
+ """
129
+ try:
130
+ ide_type_str = ide_type.value if isinstance(ide_type, IDEType) else str(ide_type)
131
+ state = {
132
+ 'session_id': session_id,
133
+ 'ide_type': ide_type_str,
134
+ 'started_at': datetime.now().isoformat()
135
+ }
136
+
137
+ state_file = _get_state_file(ide_type)
138
+
139
+ # 确保目录存在
140
+ state_file.parent.mkdir(parents=True, exist_ok=True)
141
+
142
+ with open(state_file, 'w', encoding='utf-8') as f:
143
+ json.dump(state, f, ensure_ascii=False, indent=2)
144
+
145
+ logger.debug(f'会话状态已保存: {session_id} ({ide_type_str}), 文件: {state_file.name}')
146
+ except Exception as e:
147
+ logger.error(f'保存会话状态失败: {e}')
148
+
149
+
150
+ def _clear_state(ide_type: Union[IDEType, str]):
151
+ """
152
+ 清空状态文件
153
+
154
+ Args:
155
+ ide_type: IDE 类型枚举
156
+ """
157
+ try:
158
+ state_file = _get_state_file(ide_type)
159
+ if state_file.exists():
160
+ state_file.unlink()
161
+ logger.debug(f'会话状态文件已清空: {state_file.name}')
162
+ except Exception as e:
163
+ logger.warning(f'清空会话状态文件失败: {e}')
164
+
165
+
166
+ # ============================================================================
167
+ # 公开 API
168
+ # ============================================================================
169
+
170
+ def get_active_session(ide_type: IDEType = IDEType.CLAUDE_CODE) -> Optional[str]:
171
+ """
172
+ 获取当前活跃的 session_id
173
+
174
+ Args:
175
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
176
+
177
+ Returns:
178
+ 当前活跃的 session_id,如果没有活跃会话返回 None
179
+ """
180
+ state = _read_state(ide_type)
181
+ return state.get('session_id') if state else None
182
+
183
+
184
+ def set_active_session(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
185
+ """
186
+ 设置当前活跃会话
187
+
188
+ Args:
189
+ session_id: 会话 ID
190
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
191
+ """
192
+ _write_state(session_id, ide_type)
193
+
194
+
195
+ def _create_session_record(session_id: str, cwd: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
196
+ """
197
+ 创建 session 记录(上传到 DevLake API)
198
+
199
+ Args:
200
+ session_id: Session ID
201
+ cwd: 当前工作目录
202
+ ide_type: IDE 类型枚举
203
+ """
204
+ session_data = None
205
+ try:
206
+ ide_type_str = ide_type.value if isinstance(ide_type, IDEType) else str(ide_type)
207
+
208
+ # 1. 获取 Git 信息(动态 + 静态)
209
+ git_info = get_git_info(cwd, timeout=1, include_user_info=True)
210
+ git_branch = git_info.get('git_branch', 'unknown')
211
+ git_commit = git_info.get('git_commit', 'unknown')
212
+ git_author = git_info.get('git_author', 'unknown')
213
+ git_email = git_info.get('git_email', 'unknown')
214
+
215
+ # 2. 获取 Git 仓库路径(namespace/name)
216
+ git_repo_path = get_git_repo_path(cwd)
217
+
218
+ # 3. 从 git_repo_path 提取 project_name
219
+ project_name = git_repo_path.split('/')[-1] if '/' in git_repo_path else git_repo_path
220
+
221
+ # 4. 检测平台信息和版本
222
+ platform_info = detect_platform_info(ide_type=ide_type)
223
+
224
+ # 5. 构造 session 数据
225
+ session_data = {
226
+ 'session_id': session_id,
227
+ 'user_name': git_author,
228
+ 'ide_type': ide_type_str,
229
+ 'model_name': os.getenv('CLAUDE_MODEL', 'claude-sonnet-4-5'),
230
+ 'git_repo_path': git_repo_path,
231
+ 'project_name': project_name,
232
+ 'cwd': cwd,
233
+ 'session_start_time': datetime.now().isoformat(),
234
+ 'conversation_rounds': 0,
235
+ 'is_adopted': 0,
236
+ 'git_branch': git_branch,
237
+ 'git_commit': git_commit,
238
+ 'git_author': git_author,
239
+ 'git_email': git_email,
240
+ # 新增:版本信息
241
+ 'devlake_mcp_version': platform_info['devlake_mcp_version'],
242
+ 'ide_version': platform_info['ide_version'],
243
+ 'data_source': platform_info['data_source']
244
+ }
245
+
246
+ logger.info(
247
+ f'准备创建 Session: {session_id}, '
248
+ f'repo: {git_repo_path}, branch: {git_branch}, '
249
+ f'ide: {ide_type_str} {platform_info["ide_version"] or "unknown"}, '
250
+ f'devlake-mcp: {platform_info["devlake_mcp_version"]}'
251
+ )
252
+ logger.debug(f'session_data 内容: {json.dumps(session_data, ensure_ascii=False, indent=2)}')
253
+
254
+ # 5. 调用 DevLake API 创建 session
255
+ with DevLakeClient() as client:
256
+ client.post('/api/ai-coding/sessions', session_data)
257
+
258
+ logger.info(f'成功创建 Session: {session_id}')
259
+
260
+ except Exception as e:
261
+ # API 调用失败,记录错误但不阻塞
262
+ logger.error(f'创建 Session 失败 ({session_id}): {e}')
263
+ # 保存到本地队列(支持自动重试)
264
+ if session_data:
265
+ save_failed_upload(
266
+ queue_type='session',
267
+ data=session_data,
268
+ error=str(e)
269
+ )
270
+
271
+
272
+ def start_new_session(session_id: str, cwd: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
273
+ """
274
+ 强制开始新会话(SessionStart 专用)
275
+
276
+ Args:
277
+ session_id: 会话 ID
278
+ cwd: 当前工作目录(用于获取 Git 信息)
279
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
280
+
281
+ 行为:
282
+ 1. 如果有活跃会话,先结束它(无论 session_id 是否相同)
283
+ 2. 创建新会话并设置为活跃
284
+
285
+ 用途:
286
+ SessionStart hook 调用,明确表示"新会话开始"
287
+ """
288
+ # 参数验证
289
+ if not session_id:
290
+ logger.warning('session_id 为空,跳过')
291
+ return
292
+
293
+ # 转换为枚举类型(如果是字符串)
294
+ ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
295
+
296
+ # 读取当前状态(进程级)
297
+ state = _read_state(ide_type_enum)
298
+
299
+ # 如果有活跃会话,先结束它
300
+ if state:
301
+ old_session_id = state.get('session_id')
302
+ old_ide_type = state.get('ide_type', 'unknown')
303
+
304
+ if old_session_id == session_id:
305
+ logger.info(f'重新开始会话(相同 ID): {session_id}')
306
+ else:
307
+ logger.info(f'结束旧会话: {old_session_id} → 开始新会话: {session_id}')
308
+
309
+ # 结束旧会话(old_ide_type 是字符串,会被 end_session 转换)
310
+ end_session(old_session_id, old_ide_type)
311
+
312
+ # 创建新会话
313
+ logger.info(f'创建新会话: {session_id} ({ide_type_enum.value})')
314
+ _create_session_record(session_id, cwd, ide_type_enum)
315
+ set_active_session(session_id, ide_type_enum)
316
+
317
+
318
+ def check_and_switch_session(
319
+ new_session_id: str,
320
+ cwd: str,
321
+ ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE
322
+ ) -> bool:
323
+ """
324
+ 检查会话切换,智能处理会话生命周期(UserPromptSubmit 专用)
325
+
326
+ Args:
327
+ new_session_id: 新的 session_id (Claude Code) 或 conversation_id (Cursor)
328
+ cwd: 当前工作目录(用于获取 Git 信息)
329
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
330
+
331
+ Returns:
332
+ bool: 是否发生了会话切换(True 表示切换,False 表示首次或延续)
333
+
334
+ 行为:
335
+ 1. 如果是首次会话:创建 session 并设置为活跃 → 返回 False
336
+ 2. 如果是同一会话:什么都不做(会话延续) → 返回 False
337
+ 3. 如果会话切换:结束旧会话,创建并设置新会话 → 返回 True
338
+
339
+ 用途:
340
+ UserPromptSubmit hook 调用,智能判断是否需要创建会话
341
+ """
342
+ # 参数验证
343
+ if not new_session_id:
344
+ logger.warning('new_session_id 为空,跳过会话管理')
345
+ return False
346
+
347
+ # 转换为枚举类型(如果是字符串)
348
+ ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
349
+
350
+ # 读取当前状态(进程级)
351
+ state = _read_state(ide_type_enum)
352
+
353
+ # 情况1:首次会话
354
+ if not state:
355
+ logger.info(f'首次会话: {new_session_id} ({ide_type_enum.value})')
356
+ _create_session_record(new_session_id, cwd, ide_type_enum)
357
+ set_active_session(new_session_id, ide_type_enum)
358
+ return False
359
+
360
+ current_session_id = state.get('session_id')
361
+ current_ide_type = state.get('ide_type', 'unknown')
362
+
363
+ # 情况2:同一会话
364
+ if current_session_id == new_session_id:
365
+ logger.debug(f'会话延续: {new_session_id}')
366
+ return False
367
+
368
+ # 情况3:会话切换
369
+ logger.info(
370
+ f'检测到会话切换: {current_session_id} ({current_ide_type}) '
371
+ f'-> {new_session_id} ({ide_type_enum.value})'
372
+ )
373
+
374
+ # 结束旧会话(current_ide_type 是字符串,会被 end_session 转换)
375
+ end_session(current_session_id, current_ide_type)
376
+
377
+ # 创建新会话
378
+ _create_session_record(new_session_id, cwd, ide_type_enum)
379
+
380
+ # 保存新会话状态
381
+ set_active_session(new_session_id, ide_type_enum)
382
+
383
+ return True
384
+
385
+
386
+ def end_session(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
387
+ """
388
+ 结束指定会话(更新 session_end_time)
389
+
390
+ Args:
391
+ session_id: 要结束的会话 ID
392
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
393
+
394
+ 行为:
395
+ 1. 调用 DevLake API 更新 session_end_time
396
+ 2. 如果 API 调用失败,保存到重试队列
397
+ 3. 不抛出异常,静默失败
398
+ """
399
+ try:
400
+ ide_type_str = ide_type.value if isinstance(ide_type, IDEType) else str(ide_type)
401
+
402
+ # 构造更新数据
403
+ update_data = {
404
+ 'session_end_time': datetime.now().isoformat()
405
+ }
406
+
407
+ logger.info(f'准备结束会话: {session_id} ({ide_type_str})')
408
+
409
+ # 调用 DevLake API
410
+ with DevLakeClient() as client:
411
+ client.update_session(session_id, update_data)
412
+
413
+ logger.info(f'会话已结束: {session_id}')
414
+
415
+ except Exception as e:
416
+ # API 调用失败,记录错误并保存到重试队列
417
+ logger.error(f'结束会话失败 ({session_id}): {e}')
418
+
419
+ # 保存到重试队列(支持自动重试)
420
+ save_failed_upload(
421
+ queue_type='session_end',
422
+ data={
423
+ 'session_id': session_id,
424
+ 'ide_type': ide_type_str,
425
+ **update_data
426
+ },
427
+ error=str(e)
428
+ )
429
+
430
+
431
+ def clear_session(ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
432
+ """
433
+ 清空当前会话状态
434
+
435
+ Args:
436
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
437
+
438
+ 用途:
439
+ - 手动清理状态(测试或调试)
440
+ - 重置会话跟踪
441
+ """
442
+ ide_type_str = ide_type.value if isinstance(ide_type, IDEType) else str(ide_type)
443
+ _clear_state(ide_type)
444
+ logger.info(f'当前会话状态已清空 ({ide_type_str})')
devlake_mcp/utils.py ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MCP 工具函数模块
5
+
6
+ 提供跨工具的通用功能:
7
+ - 临时目录和文件管理
8
+ - 内容压缩(gzip + base64)
9
+ - 文件过滤(排除敏感文件和二进制文件)
10
+
11
+ 改进:
12
+ - 完整的类型注解
13
+ - 使用常量配置
14
+ - 更好的文档说明
15
+ - 完善的日志记录
16
+ """
17
+
18
+ import os
19
+ import gzip
20
+ import base64
21
+ import hashlib
22
+ import tempfile
23
+ import logging
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ from .constants import (
28
+ SENSITIVE_FILE_PATTERNS,
29
+ BINARY_FILE_EXTENSIONS,
30
+ TEMP_DIR_NAME,
31
+ )
32
+
33
+ # 配置日志
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # 持久化数据目录名称
37
+ DATA_DIR_NAME = '.devlake'
38
+
39
+
40
+ def get_data_dir(persistent: bool = False) -> Path:
41
+ """
42
+ 获取跨平台的数据存储目录
43
+
44
+ 优先级:
45
+ 1. 环境变量 DEVLAKE_MCP_DATA_DIR (优先级最高,覆盖 persistent 参数)
46
+ 2. persistent=True: 用户主目录 ~/.devlake (持久化存储)
47
+ 3. persistent=False: 系统临时目录/devlake_mcp (临时存储)
48
+
49
+ Args:
50
+ persistent: True 使用持久化目录 (~/.devlake),
51
+ False 使用临时目录 (系统temp/devlake_mcp)
52
+
53
+ Returns:
54
+ 数据存储目录路径
55
+
56
+ 持久化目录 (persistent=True):
57
+ - Windows: C:\\Users\\xxx\\.devlake
58
+ - macOS: /Users/xxx/.devlake
59
+ - Linux: /home/xxx/.devlake
60
+
61
+ 临时目录 (persistent=False):
62
+ - Windows: C:\\Users\\xxx\\AppData\\Local\\Temp\\devlake_mcp
63
+ - macOS: /var/folders/xxx/T/devlake_mcp
64
+ - Linux: /tmp/devlake_mcp
65
+
66
+ Examples:
67
+ >>> # 持久化存储(session state, generation state, retry queue)
68
+ >>> data_dir = get_data_dir(persistent=True)
69
+ >>> # 临时存储(before_content 快照文件)
70
+ >>> temp_dir = get_data_dir(persistent=False)
71
+ >>> # 自定义路径
72
+ >>> os.environ['DEVLAKE_MCP_DATA_DIR'] = '/custom/path'
73
+ >>> data_dir = get_data_dir() # 返回 /custom/path
74
+ """
75
+ # 优先使用环境变量
76
+ custom_dir = os.getenv('DEVLAKE_MCP_DATA_DIR')
77
+ if custom_dir:
78
+ data_dir = Path(custom_dir)
79
+ elif persistent:
80
+ # 持久化目录: ~/.devlake
81
+ data_dir = Path.home() / DATA_DIR_NAME
82
+ else:
83
+ # 临时目录: /tmp/devlake_mcp (跨平台兼容)
84
+ data_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
85
+
86
+ # 确保目录存在
87
+ data_dir.mkdir(parents=True, exist_ok=True)
88
+ return data_dir
89
+
90
+
91
+ def get_temp_file_path(session_id: str, file_path: str) -> str:
92
+ """
93
+ 生成临时文件路径(用于存储 before_content)
94
+
95
+ Args:
96
+ session_id: 会话ID
97
+ file_path: 文件路径
98
+
99
+ Returns:
100
+ 临时文件路径(格式:{temp_dir}/{session_id}_{file_hash}.before)
101
+ """
102
+ # 使用文件路径的 hash 作为文件名(避免路径过长)
103
+ file_hash = hashlib.md5(file_path.encode()).hexdigest()[:16]
104
+
105
+ # 获取跨平台临时目录(使用临时存储模式)
106
+ temp_dir = get_data_dir(persistent=False)
107
+
108
+ # 临时文件名:{session_id}_{file_hash}.before
109
+ temp_file = temp_dir / f"{session_id}_{file_hash}.before"
110
+
111
+ return str(temp_file)
112
+
113
+
114
+ def compress_content(content: str) -> str:
115
+ """
116
+ 压缩内容(gzip + base64)
117
+
118
+ Args:
119
+ content: 原始内容
120
+
121
+ Returns:
122
+ base64 编码的 gzip 压缩内容
123
+
124
+ 注意:
125
+ 如果压缩失败,会记录错误日志并返回空字符串
126
+ """
127
+ try:
128
+ if not content:
129
+ return ''
130
+
131
+ # 压缩内容
132
+ compressed = gzip.compress(content.encode('utf-8'))
133
+ encoded = base64.b64encode(compressed).decode('ascii')
134
+
135
+ # 记录压缩效果(仅在调试模式下)
136
+ original_size = len(content.encode('utf-8'))
137
+ compressed_size = len(encoded)
138
+ compression_ratio = compressed_size / original_size if original_size > 0 else 0
139
+
140
+ logger.debug(
141
+ f"内容压缩完成: {original_size}B -> {compressed_size}B "
142
+ f"(压缩率: {compression_ratio:.1%})"
143
+ )
144
+
145
+ return encoded
146
+
147
+ except Exception as e:
148
+ logger.error(f"压缩内容失败: {e}", exc_info=True)
149
+ return ''
150
+
151
+
152
+ def should_collect_file(file_path: str) -> bool:
153
+ """
154
+ 判断是否应该采集该文件
155
+
156
+ 排除规则:
157
+ 1. 敏感文件:.env, .secret, .key 等
158
+ 2. 二进制文件:图片、压缩包、可执行文件等
159
+
160
+ Args:
161
+ file_path: 文件路径
162
+
163
+ Returns:
164
+ True 表示应该采集,False 表示跳过
165
+ """
166
+ # 排除敏感文件(使用常量配置)
167
+ file_path_lower = file_path.lower()
168
+
169
+ for pattern in SENSITIVE_FILE_PATTERNS:
170
+ if pattern in file_path_lower:
171
+ return False
172
+
173
+ # 排除二进制文件(通过后缀判断,使用常量配置)
174
+ file_ext = Path(file_path).suffix.lower()
175
+ if file_ext in BINARY_FILE_EXTENSIONS:
176
+ return False
177
+
178
+ return True
179
+
180
+
181
+ def get_file_type(file_path: str) -> str:
182
+ """
183
+ 获取文件类型(扩展名)
184
+
185
+ Args:
186
+ file_path: 文件路径
187
+
188
+ Returns:
189
+ 文件扩展名(不含点),如果没有扩展名返回 'unknown'
190
+ """
191
+ return Path(file_path).suffix.lstrip('.') or 'unknown'
192
+
193
+
194
+ def read_file_content(file_path: str) -> str:
195
+ """
196
+ 读取文件内容
197
+
198
+ Args:
199
+ file_path: 文件路径
200
+
201
+ Returns:
202
+ 文件内容,读取失败返回空字符串
203
+
204
+ 注意:
205
+ 如果文件不存在或读取失败,会记录警告日志并返回空字符串
206
+ """
207
+ try:
208
+ if not os.path.exists(file_path):
209
+ logger.debug(f"文件不存在: {file_path}")
210
+ return ''
211
+
212
+ with open(file_path, 'r', encoding='utf-8') as f:
213
+ content = f.read()
214
+ logger.debug(f"成功读取文件: {file_path} ({len(content)} 字符)")
215
+ return content
216
+
217
+ except UnicodeDecodeError as e:
218
+ logger.warning(f"文件编码错误 (可能是二进制文件): {file_path} - {e}")
219
+ return ''
220
+ except PermissionError as e:
221
+ logger.warning(f"无权限读取文件: {file_path} - {e}")
222
+ return ''
223
+ except Exception as e:
224
+ logger.error(f"读取文件失败: {file_path} - {e}", exc_info=True)
225
+ return ''