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:
2026-05-31 00:00:58 +08:00
parent 3cc9ee95e4
commit f2da0f2ed4
43 changed files with 2467 additions and 800 deletions

View File

@@ -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;
}