✨ feat: 灵台引擎 Python 绑定与消息路径接入(WP-15/WP-03 交界)

- engine/bindings:nanobind 官方薄绑定包 hunjing-lingtai(LingtaiEngine RAII、HjStatus→LingtaiError 映射、HJ_ABI_VERSION 导出);scikit-build-core 构建,直接编译引擎源码自包含
- backend 以 editable 路径依赖接入;typings/lingtai.pyi 类型存根(mypy strict 全绿)
- app/core/lingtai.py:懒初始化单例 + LingtaiUnavailable fail-closed(禁止宿主第二实现降级)
- 迁移 3:message.parser_version;post_message 的 markdown 输入经 hj_transform 规范化为 canonical blocks(展开 code/mermaid/math 并标注 parser_version,契约第 4 节)
- 测试 29 项全绿:消息展开、Python/C++ golden 字节一致、引擎注入失败时消息路径显式 500(fail-closed 验证)
This commit is contained in:
chenxin committed 2026-09-10 11:26:40 +08:00
1 parent 127191c889
commit 491136c770
15 files changed
+314 -4

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.18...3.30)
project(lingtai_bindings LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(Python COMPONENTS Interpreter Development REQUIRED)
find_package(nanobind CONFIG REQUIRED)
# 直接编译引擎源码(自包含,不依赖 dylib 安装路径)
nanobind_add_module(lingtai
src/lingtai.cpp
../src/lingtai.cpp
../src/transform.cpp)
target_include_directories(lingtai PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../include
${CMAKE_CURRENT_SOURCE_DIR}/../src)
install(TARGETS lingtai DESTINATION .)
+12
View File
@@ -0,0 +1,12 @@
[build-system]
requires = ["scikit-build-core>=0.10", "nanobind>=2.4"]
build-backend = "scikit_build_core.build"
[project]
name = "hunjing-lingtai"
version = "0.1.0"
description = "灵台引擎(Lingtai Engine)官方 Python 薄绑定(ADR-016)"
requires-python = ">=3.12"
[tool.scikit-build]
build-dir = "build/{wheel_tag}"
+88
View File
@@ -0,0 +1,88 @@
// 灵台引擎官方 Python 绑定(nanobind,ADR-016)。
// 薄封装:类型转换、句柄 RAII 与错误映射;不复制业务语义(契约 lingtai-ffi 第 8 节)。
#include <stdexcept>
#include <string>
#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h>
#include "hunjing/lingtai.h"
namespace nb = nanobind;
namespace {
struct LingtaiError : std::runtime_error {
using std::runtime_error::runtime_error;
};
struct PyEngine {
HjEngine *handle = nullptr;
PyEngine() {
const HjStatus st = hj_engine_new(HJ_ABI_VERSION, &handle);
if (st != HJ_OK) {
throw LingtaiError(std::string("灵台引擎初始化失败:") + hj_last_error());
}
}
PyEngine(const PyEngine &) = delete;
PyEngine &operator=(const PyEngine &) = delete;
~PyEngine() {
if (handle != nullptr) {
hj_engine_free(handle);
handle = nullptr;
}
}
};
const char *status_name(HjStatus st) {
switch (st) {
case HJ_OK: return "HJ_OK";
case HJ_ERR_INVALID_ARGUMENT: return "HJ_ERR_INVALID_ARGUMENT";
case HJ_ERR_OUT_OF_MEMORY: return "HJ_ERR_OUT_OF_MEMORY";
case HJ_ERR_UNSUPPORTED_ABI: return "HJ_ERR_UNSUPPORTED_ABI";
case HJ_ERR_HANDLE_INVALID: return "HJ_ERR_HANDLE_INVALID";
case HJ_ERR_REENTRANCY: return "HJ_ERR_REENTRANCY";
case HJ_ERR_PARSE_FAILED: return "HJ_ERR_PARSE_FAILED";
case HJ_ERR_LIMIT_EXCEEDED: return "HJ_ERR_LIMIT_EXCEEDED";
case HJ_ERR_POLICY_DENIED: return "HJ_ERR_POLICY_DENIED";
case HJ_ERR_SIGNATURE_INVALID: return "HJ_ERR_SIGNATURE_INVALID";
case HJ_ERR_INDEX_CORRUPT: return "HJ_ERR_INDEX_CORRUPT";
case HJ_ERR_BUFFER_TOO_SMALL: return "HJ_ERR_BUFFER_TOO_SMALL";
case HJ_ERR_INTERNAL: return "HJ_ERR_INTERNAL";
}
return "HJ_ERR_UNKNOWN";
}
} // namespace
NB_MODULE(lingtai, m) {
m.doc() = "灵台引擎(Lingtai Engine)官方 Python 绑定";
m.attr("HJ_ABI_VERSION") = HJ_ABI_VERSION;
m.def("abi_version", [] { return hj_abi_version(); }, "引擎构建时的 ABI major");
m.def("transform_version", [] { return std::string(hj_transform_version()); },
"Markdown 解析器版本");
nb::exception<LingtaiError>(m, "LingtaiError");
nb::class_<PyEngine>(m, "LingtaiEngine")
.def(nb::init<>())
.def(
"transform_markdown",
[](PyEngine &self, const std::string &input) -> std::string {
HjBuffer *buf = nullptr;
const HjStatus st =
hj_transform_markdown(self.handle, input.data(), input.size(), &buf);
if (st != HJ_OK) {
throw LingtaiError(std::string(status_name(st)) + ": " + hj_last_error());
}
std::string out(static_cast<const char *>(hj_buffer_data(buf)), hj_buffer_len(buf));
hj_buffer_free(buf);
return out;
},
nb::arg("input"),
"Markdown → canonical ContentBlock JSON(确定性、键序固定)");
}
+8
View File
@@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "hunjing-lingtai"
version = "0.1.0"
source = { editable = "." }