Skill: genify/lsp-python (version 1.1.0)

--- name: lsp-python description: "Python code quality checking and LSP integration using pylsp. Provides code diagnostics, completion, hover tips, and style analysis. Use when: checking Python errors/warnings, getting code completions, viewing function signatures, analyzing code quality, or fixing style issues." --- # LSP Python 技能 使用 Python Language Server Protocol (LSP) 进行代码质量检查和智能分析。 ## 快速开始 ### 1. 检查代码问题 ```bash # 单个文件 python3 scripts/lsp-service.py check <文件路径> # 批量检查 (推荐) python3 scripts/check_python.py <文件或目录> # 批量检查并自动修复 python3 scripts/check_python.py --auto-fix <文件或目录> ``` 示例: ```bash python3 scripts/lsp-service.py check my_script.py python3 scripts/check_python.py src/ python3 scripts/check_python.py --auto-fix src/ ``` ### 2. 获取代码补全 ```bash python3 scripts/lsp-service.py complete <文件> <行号> <字符位置> ``` ### 3. 查看符号信息 ```bash python3 scripts/lsp-service.py info <文件> <行号> <字符位置> ``` ## 依赖 - **Python 3.x** - **pylsp**: `pip install python-lsp-server` - **可选插件**: - `pip install python-lsp-server[all]` - 完整插件集 - `pip install pylsp-mypy` - 类型检查 - `pip install pylsp-black` - black 格式化 ## 核心功能 ### 代码诊断 (check) 检查 Python 文件中的错误和警告: - **pyflakes** - 代码错误检测 (未使用导入、未定义变量等) - **pycodestyle** - PEP8 风格检查 (格式、行长、空白等) 输出示例: ``` ⚠️ 第 3 行 [pyflakes]: 'os' imported but unused ⚠️ 第 6 行 [pycodestyle]: E302 expected 2 blank lines, found 1 ✅ 没有发现问题 ``` ### 代码补全 (complete) 获取指定位置的代码补全建议: ```bash python3 scripts/lsp-service.py complete script.py 5 10 ``` 输出: ``` 补全建议: • json (模块) • jsonpatch (模块) • requests (模块) ``` ### 悬停提示 (info) 查看函数签名、文档字符串等信息: ```bash python3 scripts/lsp-service.py info script.py 10 5 ``` ### 跳转定义 (goto) 查找符号的定义位置: ```bash python3 scripts/lsp-service.py goto script.py 15 10 ``` ## 自动修复代码问题 ### 清理未使用的导入 ```bash pip install autoflake autoflake --remove-all-unused-imports --in-place --recursive . ``` ### 格式化代码 ```bash pip install black black . ``` ### 完整修复流程 ```bash # 1. 备份 cp -r project/ project.backup # 2. 清理导入 autoflake --remove-all-unused-imports --in-place --recursive project/ # 3. 格式化 black project/ # 4. 验证 python3 scripts/lsp-service.py check project/main.py ``` ## 诊断严重性级别 | 级别 | 代码 | 含义 | |------|------|------| | ❌ | 1 | Error (错误) | | ⚠️ | 2 | Warning (警告) | | ℹ️ | 3 | Information (信息) | | 💡 | 4 | Hint (提示) | ## 常见问题代码 | 代码 | 含义 | 修复方法 | |------|------|----------| | E402 | 导入不在文件顶部 | 移动导入到文件开头 | | E501 | 行太长 (>79 字符) | 拆分长行或使用括号 | | W293 | 空行包含空白字符 | 删除行尾空格 | | E302 | 缺少空行 | 函数/类定义前加 2 个空行 | | E712 | 布尔比较风格 | `if x is True` → `if x` | ## 在 OpenClaw 中使用 ```bash exec: python3 /path/to/lsp-python/scripts/lsp-service.py check <file> ``` ## 批量检查项目 ```bash # 检查所有 Python 文件 find . -name "*.py" -exec python3 scripts/lsp-service.py check {} \; # 仅显示有问题的文件 for f in $(find . -name "*.py"); do result=$(python3 scripts/lsp-service.py check "$f" 2>&1) if ! echo "$result" | grep -q "✅ 没有发现问题"; then echo "=== $f ===" echo "$result" fi done ``` ## 参考资料 - **LSP 协议详解**: 见 `references/lsp-protocol.md` - **pylsp 配置**: 见 `references/pylsp-config.md` - **代码风格指南**: 见 `references/pep8-guide.md` ## 故障排除 ### pylsp 无法启动 ```bash # 检查安装 which pylsp pylsp --version # 重新安装 pip install --upgrade python-lsp-server ``` ### 检查超时 增加脚本中的 `LSP_TIMEOUT` 值 (默认 10 秒)。 ### 中文字符问题 确保文件使用 UTF-8 编码，脚本已设置 `ensure_ascii=False`。

