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,296 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Generation 生命周期管理模块
5
+
6
+ 功能:
7
+ 1. 管理 generation_id 的生命周期(创建、获取、结束)
8
+ 2. 支持跨 hook 的 generation 追踪
9
+ 3. 为每次 AI 交互分配唯一标识
10
+
11
+ 设计:
12
+ - generation_id: 一次完整 AI 交互的唯一标识(UUID)
13
+ - 状态文件: ~/.devlake/generation_state.json
14
+ - 与 session_id 关联
15
+
16
+ 使用方式:
17
+ from devlake_mcp.generation_manager import start_generation, get_current_generation_id, end_generation
18
+
19
+ # 在 UserPromptSubmit 中创建
20
+ generation_id = start_generation(session_id)
21
+
22
+ # 在 PostToolUse 中获取
23
+ generation_id = get_current_generation_id(session_id)
24
+
25
+ # 在 Stop 中结束
26
+ end_generation(session_id)
27
+ """
28
+
29
+ import json
30
+ import logging
31
+ import uuid
32
+ import os
33
+ from pathlib import Path
34
+ from datetime import datetime
35
+ from typing import Optional, Dict, Union
36
+
37
+ from .utils import get_data_dir
38
+ from .enums import IDEType
39
+
40
+ # 配置日志
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ # ============================================================================
45
+ # 辅助函数
46
+ # ============================================================================
47
+
48
+ def _get_current_pid() -> int:
49
+ """获取当前进程 PID"""
50
+ return os.getpid()
51
+
52
+
53
+ def _get_generation_state_file(ide_type: IDEType) -> Path:
54
+ """
55
+ 获取当前进程的 generation 状态文件路径(进程级隔离)
56
+
57
+ Args:
58
+ ide_type: IDE 类型枚举
59
+
60
+ Returns:
61
+ 状态文件路径
62
+
63
+ 格式: ~/.devlake/generation_<ide_type>_<pid>.json
64
+ 例如: ~/.devlake/generation_claude_code_12345.json
65
+
66
+ 说明:
67
+ 使用进程级隔离 + 持久化存储确保:
68
+ 1. 不同 IDE 类型(Claude Code/Cursor)互不干扰
69
+ 2. 同一 IDE 的多个实例互不干扰
70
+ 3. 无需文件锁,每个进程有独立状态文件
71
+ 4. 状态文件不会被系统自动清理(使用 ~/.devlake 而非 /tmp)
72
+ """
73
+ pid = _get_current_pid()
74
+ ide_type_str = ide_type.value
75
+ filename = f'generation_{ide_type_str}_{pid}.json'
76
+ return get_data_dir(persistent=True) / filename
77
+
78
+
79
+ # ============================================================================
80
+ # 状态文件管理(私有函数)
81
+ # ============================================================================
82
+
83
+ def _read_generation_state(ide_type: IDEType) -> Optional[Dict]:
84
+ """
85
+ 从状态文件读取 generation 信息
86
+
87
+ Args:
88
+ ide_type: IDE 类型枚举
89
+
90
+ Returns:
91
+ Generation 状态字典,如果文件不存在或损坏返回 None
92
+
93
+ 状态格式:
94
+ {
95
+ "session_id": "abc-123",
96
+ "generation_id": "gen-uuid-456",
97
+ "started_at": "2025-01-08T10:00:00"
98
+ }
99
+ """
100
+ try:
101
+ state_file = _get_generation_state_file(ide_type)
102
+ if state_file.exists():
103
+ with open(state_file, 'r', encoding='utf-8') as f:
104
+ return json.load(f)
105
+ except Exception as e:
106
+ logger.warning(f'读取 generation 状态文件失败: {e}')
107
+
108
+ return None
109
+
110
+
111
+ def _write_generation_state(session_id: str, generation_id: str, ide_type: IDEType):
112
+ """
113
+ 写入 generation 状态到文件
114
+
115
+ Args:
116
+ session_id: 会话 ID
117
+ generation_id: Generation ID(UUID)
118
+ ide_type: IDE 类型枚举
119
+ """
120
+ try:
121
+ state = {
122
+ 'session_id': session_id,
123
+ 'generation_id': generation_id,
124
+ 'started_at': datetime.now().isoformat()
125
+ }
126
+
127
+ state_file = _get_generation_state_file(ide_type)
128
+
129
+ # 确保目录存在
130
+ state_file.parent.mkdir(parents=True, exist_ok=True)
131
+
132
+ with open(state_file, 'w', encoding='utf-8') as f:
133
+ json.dump(state, f, ensure_ascii=False, indent=2)
134
+
135
+ logger.debug(f'Generation 状态已保存: session={session_id}, generation={generation_id}, 文件: {state_file.name}')
136
+ except Exception as e:
137
+ logger.error(f'保存 generation 状态失败: {e}')
138
+
139
+
140
+ def _clear_generation_state(ide_type: IDEType):
141
+ """
142
+ 清空 generation 状态文件
143
+
144
+ Args:
145
+ ide_type: IDE 类型枚举
146
+ """
147
+ try:
148
+ state_file = _get_generation_state_file(ide_type)
149
+ if state_file.exists():
150
+ state_file.unlink()
151
+ logger.debug(f'Generation 状态文件已清空: {state_file.name}')
152
+ except Exception as e:
153
+ logger.warning(f'清空 generation 状态文件失败: {e}')
154
+
155
+
156
+ # ============================================================================
157
+ # 公开 API
158
+ # ============================================================================
159
+
160
+ def start_generation(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE) -> str:
161
+ """
162
+ 开始新的 generation(生成并保存 generation_id)
163
+
164
+ Args:
165
+ session_id: 会话 ID
166
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
167
+
168
+ Returns:
169
+ 新生成的 generation_id(UUID 字符串)
170
+
171
+ 用途:
172
+ - 在 UserPromptSubmit Hook 中调用
173
+ - 为每次用户输入分配唯一的 generation_id
174
+ - 后续所有操作(工具调用、文件变更、响应)都关联到该 generation_id
175
+
176
+ 示例:
177
+ generation_id = start_generation("session-123", ide_type=IDEType.CLAUDE_CODE)
178
+ # generation_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
179
+ """
180
+ if not session_id:
181
+ logger.warning('session_id 为空,无法创建 generation')
182
+ return ''
183
+
184
+ # 转换为枚举类型(如果是字符串)
185
+ ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
186
+
187
+ # 生成 UUID 作为 generation_id
188
+ generation_id = str(uuid.uuid4())
189
+
190
+ # 保存到状态文件(进程级)
191
+ _write_generation_state(session_id, generation_id, ide_type_enum)
192
+
193
+ logger.info(f'新 generation 已创建: session={session_id}, generation={generation_id} ({ide_type_enum.value})')
194
+
195
+ return generation_id
196
+
197
+
198
+ def get_current_generation_id(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE) -> Optional[str]:
199
+ """
200
+ 获取当前 session 的活跃 generation_id
201
+
202
+ Args:
203
+ session_id: 会话 ID
204
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
205
+
206
+ Returns:
207
+ 当前的 generation_id,如果不存在返回 None
208
+
209
+ 用途:
210
+ - 在 PreToolUse/PostToolUse 中获取当前 generation_id
211
+ - 在 Stop Hook 中获取 generation_id 以更新 prompt
212
+ - 关联文件变更到具体的 prompt
213
+
214
+ 示例:
215
+ generation_id = get_current_generation_id("session-123", ide_type=IDEType.CLAUDE_CODE)
216
+ if generation_id:
217
+ # 使用 generation_id 关联数据
218
+ ...
219
+ """
220
+ if not session_id:
221
+ logger.debug('session_id 为空,无法获取 generation_id')
222
+ return None
223
+
224
+ # 转换为枚举类型(如果是字符串)
225
+ ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
226
+
227
+ state = _read_generation_state(ide_type_enum)
228
+
229
+ # 验证 session_id 是否匹配
230
+ if state and state.get('session_id') == session_id:
231
+ generation_id = state.get('generation_id')
232
+ logger.debug(f'获取到 generation_id: {generation_id} (session: {session_id}, ide: {ide_type_enum.value})')
233
+ return generation_id
234
+
235
+ logger.debug(f'未找到匹配的 generation_id (session: {session_id}, ide: {ide_type_enum.value})')
236
+ return None
237
+
238
+
239
+ def end_generation(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
240
+ """
241
+ 结束当前 generation(清空状态)
242
+
243
+ Args:
244
+ session_id: 会话 ID
245
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
246
+
247
+ 用途:
248
+ - 在 Stop Hook 中调用
249
+ - 标记当前 generation 已完成
250
+ - 清理状态文件,为下一次 generation 做准备
251
+
252
+ 注意:
253
+ - 只有当 session_id 匹配时才清空状态
254
+ - 如果 session_id 不匹配,不会清空(避免误删其他会话的状态)
255
+
256
+ 示例:
257
+ end_generation("session-123", ide_type=IDEType.CLAUDE_CODE)
258
+ """
259
+ if not session_id:
260
+ logger.warning('session_id 为空,无法结束 generation')
261
+ return
262
+
263
+ # 转换为枚举类型(如果是字符串)
264
+ ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
265
+
266
+ state = _read_generation_state(ide_type_enum)
267
+
268
+ # 验证 session_id 是否匹配
269
+ if state and state.get('session_id') == session_id:
270
+ generation_id = state.get('generation_id')
271
+ _clear_generation_state(ide_type_enum)
272
+ logger.info(f'Generation 已结束: session={session_id}, generation={generation_id} ({ide_type_enum.value})')
273
+ else:
274
+ logger.debug(f'session_id 不匹配或状态不存在,跳过清理 (session: {session_id}, ide: {ide_type_enum.value})')
275
+
276
+
277
+ def clear_generation(ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
278
+ """
279
+ 清空 generation 状态(强制清理)
280
+
281
+ Args:
282
+ ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
283
+
284
+ 用途:
285
+ - 手动清理状态(测试或调试)
286
+ - 重置 generation 追踪
287
+
288
+ 注意:
289
+ - 不验证 session_id,直接清空
290
+ - 慎用,可能影响正在进行的交互
291
+ """
292
+ # 转换为枚举类型(如果是字符串)
293
+ ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
294
+
295
+ _clear_generation_state(ide_type_enum)
296
+ logger.info(f'Generation 状态已强制清空 ({ide_type_enum.value})')