Add metadata validation script and module documentation
- Introduced a new Python script `check_agents_metadata.py` for validating agent metadata, including YAML parsing, rating ranges, and cross-references. - Added usage instructions and exit codes for the script. - Created a new markdown file `模块目录和功能说明.md` to outline the directory structure and functionality of the modules. - Added a text file `说明此文件不可AI修改.txt` to specify that certain files should not be modified by AI, including important information about the `dstalk` framework and its modules.
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
/*
|
||||
* plugin-lsp — LSP (Language Server Protocol) 服务
|
||||
*
|
||||
* 自行管理语言服务器子进程,使用 JSON-RPC 2.0 over stdio 通信。
|
||||
* 无外部服务依赖(不依赖 http/config 等其他插件)。
|
||||
* @file lsp_plugin.cpp
|
||||
* @brief LSP plugin: Language Server Protocol JSON-RPC client for diagnostics, hover, completion.
|
||||
* LSP 插件:Language Server Protocol JSON-RPC 客户端,用于诊断、悬停、补全。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
// plugin-lsp — LSP (Language Server Protocol) 服务 / LSP (Language Server Protocol) service
|
||||
//
|
||||
// 自行管理语言服务器子进程,使用 JSON-RPC 2.0 over stdio 通信 / Self-manages language server subprocess, communicates via JSON-RPC 2.0 over stdio.
|
||||
// 无外部服务依赖(不依赖 http/config 等其他插件) / No external service dependencies (does not depend on http/config or other plugins).
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
@@ -22,7 +27,7 @@
|
||||
#include <unordered_map>
|
||||
|
||||
// ============================================================================
|
||||
// 平台相关 — 子进程管理 (内嵌 subprocess::Process)
|
||||
// 平台相关 — 子进程管理 (内嵌 subprocess::Process) / Platform specific — subprocess management (embedded subprocess::Process)
|
||||
// ============================================================================
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -45,12 +50,12 @@
|
||||
namespace json = boost::json;
|
||||
|
||||
// ============================================================================
|
||||
// 全局指针
|
||||
// 全局指针 / Global pointers
|
||||
// ============================================================================
|
||||
static const dstalk_host_api_t* g_host = nullptr;
|
||||
|
||||
// ============================================================================
|
||||
// 子进程封装 (内嵌 subprocess.hpp)
|
||||
// 子进程封装 (内嵌 subprocess.hpp) / Subprocess wrapper (embedded subprocess.hpp)
|
||||
// ============================================================================
|
||||
struct Process {
|
||||
#ifdef _WIN32
|
||||
@@ -64,6 +69,7 @@ struct Process {
|
||||
int stdout_fd = -1;
|
||||
#endif
|
||||
|
||||
// 从给定命令行启动子进程。为 stdin/stdout 设置管道 / Start a child process from the given command line. Sets up pipes for stdin/stdout.
|
||||
bool start(const char* cmd) {
|
||||
if (!cmd || !cmd[0]) return false;
|
||||
stop();
|
||||
@@ -169,6 +175,7 @@ struct Process {
|
||||
#endif
|
||||
}
|
||||
|
||||
// 优雅终止子进程,回退到 SIGKILL/TerminateProcess / Gracefully terminate the child process, with fallback to SIGKILL/TerminateProcess.
|
||||
void stop() {
|
||||
#ifdef _WIN32
|
||||
if (hProcess != INVALID_HANDLE_VALUE) {
|
||||
@@ -198,6 +205,7 @@ struct Process {
|
||||
#endif
|
||||
}
|
||||
|
||||
// 将数据字符串写入子进程 stdin 管道 / Write a data string to the child's stdin pipe.
|
||||
bool write(const std::string& data) {
|
||||
if (data.empty()) return true;
|
||||
#ifdef _WIN32
|
||||
@@ -219,6 +227,7 @@ struct Process {
|
||||
#endif
|
||||
}
|
||||
|
||||
// 从子进程 stdout 管道读取一行(到并包括 '\n') / Read one line (up to and including '\n') from the child's stdout pipe.
|
||||
bool read_line(std::string& line) {
|
||||
line.clear();
|
||||
#ifdef _WIN32
|
||||
@@ -242,6 +251,7 @@ struct Process {
|
||||
#endif
|
||||
}
|
||||
|
||||
// 从子进程 stdout 管道读取恰好 count 字节到 buf / Read exactly `count` bytes from the child's stdout pipe into `buf`.
|
||||
bool read_bytes(std::string& buf, int count) {
|
||||
if (count <= 0) { buf.clear(); return true; }
|
||||
#ifdef _WIN32
|
||||
@@ -274,7 +284,7 @@ struct Process {
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// LSP 状态(静态单例)
|
||||
// LSP 状态(静态单例) / LSP state (static singleton)
|
||||
// ============================================================================
|
||||
struct LspState {
|
||||
Process proc;
|
||||
@@ -283,23 +293,24 @@ struct LspState {
|
||||
|
||||
std::atomic<int> next_id{1};
|
||||
|
||||
// 响应用于同步等待
|
||||
// 响应用于同步等待 / Responses for synchronous waiting
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::unordered_map<int, std::string> pending_responses;
|
||||
|
||||
// 诊断缓存: URI -> JSON 字符串
|
||||
// 诊断缓存: URI -> JSON 字符串 / Diagnostics cache: URI -> JSON string
|
||||
std::unordered_map<std::string, std::string> diagnostics;
|
||||
|
||||
// 读取线程
|
||||
// 读取线程 / Reader thread
|
||||
std::thread reader_thread;
|
||||
};
|
||||
static LspState g_lsp;
|
||||
|
||||
// ============================================================================
|
||||
// 辅助函数
|
||||
// 辅助函数 / Helper functions
|
||||
// ============================================================================
|
||||
|
||||
// 去除 string_view 首尾空白 / Trim leading and trailing whitespace from a string_view.
|
||||
static std::string_view trim(std::string_view sv) {
|
||||
while (!sv.empty() && (sv.front() == ' ' || sv.front() == '\t' ||
|
||||
sv.front() == '\r' || sv.front() == '\n'))
|
||||
@@ -310,6 +321,7 @@ static std::string_view trim(std::string_view sv) {
|
||||
return sv;
|
||||
}
|
||||
|
||||
// 将 JSON-RPC 消息体包装在 LSP 头中 (Content-Length: ...\r\n\r\n) / Wrap a JSON-RPC message body in an LSP header (Content-Length: ...\r\n\r\n).
|
||||
static std::string frame_message(const std::string& body) {
|
||||
std::string frame;
|
||||
frame.reserve(64 + body.size());
|
||||
@@ -320,6 +332,7 @@ static std::string frame_message(const std::string& body) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
// 从 LSP 头行中解析 Content-Length 值。解析失败返回 -1 / Parse the Content-Length value from an LSP header line. Returns -1 on parse failure.
|
||||
static int parse_content_length(const std::string& line) {
|
||||
auto sv = trim(std::string_view(line));
|
||||
const char prefix[] = "Content-Length:";
|
||||
@@ -341,9 +354,10 @@ static int parse_content_length(const std::string& line) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON-RPC 消息发送
|
||||
// JSON-RPC 消息发送 / JSON-RPC message sending
|
||||
// ============================================================================
|
||||
|
||||
// 向 LSP 服务器发送 JSON-RPC 请求并返回分配的请求 id / Send a JSON-RPC request to the LSP server and return the assigned request id.
|
||||
static int send_request(const std::string& method, const json::object& params) {
|
||||
int id = g_lsp.next_id.fetch_add(1);
|
||||
|
||||
@@ -358,6 +372,7 @@ static int send_request(const std::string& method, const json::object& params) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// 向 LSP 服务器发送 JSON-RPC 通知(无 id 字段,不期待响应) / Send a JSON-RPC notification to the LSP server (no id field, no response expected).
|
||||
static void send_notification(const std::string& method, const json::object& params) {
|
||||
json::object msg;
|
||||
msg["jsonrpc"] = "2.0";
|
||||
@@ -369,9 +384,12 @@ static void send_notification(const std::string& method, const json::object& par
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 消息处理
|
||||
// 消息处理 / Message handling
|
||||
// ============================================================================
|
||||
|
||||
// 分发接收到的 JSON-RPC 消息:将响应路由到待处理队列,
|
||||
// 处理 textDocument/publishDiagnostics 通知并存入诊断缓存 / Dispatch a received JSON-RPC message: route responses to pending queue,
|
||||
// handle textDocument/publishDiagnostics notifications into diagnostics cache.
|
||||
static void handle_message(const std::string& body) {
|
||||
try {
|
||||
json::value val;
|
||||
@@ -383,14 +401,14 @@ static void handle_message(const std::string& body) {
|
||||
catch (...) { return; }
|
||||
|
||||
if (msg.contains("id") && !msg.contains("method")) {
|
||||
// 响应 (有 id, 无 method)
|
||||
// 响应 (有 id, 无 method) / Response (has id, no method)
|
||||
int id = static_cast<int>(msg["id"].as_int64());
|
||||
std::lock_guard<std::mutex> lock(g_lsp.mutex);
|
||||
g_lsp.pending_responses[id] = body;
|
||||
g_lsp.cv.notify_all();
|
||||
|
||||
} else if (msg.contains("method") && !msg.contains("id")) {
|
||||
// 通知 (有 method, 无 id)
|
||||
// 通知 (有 method, 无 id) / Notification (has method, no id)
|
||||
std::string method;
|
||||
try { method = json::value_to<std::string>(msg["method"]); }
|
||||
catch (...) { return; }
|
||||
@@ -419,17 +437,18 @@ static void handle_message(const std::string& body) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 读取线程主循环
|
||||
// 读取线程主循环 / Reader thread main loop
|
||||
// ============================================================================
|
||||
|
||||
// 读取线程主循环:解析 LSP header+body 帧并分发消息 / Main loop for the reader thread: parse LSP header+body frames and dispatch messages.
|
||||
static void reader_loop() {
|
||||
try {
|
||||
while (g_lsp.running) {
|
||||
int content_length = -1;
|
||||
bool pipe_ok = true;
|
||||
|
||||
// 状态机式读取 header 块:循环 read_line 直到读到空行
|
||||
// LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header
|
||||
// 状态机式读取 header 块:循环 read_line 直到读到空行 / State-machine header block read: loop read_line until empty line
|
||||
// LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header / LSP 3.17: header block ends with empty line (\r\n), allows other headers like Content-Type
|
||||
while (pipe_ok) {
|
||||
std::string line;
|
||||
if (!g_lsp.proc.read_line(line)) {
|
||||
@@ -437,18 +456,18 @@ static void reader_loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
// header 块以空行结束
|
||||
// header 块以空行结束 / header block ends with empty line
|
||||
auto sv = trim(std::string_view(line));
|
||||
if (sv.empty()) break;
|
||||
|
||||
// 累积 Content-Length;遇到其他 header 不丢弃,继续读取下一行
|
||||
// 累积 Content-Length;遇到其他 header 不丢弃,继续读取下一行 / Accumulate Content-Length; don't discard other headers, continue reading next line
|
||||
int len = parse_content_length(line);
|
||||
if (len >= 0) content_length = len;
|
||||
}
|
||||
|
||||
if (!pipe_ok) break;
|
||||
|
||||
// 空行前都没读到 Content-Length,协议错误——记日志并跳过这一帧
|
||||
// 空行前都没读到 Content-Length,协议错误——记日志并跳过这一帧 / Content-Length not read before empty line, protocol error — log and skip this frame
|
||||
if (content_length < 0) {
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header");
|
||||
continue;
|
||||
@@ -471,38 +490,39 @@ static void reader_loop() {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LSP 服务 vtable 实现 (定义在 vtable 变量之前)
|
||||
// LSP 服务 vtable 实现 (定义在 vtable 变量之前) / LSP service vtable implementation (defined before vtable variable)
|
||||
// ============================================================================
|
||||
|
||||
static void g_lsp_impl_stop();
|
||||
static void g_lsp_impl_stop_nolock();
|
||||
static void g_lsp_impl_stop_locked(std::unique_lock<std::mutex>& lock);
|
||||
|
||||
// 启动 LSP 服务器进程,发送 initialize/initialized 握手,启动读取线程 / Start the LSP server process, send initialize/initialized handshake, start reader thread.
|
||||
static int g_lsp_impl_start(const char* server_cmd, const char* language) {
|
||||
if (!server_cmd || !server_cmd[0]) return -1;
|
||||
|
||||
try {
|
||||
// 如果已在运行, 先停止
|
||||
// 如果已在运行, 先停止 / If already running, stop first
|
||||
if (g_lsp.running) {
|
||||
g_lsp_impl_stop();
|
||||
}
|
||||
|
||||
g_lsp.language = language ? language : "";
|
||||
|
||||
// 启动进程
|
||||
// 启动进程 / Start process
|
||||
if (!g_lsp.proc.start(server_cmd)) {
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] failed to start: %s", server_cmd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 重置 ID 计数器
|
||||
// 重置 ID 计数器 / Reset ID counter
|
||||
g_lsp.next_id = 1;
|
||||
|
||||
// 启动读取线程
|
||||
// 启动读取线程 / Start reader thread
|
||||
g_lsp.running = true;
|
||||
g_lsp.reader_thread = std::thread(reader_loop);
|
||||
|
||||
// 构建 initialize 参数
|
||||
// 构建 initialize 参数 / Build initialize params
|
||||
json::object text_doc_caps;
|
||||
{
|
||||
json::object hover;
|
||||
@@ -526,10 +546,10 @@ static int g_lsp_impl_start(const char* server_cmd, const char* language) {
|
||||
init_params["rootUri"] = nullptr;
|
||||
init_params["capabilities"] = capabilities;
|
||||
|
||||
// 发送 initialize 请求
|
||||
// 发送 initialize 请求 / Send initialize request
|
||||
int init_id = send_request("initialize", init_params);
|
||||
|
||||
// 等待 initialize 响应 (最多 10 秒)
|
||||
// 等待 initialize 响应 (最多 10 秒) / Wait for initialize response (max 10 seconds)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_lsp.mutex);
|
||||
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [init_id]() {
|
||||
@@ -544,7 +564,7 @@ static int g_lsp_impl_start(const char* server_cmd, const char* language) {
|
||||
g_lsp.pending_responses.erase(init_id);
|
||||
}
|
||||
|
||||
// 发送 initialized 通知
|
||||
// 发送 initialized 通知 / Send initialized notification
|
||||
send_notification("initialized", json::object{});
|
||||
|
||||
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server started: %s", server_cmd);
|
||||
@@ -558,14 +578,15 @@ static int g_lsp_impl_start(const char* server_cmd, const char* language) {
|
||||
}
|
||||
}
|
||||
|
||||
// 停止 LSP 服务器:发送 shutdown 请求,发送 exit 通知,停止进程和线程 / Stop the LSP server: send shutdown request, send exit notification, stop process & thread.
|
||||
static void g_lsp_impl_stop_nolock() {
|
||||
try {
|
||||
if (!g_lsp.running) return;
|
||||
|
||||
// 发送 shutdown 请求
|
||||
// 发送 shutdown 请求 / Send shutdown request
|
||||
int shutdown_id = send_request("shutdown", json::object{});
|
||||
|
||||
// 等待 shutdown 响应 (最多 2 秒)
|
||||
// 等待 shutdown 响应 (最多 2 秒) / Wait for shutdown response (max 2 seconds)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_lsp.mutex);
|
||||
g_lsp.cv.wait_for(lock, std::chrono::seconds(2), [shutdown_id]() {
|
||||
@@ -574,10 +595,10 @@ static void g_lsp_impl_stop_nolock() {
|
||||
g_lsp.pending_responses.clear();
|
||||
}
|
||||
|
||||
// 发送 exit 通知
|
||||
// 发送 exit 通知 / Send exit notification
|
||||
send_notification("exit", json::object{});
|
||||
|
||||
// 停止读取线程
|
||||
// 停止读取线程 / Stop reader thread
|
||||
g_lsp.running = false;
|
||||
g_lsp.proc.stop();
|
||||
|
||||
@@ -593,15 +614,18 @@ static void g_lsp_impl_stop_nolock() {
|
||||
}
|
||||
}
|
||||
|
||||
// 公开 stop:无锁获取(委托给 g_lsp_impl_stop_nolock) / Public stop: acquires no lock (delegates to g_lsp_impl_stop_nolock).
|
||||
static void g_lsp_impl_stop() {
|
||||
g_lsp_impl_stop_nolock();
|
||||
}
|
||||
|
||||
// Stop 辅助函数:在调用 g_lsp_impl_stop_nolock 前解锁给定的 unique_lock / Stop helper: unlocks the given unique_lock before calling g_lsp_impl_stop_nolock.
|
||||
static void g_lsp_impl_stop_locked(std::unique_lock<std::mutex>& lock) {
|
||||
lock.unlock();
|
||||
g_lsp_impl_stop_nolock();
|
||||
}
|
||||
|
||||
// 向 LSP 服务器发送 textDocument/didOpen 通知 / Send a textDocument/didOpen notification to the LSP server.
|
||||
static int g_lsp_impl_open_document(const char* uri, const char* content,
|
||||
const char* lang_id) {
|
||||
if (!g_lsp.running) return -1;
|
||||
@@ -628,6 +652,7 @@ static int g_lsp_impl_open_document(const char* uri, const char* content,
|
||||
}
|
||||
}
|
||||
|
||||
// 向 LSP 服务器发送 textDocument/didClose 通知 / Send a textDocument/didClose notification to the LSP server.
|
||||
static int g_lsp_impl_close_document(const char* uri) {
|
||||
if (!g_lsp.running) return -1;
|
||||
if (!uri) return -1;
|
||||
@@ -650,6 +675,7 @@ static int g_lsp_impl_close_document(const char* uri) {
|
||||
}
|
||||
}
|
||||
|
||||
// 返回给定文档 URI 的缓存诊断 JSON / Return the cached diagnostics JSON for the given document URI.
|
||||
static int g_lsp_impl_get_diagnostics(const char* uri, char** json_out) {
|
||||
if (!g_lsp.running) return -1;
|
||||
if (!uri || !json_out) return -1;
|
||||
@@ -674,6 +700,7 @@ static int g_lsp_impl_get_diagnostics(const char* uri, char** json_out) {
|
||||
}
|
||||
}
|
||||
|
||||
// 发送 textDocument/hover 请求并以 JSON 返回悬停结果 / Send a textDocument/hover request and return the hover result as JSON.
|
||||
static int g_lsp_impl_get_hover(const char* uri, int line, int col, char** json_out) {
|
||||
if (!g_lsp.running) return -1;
|
||||
if (!uri || !json_out) return -1;
|
||||
@@ -727,6 +754,7 @@ static int g_lsp_impl_get_hover(const char* uri, int line, int col, char** json_
|
||||
}
|
||||
}
|
||||
|
||||
// 发送 textDocument/completion 请求并以 JSON 返回补全列表 / Send a textDocument/completion request and return the completion list as JSON.
|
||||
static int g_lsp_impl_get_completion(const char* uri, int line, int col, char** json_out) {
|
||||
if (!g_lsp.running) return -1;
|
||||
if (!uri || !json_out) return -1;
|
||||
@@ -781,7 +809,7 @@ static int g_lsp_impl_get_completion(const char* uri, int line, int col, char**
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 服务 vtable
|
||||
// 服务 vtable / Service vtable
|
||||
// ============================================================================
|
||||
|
||||
static dstalk_lsp_service_t g_service_vtable = {
|
||||
@@ -795,15 +823,17 @@ static dstalk_lsp_service_t g_service_vtable = {
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期回调
|
||||
// 生命周期回调 / Lifecycle callbacks
|
||||
// ============================================================================
|
||||
|
||||
// 插件初始化:保存主机指针并注册 lsp 服务 / Plugin init: store host pointer and register the lsp service.
|
||||
static int on_init(const dstalk_host_api_t* host) {
|
||||
g_host = host;
|
||||
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] initializing LSP service plugin");
|
||||
return host->register_service("lsp", 1, &g_service_vtable);
|
||||
}
|
||||
|
||||
// 插件关闭:如果运行中则停止 LSP 服务器,清空主机指针 / Plugin shutdown: stop LSP server if running, null out host pointer.
|
||||
static void on_shutdown() {
|
||||
try {
|
||||
if (g_lsp.running) {
|
||||
@@ -821,20 +851,21 @@ static void on_shutdown() {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 插件描述符
|
||||
// 插件描述符 / Plugin descriptor
|
||||
// ============================================================================
|
||||
|
||||
static dstalk_plugin_info_t g_info = {
|
||||
/* .name = */ "lsp",
|
||||
/* .version = */ "1.0.0",
|
||||
/* .description = */ "Language Server Protocol client (subprocess manager)",
|
||||
/* .description = */ "Language Server Protocol client (subprocess manager) / Language Server Protocol 客户端(子进程管理器)",
|
||||
/* .api_version = */ DSTALK_API_VERSION,
|
||||
/* .dependencies = */ { NULL }, // 无依赖,自行管理子进程
|
||||
/* .dependencies = */ { NULL }, // 无依赖,自行管理子进程 / No dependencies, self-manages subprocess
|
||||
/* .on_init = */ on_init,
|
||||
/* .on_shutdown = */ on_shutdown,
|
||||
/* .on_event = */ nullptr,
|
||||
};
|
||||
|
||||
// 必须入口点:返回插件描述符给主机 / Mandatory entry point: returns the plugin descriptor to the host.
|
||||
extern "C" DSTALK_PLUGIN_EXPORT dstalk_plugin_info_t* dstalk_plugin_init(void) {
|
||||
return &g_info;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user