path: references/lsp-protocol.md
content: # LSP 协议详解

## 什么是 LSP

**Language Server Protocol (LSP)** 是由 Microsoft 开发的开放协议，用于编辑器/IDE 与语言服务器之间的通信。

## 核心架构

```
┌─────────────────┐         JSON-RPC         ┌─────────────────┐
│   编辑器/客户端   │ ←──────────────────────→ │   语言服务器     │
│  (VSCode/Nvim)  │      (stdin/stdout)      │  (pylsp/ts 等)  │
└─────────────────┘                          └─────────────────┘
```

## JSON-RPC 消息格式

### 请求格式

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/completion",
  "params": {
    "textDocument": {"uri": "file:///path/to/file.py"},
    "position": {"line": 0, "character": 0}
  }
}
```

### 响应格式

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "isIncomplete": false,
    "items": [...]
  }
}
```

### 通知格式 (无响应)

```json
{
  "jsonrpc": "2.0",
  "method": "textDocument/didOpen",
  "params": {...}
}
```

## LSP 消息头

```
Content-Length: <length>\r\n
\r\n
<message-body>
```

## 核心能力

| 功能 | 方法 | 说明 |
|------|------|------|
| 自动补全 | `textDocument/completion` | 获取代码补全建议 |
| 跳转定义 | `textDocument/definition` | 查找符号定义 |
| 查找引用 | `textDocument/references` | 查找所有引用 |
| 悬停提示 | `textDocument/hover` | 获取符号信息 |
| 诊断检查 | `textDocument/publishDiagnostics` | 错误/警告通知 |
| 符号搜索 | `workspace/symbol` | 工作区符号搜索 |
| 格式化 | `textDocument/formatting` | 代码格式化 |
| 重命名 | `textDocument/rename` | 符号重命名 |
| 代码动作 | `textDocument/codeAction` | 快速修复等 |

## 生命周期

### 1. Initialize

客户端发送能力协商:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "processId": 12345,
    "rootUri": "file:///path/to/workspace",
    "capabilities": {...},
    "trace": "off"
  }
}
```

### 2. Initialized

服务器确认就绪:

```json
{
  "jsonrpc": "2.0",
  "method": "initialized",
  "params": {}
}
```

### 3. Document Open

文件打开时通知服务器:

```json
{
  "jsonrpc": "2.0",
  "method": "textDocument/didOpen",
  "params": {
    "textDocument": {
      "uri": "file:///path/to/file.py",
      "languageId": "python",
      "version": 1,
      "text": "..."
    }
  }
}
```

### 4. Document Change

内容变更时同步:

```json
{
  "jsonrpc": "2.0",
  "method": "textDocument/didChange",
  "params": {
    "textDocument": {"uri": "...", "version": 2},
    "contentChanges": [{"text": "..."}]
  }
}
```

### 5. Shutdown

优雅退出:

```json
{"jsonrpc": "2.0", "id": 99, "method": "shutdown", "params": {}}
{"jsonrpc": "2.0", "method": "exit"}
```

## 文档同步模式

- **Full Sync**: 每次变更发送完整内容
- **Incremental Sync**: 只发送变更范围 (range + text)
- **None**: 服务器自行读取文件

## pylsp 特定实现

### 启动 pylsp

```bash
pylsp  # 默认 stdio 模式
```

### 诊断通知

诊断通过通知推送，而非响应:

```json
{
  "jsonrpc": "2.0",
  "method": "textDocument/publishDiagnostics",
  "params": {
    "uri": "file:///path/to/file.py",
    "diagnostics": [
      {
        "source": "pyflakes",
        "range": {"start": {"line": 0, "character": 0}, "end": {...}},
        "message": "'os' imported but unused",
        "severity": 2
      }
    ]
  }
}
```

## 补全项类型

| kind | 类型 |
|------|------|
| 1 | 文本 |
| 2 | 方法 |
| 3 | 函数 |
| 4 | 构造器 |
| 5 | 字段 |
| 6 | 变量 |
| 7 | 类 |
| 8 | 接口 |
| 9 | 模块 |
| 10 | 属性 |

## 诊断严重性

| severity | 级别 |
|----------|------|
| 1 | Error |
| 2 | Warning |
| 3 | Information |
| 4 | Hint |

## 实现示例

### Python 客户端

```python
import subprocess
import json

