✨ 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:
1 parent
127191c889
commit
491136c770
15 files changed
+314
-4
No files matched your search
@@ -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(确定性、键序固定)");
|
||||
}
|
||||
Reference in new issue
Block a user