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,204 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 用户提示词提交时记录会话信息(UserPromptSubmit Hook)
5
+
6
+ 触发时机: 用户点击发送按钮后、发起后端请求之前
7
+
8
+ Claude Code 输入格式:
9
+ {
10
+ "session_id": "abc123",
11
+ "transcript_path": "/Users/.../.claude/projects/.../xxx.jsonl",
12
+ "cwd": "/Users/...",
13
+ "permission_mode": "default",
14
+ "hook_event_name": "UserPromptSubmit",
15
+ "prompt": "Write a function to calculate the factorial of a number"
16
+ }
17
+
18
+ 功能:
19
+ 1. 调用 session_manager.check_and_switch_session() 自动处理会话生命周期
20
+ - 首次会话:创建 session
21
+ - 会话延续:什么都不做
22
+ - 会话切换:结束旧的,创建新的
23
+ 2. 上传用户的 prompt 内容(记录用户输入)
24
+ 3. 静默退出,不阻塞用户操作
25
+
26
+ 数据流:
27
+ - Session: 由 session_manager 自动管理
28
+ - Prompt: 每次用户输入 → POST /api/ai-coding/prompts
29
+
30
+ 注意:
31
+ - 所有会话管理逻辑已集中到 session_manager 模块
32
+ - API 调用使用 try-except 确保不阻塞用户
33
+ - 异步执行,立即返回
34
+ """
35
+
36
+ import sys
37
+ import os
38
+ import json
39
+ import logging
40
+ from datetime import datetime
41
+ from pathlib import Path
42
+
43
+ # 导入公共工具
44
+ from devlake_mcp.hooks.hook_utils import run_async
45
+ from devlake_mcp.client import DevLakeClient
46
+ from devlake_mcp.git_utils import get_git_info, get_git_repo_path
47
+ from devlake_mcp.retry_queue import save_failed_upload
48
+ from devlake_mcp.session_manager import check_and_switch_session
49
+ from devlake_mcp.generation_manager import start_generation
50
+ from devlake_mcp.logging_config import configure_logging, get_log_dir
51
+ from devlake_mcp.constants import HOOK_LOG_DIR
52
+ from devlake_mcp.enums import IDEType
53
+
54
+ # 配置日志(启动时调用一次)
55
+ configure_logging(log_dir=get_log_dir(HOOK_LOG_DIR), log_file='user_prompt_submit.log')
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ def upload_prompt(
60
+ session_id: str,
61
+ prompt_content: str,
62
+ cwd: str,
63
+ transcript_path: str = None,
64
+ permission_mode: str = 'default'
65
+ ):
66
+ """
67
+ 上传 Prompt 记录到 DevLake API
68
+
69
+ Args:
70
+ session_id: Session ID
71
+ prompt_content: 用户输入的 prompt 文本
72
+ cwd: 当前工作目录
73
+ transcript_path: 转录文件路径(可选)
74
+ """
75
+ prompt_data = None # 初始化,确保 except 块可访问
76
+ try:
77
+ # 1. 获取 Git 信息(动态 + 静态)
78
+ git_info = get_git_info(cwd, timeout=1, include_user_info=True)
79
+ git_author = git_info.get('git_author', 'unknown')
80
+
81
+ # 2. 获取 Git 仓库路径
82
+ git_repo_path = get_git_repo_path(cwd)
83
+
84
+ # 3. 从 git_repo_path 提取 project_name
85
+ project_name = git_repo_path.split('/')[-1] if '/' in git_repo_path else git_repo_path
86
+
87
+ # 4. 生成 prompt_uuid(使用 generation_id)
88
+ prompt_uuid = start_generation(session_id, ide_type=IDEType.CLAUDE_CODE)
89
+ logger.debug(f'生成 generation_id: {prompt_uuid}')
90
+
91
+ # 5. 获取 prompt_sequence(必填字段)
92
+ with DevLakeClient() as client:
93
+ # 先获取下一个序号
94
+ next_seq_response = client.get('/api/ai-coding/prompts/next-sequence', params={'session_id': session_id})
95
+ prompt_sequence = next_seq_response.get('next_sequence', 1)
96
+ logger.debug(f'获取 prompt_sequence: {prompt_sequence}')
97
+
98
+ # 6. 构造 prompt 数据
99
+ prompt_data = {
100
+ 'session_id': session_id,
101
+ 'prompt_uuid': prompt_uuid,
102
+ 'prompt_sequence': prompt_sequence, # 必填字段
103
+ 'prompt_content': prompt_content,
104
+ 'prompt_submit_time': datetime.now().isoformat(), # API 使用 prompt_submit_time
105
+ 'cwd': cwd, # 当前工作目录
106
+ 'permission_mode': permission_mode # 权限模式
107
+ }
108
+
109
+ # 添加 transcript_path(如果有)
110
+ if transcript_path:
111
+ prompt_data['transcript_path'] = transcript_path
112
+
113
+ logger.info(f'准备上传 Prompt: {session_id}, prompt_uuid: {prompt_uuid}, sequence: {prompt_sequence}, content: {prompt_content[:50]}...')
114
+
115
+ # 7. 调用 DevLake API 创建 prompt
116
+ with DevLakeClient() as client:
117
+ client.create_prompt(prompt_data)
118
+
119
+ logger.info(f'成功上传 Prompt: {prompt_uuid}')
120
+
121
+ except Exception as e:
122
+ # API 调用失败,记录错误但不阻塞
123
+ logger.error(
124
+ f'上传 Prompt 失败 ({session_id}): '
125
+ f'异常类型={type(e).__name__}, '
126
+ f'错误信息={str(e)}',
127
+ exc_info=True # 记录完整堆栈信息
128
+ )
129
+ # 保存到本地队列(支持自动重试)
130
+ if prompt_data:
131
+ save_failed_upload(
132
+ queue_type='prompt',
133
+ data=prompt_data,
134
+ error=str(e)
135
+ )
136
+
137
+
138
+ @run_async
139
+ def main():
140
+ """
141
+ UserPromptSubmit Hook 主逻辑
142
+ """
143
+ try:
144
+ # 1. 从 stdin 读取 hook 输入
145
+ input_data = json.load(sys.stdin)
146
+
147
+ # 2. 获取关键字段
148
+ session_id = input_data.get('session_id')
149
+ prompt_content = input_data.get('prompt', '')
150
+ transcript_path = input_data.get('transcript_path')
151
+ permission_mode = input_data.get('permission_mode', 'default')
152
+
153
+ # 注意:如果 cwd 是空字符串,也应该使用 os.getcwd()
154
+ raw_cwd = input_data.get('cwd')
155
+ logger.debug(f'input_data 中的 cwd 原始值: {repr(raw_cwd)}')
156
+
157
+ cwd = raw_cwd or os.getcwd()
158
+ logger.debug(f'最终使用的 cwd: {cwd}')
159
+
160
+ if not session_id:
161
+ logger.error('未获取到 session_id,跳过处理')
162
+ sys.exit(0)
163
+ return # 确保退出(测试时 sys.exit 被 mock)
164
+
165
+ if not prompt_content:
166
+ logger.debug('未获取到 prompt 内容,跳过上传')
167
+ sys.exit(0)
168
+ return # 确保退出(测试时 sys.exit 被 mock)
169
+
170
+ logger.debug(f'UserPromptSubmit 触发 - session_id: {session_id}, prompt: {prompt_content[:50]}...')
171
+
172
+ # 3. 会话管理(自动处理首次会话、会话切换、会话延续)
173
+ try:
174
+ check_and_switch_session(
175
+ new_session_id=session_id,
176
+ cwd=cwd,
177
+ ide_type=IDEType.CLAUDE_CODE
178
+ )
179
+ except Exception as e:
180
+ logger.error(f'会话管理失败: {e}')
181
+
182
+ # 4. 上传 prompt(记录用户输入)
183
+ try:
184
+ upload_prompt(
185
+ session_id=session_id,
186
+ prompt_content=prompt_content,
187
+ cwd=cwd,
188
+ transcript_path=transcript_path,
189
+ permission_mode=permission_mode
190
+ )
191
+ except Exception as e:
192
+ logger.error(f'上传 prompt 失败: {e}')
193
+
194
+ # 成功,正常退出
195
+ sys.exit(0)
196
+
197
+ except Exception as e:
198
+ # 任何异常都静默失败(不阻塞用户)
199
+ logger.error(f'UserPromptSubmit Hook 执行失败: {e}', exc_info=True)
200
+ sys.exit(0)
201
+
202
+
203
+ if __name__ == '__main__':
204
+ main()
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 日志配置模块(简化版)
5
+
6
+ 提供符合 Python logging 最佳实践的配置函数。
7
+
8
+ 使用方法:
9
+ # 在应用/hook 启动时调用一次
10
+ from devlake_mcp.logging_config import configure_logging, get_log_dir
11
+ configure_logging(log_dir=get_log_dir('.claude/logs'), log_file='hook.log')
12
+
13
+ # 之后各个模块直接用标准方式
14
+ import logging
15
+ logger = logging.getLogger(__name__)
16
+ logger.info('message')
17
+ """
18
+
19
+ import os
20
+ import logging
21
+ from pathlib import Path
22
+ from typing import Optional
23
+
24
+ from .constants import VALID_LOG_LEVELS, DEFAULT_LOG_LEVEL
25
+
26
+
27
+ def get_log_dir(default_dir: str) -> str:
28
+ """
29
+ 获取日志目录路径
30
+
31
+ 根据配置文件的位置和内容,自动选择项目目录或全局目录。
32
+
33
+ Args:
34
+ default_dir: 默认日志目录(相对于项目根目录),如 '.claude/logs' 或 '.cursor/logs'
35
+
36
+ Returns:
37
+ str: 日志目录路径
38
+
39
+ 优先级逻辑:
40
+ 1. 优先检查项目配置(./.claude/settings.json 或 ./.cursor/hooks.json)
41
+ - Claude Code: 检查 settings.json 中是否有 "hooks" 配置
42
+ - Cursor: 检查 hooks.json 是否存在且有效
43
+ 2. 如果项目配置存在且有效,使用项目日志目录
44
+ 3. 否则检查全局配置(~/.claude/settings.json 或 ~/.cursor/hooks.json)
45
+ 4. 如果全局配置存在且有效,使用全局日志目录
46
+ 5. 最后使用项目日志目录作为默认值
47
+ """
48
+ import json
49
+
50
+ home = Path.home()
51
+ cwd = Path.cwd()
52
+
53
+ # 根据 default_dir 判断是 Claude Code 还是 Cursor
54
+ if '.claude' in default_dir:
55
+ project_config = cwd / ".claude" / "settings.json"
56
+ global_config = home / ".claude" / "settings.json"
57
+ project_log_dir = str(cwd / ".claude" / "logs")
58
+ global_log_dir = str(home / ".claude" / "logs")
59
+
60
+ # 1. 优先检查项目配置
61
+ if project_config.exists():
62
+ try:
63
+ with open(project_config, 'r', encoding='utf-8') as f:
64
+ config = json.load(f)
65
+ # 检查是否有 hooks 配置
66
+ if 'hooks' in config and config['hooks']:
67
+ return project_log_dir
68
+ except Exception:
69
+ pass
70
+
71
+ # 2. 检查全局配置
72
+ if global_config.exists():
73
+ try:
74
+ with open(global_config, 'r', encoding='utf-8') as f:
75
+ config = json.load(f)
76
+ # 检查是否有 hooks 配置
77
+ if 'hooks' in config and config['hooks']:
78
+ return global_log_dir
79
+ except Exception:
80
+ pass
81
+
82
+ elif '.cursor' in default_dir:
83
+ project_config = cwd / ".cursor" / "hooks.json"
84
+ global_config = home / ".cursor" / "hooks.json"
85
+ project_log_dir = str(cwd / ".cursor" / "logs")
86
+ global_log_dir = str(home / ".cursor" / "logs")
87
+
88
+ # 1. 优先检查项目配置
89
+ if project_config.exists():
90
+ try:
91
+ with open(project_config, 'r', encoding='utf-8') as f:
92
+ config = json.load(f)
93
+ # 检查是否有有效的 hooks 配置
94
+ if config.get('version') and 'hooks' in config and config['hooks']:
95
+ return project_log_dir
96
+ except Exception:
97
+ pass
98
+
99
+ # 2. 检查全局配置
100
+ if global_config.exists():
101
+ try:
102
+ with open(global_config, 'r', encoding='utf-8') as f:
103
+ config = json.load(f)
104
+ # 检查是否有有效的 hooks 配置
105
+ if config.get('version') and 'hooks' in config and config['hooks']:
106
+ return global_log_dir
107
+ except Exception:
108
+ pass
109
+
110
+ # 3. 默认使用项目日志目录
111
+ return default_dir
112
+
113
+
114
+ def configure_logging(
115
+ log_dir: Optional[str] = None,
116
+ log_file: Optional[str] = None
117
+ ):
118
+ """
119
+ 配置全局 logging(在应用启动时调用一次)
120
+
121
+ 根据环境变量配置日志行为:
122
+ - DEVLAKE_MCP_LOGGING_ENABLED: 是否启用(默认 true)
123
+ - DEVLAKE_MCP_LOG_LEVEL: 日志级别(默认 INFO)
124
+ - DEVLAKE_MCP_CONSOLE_LOG: 是否输出到控制台(默认 false,仅在开发调试时启用)
125
+
126
+ Args:
127
+ log_dir: 日志文件目录(可选)
128
+ log_file: 日志文件名(可选)
129
+
130
+ 示例:
131
+ >>> from devlake_mcp.logging_config import configure_logging
132
+ >>> configure_logging(log_dir='.claude/logs', log_file='hook.log')
133
+ >>>
134
+ >>> import logging
135
+ >>> logger = logging.getLogger(__name__)
136
+ >>> logger.info('Hello')
137
+
138
+ 注意:
139
+ - 默认情况下,日志只写入文件,不输出到控制台
140
+ - 控制台输出会在 IDE hook 界面显示为 Error Output(stderr)
141
+ - 如需调试,可设置环境变量 DEVLAKE_MCP_CONSOLE_LOG=true 启用控制台输出
142
+ """
143
+ # 读取环境变量
144
+ enabled = os.getenv('DEVLAKE_MCP_LOGGING_ENABLED', 'true').lower() in ('true', '1', 'yes')
145
+ level_str = os.getenv('DEVLAKE_MCP_LOG_LEVEL', DEFAULT_LOG_LEVEL).upper()
146
+
147
+ # 获取日志级别
148
+ level = VALID_LOG_LEVELS.get(level_str, VALID_LOG_LEVELS[DEFAULT_LOG_LEVEL])
149
+
150
+ # 如果禁用,使用 NullHandler
151
+ if not enabled:
152
+ logging.basicConfig(
153
+ level=level,
154
+ handlers=[logging.NullHandler()]
155
+ )
156
+ return
157
+
158
+ # 准备 handlers
159
+ handlers = []
160
+ formatter = logging.Formatter(
161
+ '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
162
+ datefmt='%Y-%m-%d %H:%M:%S'
163
+ )
164
+
165
+ # 文件 handler(如果提供了 log_dir 和 log_file)
166
+ if log_dir and log_file:
167
+ try:
168
+ Path(log_dir).mkdir(parents=True, exist_ok=True)
169
+ file_handler = logging.FileHandler(
170
+ Path(log_dir) / log_file,
171
+ encoding='utf-8'
172
+ )
173
+ file_handler.setFormatter(formatter)
174
+ file_handler.setLevel(level)
175
+ handlers.append(file_handler)
176
+ except Exception as e:
177
+ # 创建文件 handler 失败,只用控制台
178
+ print(f"警告:无法创建日志文件 {log_dir}/{log_file}: {e}")
179
+
180
+ # 控制台 handler(仅在开发调试时启用)
181
+ # 通过环境变量 DEVLAKE_MCP_CONSOLE_LOG=true 启用
182
+ if os.getenv('DEVLAKE_MCP_CONSOLE_LOG', 'false').lower() in ('true', '1', 'yes'):
183
+ import sys
184
+ console_handler = logging.StreamHandler(sys.stdout) # 使用 stdout 而不是 stderr
185
+ console_handler.setFormatter(formatter)
186
+ console_handler.setLevel(level)
187
+ handlers.append(console_handler)
188
+
189
+ # 如果没有任何 handler,添加 NullHandler(避免 logging 警告)
190
+ if not handlers:
191
+ handlers.append(logging.NullHandler())
192
+
193
+ # 配置全局 logging
194
+ logging.basicConfig(
195
+ level=level,
196
+ handlers=handlers,
197
+ force=True # 覆盖已有配置
198
+ )
199
+
200
+ # 抑制第三方库的 DEBUG 日志
201
+ for lib in ['urllib3', 'urllib3.connectionpool', 'requests']:
202
+ logging.getLogger(lib).setLevel(logging.WARNING)