def send_request(pylsp, method, params, req_id=None):
    request = {"jsonrpc": "2.0", "method": method, "params": params}
    if req_id:
        request["id"] = req_id
    
    content = json.dumps(request)
    header = f"Content-Length: {len(content)}\r\n\r\n"
    pylsp.stdin.write(header.encode() + content.encode())
    pylsp.stdin.flush()

def read_response(pylsp):
    content_length = 0
    while True:
        line = pylsp.stdout.readline()
        if line.startswith(b'Content-Length:'):
            content_length = int(line.split(b':')[1])
        if line == b'\r\n':
            break
    
    if content_length > 0:
        return json.loads(pylsp.stdout.read(content_length))
    return None
```

## 参考资料

- [LSP 官方文档](https://microsoft.github.io/language-server-protocol/)
- [JSON-RPC 2.0 规范](https://www.jsonrpc.org/specification)
- [pylsp GitHub](https://github.com/python-lsp/python-lsp-server)


path: references/pep8-guide.md
content: # PEP8 代码风格指南

## 核心规则

### 1. 缩进

- 使用 **4 个空格** 作为缩进
- 不要使用 Tab
- 续行使用悬挂缩进

```python
# 正确
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# 错误
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)
```

### 2. 空行

- 顶层函数/类定义之间：**2 个空行**
- 方法定义之间：**1 个空行**
- 函数内逻辑段落：**1 个空行**

```python
# 正确
class MyClass:
    def method_one(self):
        pass
    
    def method_two(self):
        pass


def function_one():
    pass


def function_two():
    pass

# 错误 (E302)
def function_one():
    pass
def function_two():  # 缺少 2 个空行
    pass
```

### 3. 导入

- 放在文件顶部
- 按顺序分组：标准库 → 第三方 → 本地
- 每组之间 1 个空行

```python
# 正确
import os
import sys

import requests
import numpy as np

from . import local_module
from .utils import helper
```

### 4. 行长度

- 最大 **79 字符** (代码)
- 最大 **99 字符** (文档/注释)

```python
# 正确 (使用括号隐式续行)
result = (
    some_long_variable_name +
    another_long_variable_name
)

# 错误 (E501)
result = some_long_variable_name + another_long_variable_name + yet_another_one
```

### 5. 空白字符

- 行尾不要有空格 (W293)
- 空行不要有空格

```bash
# 清理行尾空格
sed -i 's/[[:space:]]*$//' file.py
```

### 6. 命名约定

| 类型 | 风格 | 示例 |
|------|------|------|
| 函数/变量 | snake_case | `my_function`, `my_var` |
| 类 | PascalCase | `MyClass` |
| 常量 | UPPER_CASE | `MAX_SIZE` |
| 私有 | _prefix | `_internal` |

### 7. 布尔比较

```python
# 正确
if condition:
if not condition:

# 避免 (E712)
if condition is True:
if condition is False:
```

## 常见错误代码

### E 类错误 (Error)

| 代码 | 说明 | 修复 |
|------|------|------|
| E302 | 缺少 2 个空行 | 在函数/类前加空行 |
| E303 | 多余的空行 | 删除多余空行 |
| E305 | 类/函数后缺少 2 个空行 | 在定义后加空行 |
| E402 | 导入不在顶部 | 移动导入到文件开头 |
| E501 | 行太长 | 拆分长行 |
| E711 | 与 None 比较 | 使用 `if x is None` |
| E712 | 与 True/False 比较 | 使用 `if x` 或 `if not x` |
| E722 | 裸 except | 使用 `except Exception` |

### W 类警告 (Warning)

| 代码 | 说明 | 修复 |
|------|------|------|
| W291 | 行尾有空格 | 删除空格 |
| W293 | 空行有空格 | 删除空格 |
| W391 | 文件末尾多余空行 | 删除多余空行 |
| W503 | 运算符在行首 | 移到行尾 (或忽略) |

## 自动修复工具

### black

```bash
pip install black
black file.py
```

### autopep8

```bash
pip install autopep8
autopep8 --in-place --aggressive file.py
```

### autoflake

```bash
pip install autoflake
autoflake --remove-all-unused-imports --in-place file.py
```

### isort

```bash
pip install isort
isort file.py
```

## 完整修复流程

```bash
# 1. 备份
cp file.py file.py.backup

# 2. 清理导入
autoflake --remove-all-unused-imports --in-place file.py

# 3. 排序导入
isort file.py

# 4. 格式化
black file.py

# 5. 验证
pycodestyle file.py
```

## Pre-commit 配置

`.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/psf/black
    rev: 24.1.0
    hooks:
      - id: black
  
  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
  
  - repo: https://github.com/pycqa/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: [--max-line-length=88, --extend-ignore=E203]
```

## 在 CI 中使用

GitHub Actions 示例:

```yaml
name: Code Quality

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      
      - name: Install tools
        run: |
          pip install black flake8 isort
      
      - name: Run black
        run: black --check .
      
      - name: Run isort
        run: isort --check-only .
      
      - name: Run flake8
        run: flake8 .
```

## 参考资料

- [PEP 8 官方文档](https://pep8.org/)
- [pycodestyle GitHub](https://github.com/PyCQA/pycodestyle)
- [black GitHub](https://github.com/psf/black)


path: references/pylsp-config.md
content: # pylsp 配置指南

## 安装

### 基础安装

```bash
pip install python-lsp-server
```

### 完整安装 (所有插件)

```bash
pip install python-lsp-server[all]
```

### 单独安装插件

```bash
# 代码检查
pip install pylsp-mypy        # mypy 类型检查
pip install pylsp-flake8      # flake8 检查
pip install pylsp-pylint      # pylint 检查

# 格式化
pip install pylsp-black       # black 格式化
pip install pylsp-autopep8    # autopep8 格式化

# 重构
pip install pylsp-rope        # rope 重构支持

# 其他
pip install pylsp-isort       # import 排序
```

## 配置文件

### 项目级配置 (. pylsp.ini 或 setup.cfg)

```ini
# .pylsp.ini
[pylsp]
plugins/pycodestyle/enabled = true
plugins/pycodestyle/maxLineLength = 100
plugins/pyflakes/enabled = true
plugins/mypy/enabled = true
plugins/black/enabled = true
```

### 全局配置 (~/.config/pylsp/config.yml)

```yaml
pylsp:
  configurationSources:
    - pycodestyle
    - pyflakes
  plugins:
    pycodestyle:
      enabled: true
      maxLineLength: 100
    pyflakes:
      enabled: true
    mypy:
      enabled: true
      live_mode: true
      strict: false
    black:
      enabled: true
      line_length: 88
    autopep8:
      enabled: true
    isort:
      enabled: true
```

## 检查器配置

### pycodestyle (PEP8)

```ini
[pycodestyle]
max-line-length = 100
ignore = E501,W503
```

常见错误代码:
- **E402**: 导入不在文件顶部
- **E501**: 行太长
- **E302**: 缺少空行
- **W293**: 空行有空白字符
- **W503**: 运算符在行首 (可忽略)

### pyflakes

无需配置，检测:
- 未使用的导入
- 未定义的变量
- 重复的键
- 无效的字符串格式

### mypy (类型检查)

```ini
[mypy]
python_version = 3.12
warn_return_any = True
warn_unused_configs = True
ignore_missing_imports = True
```

## 编辑器集成

### VS Code

安装 Python 扩展后自动使用 pylsp。

配置 (`settings.json`):

```json
{
  "python.languageServer": "Pylance",
  "python.linting.pylspEnabled": true,
  "python.linting.pycodestyleEnabled": true,
  "python.linting.pyflakesEnabled": true,
  "python.formatting.provider": "black"
}
```

### Neovim (nvim-lspconfig)

```lua
require'lspconfig'.pylsp.setup{
  settings = {
    pylsp = {
      plugins = {
        pycodestyle = {
          enabled = true,
          maxLineLength = 100
        },
        pyflakes = {
          enabled = true
        },
        black = {
          enabled = true
        }
      }
    }
  }
}
```

### Emacs (eglot)

```elisp
(add-hook 'python-mode-hook 'eglot-ensure)
(add-to-list 'eglot-server-programs '(python-mode . ("pylsp")))
```

## 性能优化

### 禁用不需要的插件

```ini
[pylsp]
plugins/pylint/enabled = false
plugins/pydocstyle/enabled = false
plugins/yapf/enabled = false
```

### 增量检查

```yaml
pylsp:
  plugins:
    mypy:
      live_mode: false  # 保存时检查而非实时
```

### 缓存

```bash
# mypy 缓存
export MYPY_CACHE_DIR=~/.cache/mypy
```

## 故障排除

### pylsp 无法启动

```bash
# 检查安装
which pylsp
pylsp --version

# 查看日志
pylsp --log-file pylsp.log
```

### 插件冲突

```bash
# 列出已安装插件
pip list | grep pylsp

# 禁用冲突插件
# 在配置文件中设置 enabled = false
```

### 性能问题

```bash
# 使用 --check-parent-process 自动退出
pylsp --check-parent-process

# 限制检查范围
# 在配置中设置 exclude 模式
```

## 最佳实践

1. **项目级配置优先**: 将配置放在项目中而非全局
2. **只启用需要的插件**: 减少内存和 CPU 使用
3. **使用 pre-commit**: 在提交前自动检查
4. **CI 集成**: 在 CI 中运行相同检查

## Pre-commit 集成

`.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/python-lsp/pylsp
    rev: v1.14.0
    hooks:
      - id: pylsp
        args: [--pycodestyle, --pyflakes]
```

## 参考资料

- [pylsp 官方文档](https://github.com/python-lsp/python-lsp-server)
- [pylsp 插件列表](https://github.com/python-lsp/python-lsp-server#plugins)
- [PEP 8 风格指南](https://pep8.org/)


path: scripts/check_python.py
content: #!/usr/bin/env python3
"""
Python 代码质量检查工具 - 批量检查并生成报告

用法:
    python check_python.py [文件或目录...]
    python check_python.py --auto-fix [文件或目录...]
"""

import subprocess
import sys
import os
from pathlib import Path
from datetime import datetime

def find_python_files(paths):
    """查找所有 Python 文件"""
    py_files = []
    for path in paths:
        p = Path(path)
        if p.is_file() and p.suffix == '.py':
            py_files.append(str(p))
        elif p.is_dir():
            py_files.extend([str(f) for f in p.rglob('*.py')])
    return py_files

def check_file(filepath):
    """检查单个文件"""
    script = Path(__file__).parent / 'scripts' / 'lsp-service.py'
    result = subprocess.run(
        [sys.executable, str(script), 'check', filepath],
        capture_output=True, text=True
    )
    return result.stdout.strip()

def fix_file(filepath):
    """自动修复文件"""
    print(f"🔧 修复 {filepath}...")
    
    # 1. 清理导入
    subprocess.run(
        ['autoflake', '--remove-all-unused-imports', '--in-place', filepath],
        capture_output=True
    )
    
    # 2. 格式化
    subprocess.run(['black', '-q', filepath], capture_output=True)
    
    # 3. 重新检查
    return check_file(filepath)

def generate_report(results):
    """生成检查报告"""
    report = []
    report.append("# Python 代码质量检查报告")
    report.append(f"\n检查时间：{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    report.append(f"\n检查文件数：{len(results)}\n")
    
    passed = sum(1 for r in results if '✅' in r['result'] or not r['result'].strip())
    failed = len(results) - passed
    
    report.append("## 统计\n")
    report.append(f"- ✅ 通过：{passed}")
    report.append(f"- ⚠️  有问题：{failed}\n")
    
    if failed > 0:
        report.append("## 问题详情\n")
        for r in results:
            if '✅' not in r['result'] and r['result'].strip():
                report.append(f"### {r['file']}\n")
                report.append(r['result'])
                report.append("")
    
    return '\n'.join(report)

def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    
    auto_fix = '--auto-fix' in sys.argv
    paths = [a for a in sys.argv[1:] if not a.startswith('--')]
    
    if not paths:
        paths = ['.']
    
    py_files = find_python_files(paths)
    
    if not py_files:
        print("❌ 未找到 Python 文件")
        sys.exit(1)
    
    print(f"📋 找到 {len(py_files)} 个 Python 文件\n")
    
    results = []
    for filepath in py_files:
        print(f"检查 {filepath}...")
        
        if auto_fix:
            result = fix_file(filepath)
        else:
            result = check_file(filepath)
        
        results.append({'file': filepath, 'result': result})
        
        # 显示简要结果
        if '✅' in result:
            print(f"  ✅ 通过")
        else:
            issues = len([l for l in result.split('\n') if l.startswith('⚠️') or l.startswith('❌')])
            print(f"  ⚠️  发现 {issues} 个问题")
    
    # 生成报告
    report = generate_report(results)
    
    # 保存报告
    report_file = f"lsp-check-report-{datetime.now().strftime('%Y%m%d-%H%M%S')}.md"
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(report)
    
    print(f"\n📄 报告已保存到：{report_file}")
    
    # 返回状态码
    failed = sum(1 for r in results if '✅' not in r['result'] and r['result'].strip())
    if failed > 0:
        print(f"\n❌ {failed} 个文件有问题需要修复")
        if not auto_fix:
            print("💡 提示：使用 --auto-fix 自动修复")
        sys.exit(1)
    else:
        print("\n✅ 所有文件通过检查!")
        sys.exit(0)

if __name__ == "__main__":
    main()


path: scripts/lsp-python.py
content: #!/usr/bin/env python3
"""
Python LSP Client - Simple wrapper for pylsp interactions

Usage:
    python lsp-python.py <command> [args]
    
Commands:
    init <file_path>     - Initialize LSP session for a file
    completion <file> <line> <char> - Get completions
    diagnostics <file>   - Get diagnostics/errors
    hover <file> <line> <char> - Get hover info
    definition <file> <line> <char> - Go to definition
"""

import json
import subprocess
import sys
import os
from pathlib import Path

LSP_TIMEOUT = 5

def send_request(pylsp, request, timeout=5):
    """Send JSON-RPC request to pylsp"""
    import time
    
    content = json.dumps(request)
    header = f"Content-Length: {len(content)}\r\n\r\n"
    pylsp.stdin.write(header.encode() + content.encode())
    pylsp.stdin.flush()
    
    # Read response headers
    content_length = 0
    start = time.time()
    while time.time() - start < timeout:
        line = pylsp.stdout.readline()
        if not line:
            time.sleep(0.1)
            continue
        if line.startswith(b'Content-Length:'):
            content_length = int(line.split(b':')[1].strip())
        if line == b'\r\n':
            break
    
    if content_length == 0:
        return None
    
    # Read response body
    response = pylsp.stdout.read(content_length).decode()
    return json.loads(response)

def lsp_init(pylsp, root_path):
    """Initialize LSP session"""
    request = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
            "processId": os.getpid(),
            "rootUri": f"file://{root_path}",
            "capabilities": {
                "textDocument": {
                    "completion": {"dynamicRegistration": True},
                    "hover": {"dynamicRegistration": True},
                    "definition": {"dynamicRegistration": True}
                }
            },
            "trace": "off"
        }
    }
    return send_request(pylsp, request)

def lsp_open_file(pylsp, file_path, content):
    """Notify server that file is open"""
    request = {
        "jsonrpc": "2.0",
        "method": "textDocument/didOpen",
        "params": {
            "textDocument": {
                "uri": f"file://{file_path}",
                "languageId": "python",
                "version": 1,
                "text": content
            }
        }
    }
    pylsp.stdin.write((f"Content-Length: {len(json.dumps(request))}\r\n\r\n" + json.dumps(request)).encode())
    pylsp.stdin.flush()

def lsp_completion(pylsp, file_path, line, char):
    """Get code completions"""
    request = {
        "jsonrpc": "2.0",
        "id": 2,
        "method": "textDocument/completion",
        "params": {
            "textDocument": {"uri": f"file://{file_path}"},
            "position": {"line": line, "character": char}
        }
    }
    return send_request(pylsp, request)

def lsp_diagnostics(pylsp, file_path):
    """Get diagnostics (errors/warnings)"""
    # Diagnostics are pushed by server, we need to wait or check publishDiagnostics
    # For now, request it via workspace diagnostic pull if supported
    request = {
        "jsonrpc": "2.0",
        "id": 3,
        "method": "textDocument/diagnostic",
        "params": {
            "textDocument": {"uri": f"file://{file_path}"}
        }
    }
    return send_request(pylsp, request)

def lsp_hover(pylsp, file_path, line, char):
    """Get hover information"""
    request = {
        "jsonrpc": "2.0",
        "id": 4,
        "method": "textDocument/hover",
        "params": {
            "textDocument": {"uri": f"file://{file_path}"},
            "position": {"line": line, "character": char}
        }
    }
    return send_request(pylsp, request)

def lsp_definition(pylsp, file_path, line, char):
    """Go to definition"""
    request = {
        "jsonrpc": "2.0",
        "id": 5,
        "method": "textDocument/definition",
        "params": {
            "textDocument": {"uri": f"file://{file_path}"},
            "position": {"line": line, "character": char}
        }
    }
    return send_request(pylsp, request)

def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    
    command = sys.argv[1]
    
    # Start pylsp process
    pylsp = subprocess.Popen(
        ["pylsp"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        bufsize=0
    )
    
    try:
        if command == "init":
            file_path = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
            result = lsp_init(pylsp, file_path)
            print(json.dumps(result, indent=2))
        
        elif command == "completion":
            if len(sys.argv) < 5:
                print("Usage: lsp-python.py completion <file> <line> <char>")
                sys.exit(1)
            file_path = os.path.abspath(sys.argv[2])
            line = int(sys.argv[3])
            char = int(sys.argv[4])
            content = Path(file_path).read_text()
            lsp_init(pylsp, os.path.dirname(file_path))
            lsp_open_file(pylsp, file_path, content)
            result = lsp_completion(pylsp, file_path, line, char)
            print(json.dumps(result, indent=2, ensure_ascii=False))
        
        elif command == "diagnostics":
            if len(sys.argv) < 3:
                print("Usage: lsp-python.py diagnostics <file>")
                sys.exit(1)
            file_path = os.path.abspath(sys.argv[2])
            content = Path(file_path).read_text()
            lsp_init(pylsp, os.path.dirname(file_path))
            lsp_open_file(pylsp, file_path, content)
            # Wait a bit for diagnostics
            import time
            time.sleep(1)
            result = lsp_diagnostics(pylsp, file_path)
            print(json.dumps(result, indent=2, ensure_ascii=False))
        
        elif command == "hover":
            if len(sys.argv) < 5:
                print("Usage: lsp-python.py hover <file> <line> <char>")
                sys.exit(1)
            file_path = os.path.abspath(sys.argv[2])
            line = int(sys.argv[3])
            char = int(sys.argv[4])
            content = Path(file_path).read_text()
            lsp_init(pylsp, os.path.dirname(file_path))
            lsp_open_file(pylsp, file_path, content)
            result = lsp_hover(pylsp, file_path, line, char)
            print(json.dumps(result, indent=2, ensure_ascii=False))
        
        elif command == "definition":
            if len(sys.argv) < 5:
                print("Usage: lsp-python.py definition <file> <line> <char>")
                sys.exit(1)
            file_path = os.path.abspath(sys.argv[2])
            line = int(sys.argv[3])
            char = int(sys.argv[4])
            content = Path(file_path).read_text()
            lsp_init(pylsp, os.path.dirname(file_path))
            lsp_open_file(pylsp, file_path, content)
            result = lsp_definition(pylsp, file_path, line, char)
            print(json.dumps(result, indent=2, ensure_ascii=False))
        
        else:
            print(f"Unknown command: {command}")
            print(__doc__)
            sys.exit(1)
    
    finally:
        # Shutdown gracefully
        shutdown_req = {"jsonrpc": "2.0", "id": 99, "method": "shutdown", "params": {}}
        pylsp.stdin.write((f"Content-Length: {len(json.dumps(shutdown_req))}\r\n\r\n" + json.dumps(shutdown_req)).encode())
        pylsp.stdin.flush()
        pylsp.terminate()
        pylsp.wait(timeout=2)

if __name__ == "__main__":
    main()


path: scripts/lsp-service.py
content: #!/usr/bin/env python3
"""
LSP Python 服务封装 - 提供简洁的 LSP 调用接口

用法:
    python lsp-service.py <command> <args...>

Commands:
    check <file>              - 检查文件错误/警告
    complete <file> <line> <char> - 代码补全
    info <file> <line> <char> - 获取符号信息 (hover)
    goto <file> <line> <char> - 跳转到定义
"""

import subprocess
import json
import sys
import os
from pathlib import Path

def run_lsp(command, args):
    """调用 lsp-python.py 并解析结果"""
    script_path = Path(__file__).parent / "lsp-python.py"
    cmd = [sys.executable, str(script_path), command] + args
    
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=10
    )
    
    if result.returncode != 0:
        return {"error": result.stderr}
    
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return {"raw": result.stdout}

def format_diagnostics(result):
    """格式化诊断结果"""
    if "error" in result:
        return f"❌ 错误：{result['error']}"
    
    params = result.get("params", {})
    diagnostics = params.get("diagnostics", [])
    
    if not diagnostics:
        return "✅ 没有发现问题"
    
    lines = []
    for diag in diagnostics:
        source = diag.get("source", "unknown")
        message = diag.get("message", "")
        severity = diag.get("severity", 2)
        line = diag.get("range", {}).get("start", {}).get("line", 0) + 1
        
        icon = {1: "❌", 2: "⚠️", 3: "ℹ️", 4: "💡"}.get(severity, "•")
        lines.append(f"{icon} 第{line}行 [{source}]: {message}")
    
    return "\n".join(lines)

def format_completions(result):
    """格式化补全结果"""
    if "error" in result:
        return f"❌ 错误：{result['error']}"
    
    items = result.get("result", {}).get("items", [])
    
    if not items:
        return "没有补全建议"
    
    lines = ["补全建议:"]
    for item in items[:10]:  # 最多显示 10 个
        label = item.get("label", "")
        kind = item.get("kind", 0)
        kind_names = {
            1: "文本", 2: "方法", 3: "函数", 4: "构造器", 5: "字段",
            6: "变量", 7: "类", 8: "接口", 9: "模块", 10: "属性"
        }
        kind_name = kind_names.get(kind, "其他")
        lines.append(f"  • {label} ({kind_name})")
    
    if len(items) > 10:
        lines.append(f"  ... 还有 {len(items) - 10} 个建议")
    
    return "\n".join(lines)

def format_hover(result):
    """格式化悬停信息"""
    if "error" in result:
        return f"❌ 错误：{result['error']}"
    
    contents = result.get("result", {})
    if isinstance(contents, dict):
        value = contents.get("value", "")
        # 清理 markdown 格式
        value = value.replace("```python", "").replace("```", "").strip()
        return value if value else "无信息"
    return str(contents)

def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    
    command = sys.argv[1]
    args = sys.argv[2:]
    
    if command == "check":
        if not args:
            print("用法：lsp-service.py check <file>")
            sys.exit(1)
        result = run_lsp("diagnostics", [os.path.abspath(args[0])])
        print(format_diagnostics(result))
    
    elif command == "complete":
        if len(args) < 3:
            print("用法：lsp-service.py complete <file> <line> <char>")
            sys.exit(1)
        result = run_lsp("completion", [os.path.abspath(args[0]), args[1], args[2]])
        print(format_completions(result))
    
    elif command == "info":
        if len(args) < 3:
            print("用法：lsp-service.py info <file> <line> <char>")
            sys.exit(1)
        result = run_lsp("hover", [os.path.abspath(args[0]), args[1], args[2]])
        print(format_hover(result))
    
    elif command == "goto":
        if len(args) < 3:
            print("用法：lsp-service.py goto <file> <line> <char>")
            sys.exit(1)
        result = run_lsp("definition", [os.path.abspath(args[0]), args[1], args[2]])
        print(json.dumps(result, indent=2, ensure_ascii=False))
    
    else:
        print(f"未知命令：{command}")
        print(__doc__)
        sys.exit(1)

if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)