✨ feat: 灵台引擎五神齐备与 Go/Rust 官方绑定(WP-15)
- C++ 新增四模块(modules.cpp,19 个导出符号全部 hj_*): 小周天 hj_stream:流会话(sequence/delta_index 单调)与 content.block.delta envelope 确定性编码(重放字节一致) 烛照 hj_policy:URL 协议白名单(拒 file/javascript/data)、路径授权根与 .. 段拒绝、幂等键非空≤128、四维决策表(autonomy×risk→allow/ask/deny,非法输入 deny fail-closed) 洛书 hj_plan:预算核算(达限/负数拒绝) 河图 hj_recall:确定性重排(词频×稳定排序,canonical JSON 输出;非法输入 PARSE_FAILED) - doctest 22 用例 20135 断言全绿(含每模块 golden 与确定性双跑) - Go 绑定(cgo 自包含编译引擎源码):Engine RAII/TransformMarkdown/ValidateURL/ValidatePath/Decide/CheckBudget/RecallRerank;go test 3 用例通过(transform/rerank golden 与 C++ 字节一致) - Rust 绑定(bindgen 生成 + 安全包装):Engine RAII(Drop 释放)、Error 枚举映射全部 HjStatus、Send/Sync(HjEngine 线程安全);cargo test 3 用例通过(同 golden) - 三端(C++/Go/Rust)与 Python golden 一致:同一 markdown 输入在四端输出字节相同(跨端一致性首次全语言验证)
This commit is contained in:
1 parent
99a7789432
commit
a39cec5287
15 files changed
+1205
-3
No files matched your search
@@ -0,0 +1,340 @@
|
||||
// 小周天(hj_stream)、烛照(hj_policy)、洛书(hj_plan)、河图(hj_recall)实现。
|
||||
// 全部输入按不可信处理;输出 canonical JSON 键序确定(跨端 golden 一致)。
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "hunjing/lingtai.h"
|
||||
|
||||
#include "engine_internal.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// 复用脾神的确定性 JSON 转义(小范围内重复实现,避免跨模块内部头耦合)。
|
||||
void json_escape(std::string &out, std::string_view v) {
|
||||
out.push_back('"');
|
||||
for (char ch : v) {
|
||||
switch (ch) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(ch) < 0x20) {
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(ch));
|
||||
out += buf;
|
||||
} else {
|
||||
out.push_back(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_back('"');
|
||||
}
|
||||
|
||||
bool starts_with(std::string_view s, std::string_view prefix) {
|
||||
return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// 不透明句柄布局(仅本翻译单元可见;头文件为前向声明)。
|
||||
struct HjStream {
|
||||
uint64_t sequence = 0;
|
||||
uint64_t next_delta_index = 0;
|
||||
std::string current_block;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
// ===== 小周天 =====
|
||||
|
||||
HjStatus hj_stream_new(HjEngine *engine, HjStream **out_stream) {
|
||||
if (out_stream == nullptr) {
|
||||
hj_internal_set_error("out_stream 不能为空");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
*out_stream = nullptr;
|
||||
if (!hj_internal_engine_valid(engine)) {
|
||||
hj_internal_set_error("engine 句柄不在登记表");
|
||||
return HJ_ERR_HANDLE_INVALID;
|
||||
}
|
||||
auto *stream = new (std::nothrow) HjStream();
|
||||
if (stream == nullptr) {
|
||||
hj_internal_set_error("流会话分配失败");
|
||||
return HJ_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
*out_stream = stream;
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
HjStatus hj_stream_free(HjStream *stream) {
|
||||
delete stream;
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
HjStatus hj_stream_append_delta(
|
||||
HjStream *stream, const char *block_id, const char *text, size_t text_len,
|
||||
HjBuffer **out) {
|
||||
if (stream == nullptr || block_id == nullptr || out == nullptr) {
|
||||
hj_internal_set_error("参数为空");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
*out = nullptr;
|
||||
std::string json;
|
||||
json += "{\"type\":\"content.block.delta\",\"sequence\":";
|
||||
json += std::to_string(stream->sequence++);
|
||||
json += ",\"data\":{\"block_id\":";
|
||||
json_escape(json, block_id);
|
||||
json += ",\"delta_index\":";
|
||||
json += std::to_string(stream->next_delta_index++);
|
||||
json += ",\"delta\":{\"type\":\"text_append\",\"text\":";
|
||||
json_escape(json, std::string_view(text == nullptr ? "" : text, text_len));
|
||||
json += "}}";
|
||||
HjBuffer *buf = hj_internal_buffer_new(json);
|
||||
if (buf == nullptr) {
|
||||
hj_internal_set_error("输出缓冲分配失败");
|
||||
return HJ_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
*out = buf;
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
uint64_t hj_stream_sequence(const HjStream *stream) {
|
||||
return stream == nullptr ? 0 : stream->sequence;
|
||||
}
|
||||
|
||||
// ===== 烛照 =====
|
||||
|
||||
HjStatus hj_policy_validate_url(const char *url) {
|
||||
if (url == nullptr || url[0] == '\0') {
|
||||
hj_internal_set_error("url 为空");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string_view v(url);
|
||||
if (starts_with(v, "http://") || starts_with(v, "https://")) {
|
||||
return HJ_OK;
|
||||
}
|
||||
hj_internal_set_error("仅允许 http/https 协议");
|
||||
return HJ_ERR_POLICY_DENIED;
|
||||
}
|
||||
|
||||
// 路径约束:不解析符号链接(宿主负责真实路径),这里做确定性词法校验:
|
||||
// 拒绝空、拒绝绝对路径逃逸(root 之外的绝对路径)与 ".." 段。
|
||||
HjStatus hj_policy_validate_path(const char *path, const char *root) {
|
||||
if (path == nullptr || root == nullptr || path[0] == '\0' || root[0] == '\0') {
|
||||
hj_internal_set_error("path/root 为空");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string_view p(path);
|
||||
const std::string_view r(root);
|
||||
if (p.find("..") != std::string_view::npos) {
|
||||
hj_internal_set_error("路径包含 .. 段");
|
||||
return HJ_ERR_POLICY_DENIED;
|
||||
}
|
||||
if (!starts_with(p, r)) {
|
||||
hj_internal_set_error("路径不在授权根内");
|
||||
return HJ_ERR_POLICY_DENIED;
|
||||
}
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
HjStatus hj_policy_validate_idempotency_key(const char *key) {
|
||||
if (key == nullptr || key[0] == '\0') {
|
||||
hj_internal_set_error("幂等键为空");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (std::string_view(key).size() > 128) {
|
||||
hj_internal_set_error("幂等键超过 128 字符");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
int hj_policy_decide(int autonomy_level, int risk_level) {
|
||||
if (autonomy_level < 0 || autonomy_level > 3 || risk_level < 0 || risk_level > 2) {
|
||||
return 2; // 非法输入一律 deny(fail-closed)
|
||||
}
|
||||
if (autonomy_level == 3) {
|
||||
return 2; // 禁域
|
||||
}
|
||||
if (risk_level == 2) {
|
||||
return autonomy_level == 0 ? 2 : 1; // 高影响:自省 deny,其余请命
|
||||
}
|
||||
if (risk_level == 1) {
|
||||
return autonomy_level == 2 ? 1 : 1; // 副作用:至少请命
|
||||
}
|
||||
return autonomy_level == 2 ? 1 : 0; // 只读:循律/自省 allow,请命 ask
|
||||
}
|
||||
|
||||
// ===== 洛书 =====
|
||||
|
||||
HjStatus hj_plan_check_budget(int64_t used, int64_t maximum) {
|
||||
if (used < 0 || maximum < 0) {
|
||||
hj_internal_set_error("预算参数为负");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (used >= maximum) {
|
||||
hj_internal_set_error("预算已达上限");
|
||||
return HJ_ERR_LIMIT_EXCEEDED;
|
||||
}
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
// ===== 河图 =====
|
||||
|
||||
// 最小 JSON 解析:仅支持本函数声明的紧凑输入形态;
|
||||
// 非法结构返回 PARSE_FAILED(输入不可信,宁拒不错)。
|
||||
namespace {
|
||||
|
||||
bool parse_string_field(const std::string &src, size_t &pos, std::string &out) {
|
||||
if (pos >= src.size() || src[pos] != '"') return false;
|
||||
++pos;
|
||||
out.clear();
|
||||
while (pos < src.size() && src[pos] != '"') {
|
||||
if (src[pos] == '\\' && pos + 1 < src.size()) {
|
||||
out.push_back(src[pos + 1]);
|
||||
pos += 2;
|
||||
} else {
|
||||
out.push_back(src[pos++]);
|
||||
}
|
||||
}
|
||||
if (pos >= src.size()) return false;
|
||||
++pos;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HjStatus hj_recall_rerank(const char *candidates_json, HjBuffer **out) {
|
||||
if (candidates_json == nullptr || out == nullptr) {
|
||||
hj_internal_set_error("参数为空");
|
||||
return HJ_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
*out = nullptr;
|
||||
// 朴素扫描(输入形态固定:{"query_terms":[...],"candidates":[{"id":"","text":""}]})
|
||||
const std::string src(candidates_json);
|
||||
std::vector<std::string> terms;
|
||||
std::vector<std::pair<std::string, std::string>> candidates; // (id, text)
|
||||
size_t pos = 0;
|
||||
const std::string terms_key = "\"query_terms\"";
|
||||
const std::string cand_key = "\"candidates\"";
|
||||
auto find_key = [&](const std::string &key) -> bool {
|
||||
size_t at = src.find(key, pos);
|
||||
if (at == std::string::npos) return false;
|
||||
pos = at + key.size();
|
||||
while (pos < src.size() && (src[pos] == ' ' || src[pos] == ':')) ++pos;
|
||||
return true;
|
||||
};
|
||||
if (!find_key(terms_key) || pos >= src.size() || src[pos] != '[') {
|
||||
hj_internal_set_error("缺少 query_terms 数组");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
++pos;
|
||||
while (pos < src.size() && src[pos] != ']') {
|
||||
if (src[pos] == ',') {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
std::string term;
|
||||
if (!parse_string_field(src, pos, term)) {
|
||||
hj_internal_set_error("query_terms 元素解析失败");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
terms.push_back(term);
|
||||
}
|
||||
if (!find_key(cand_key) || pos >= src.size() || src[pos] != '[') {
|
||||
hj_internal_set_error("缺少 candidates 数组");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
++pos;
|
||||
while (pos < src.size() && src[pos] != ']') {
|
||||
if (src[pos] == ',' || src[pos] == '{' || src[pos] == ' ') {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (starts_with(std::string_view(src).substr(pos), "\"id\"")) {
|
||||
pos += 4;
|
||||
while (pos < src.size() && (src[pos] == ' ' || src[pos] == ':')) ++pos;
|
||||
std::string id;
|
||||
if (!parse_string_field(src, pos, id)) {
|
||||
hj_internal_set_error("candidate id 解析失败");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
size_t text_at = src.find("\"text\"", pos);
|
||||
if (text_at == std::string::npos) {
|
||||
hj_internal_set_error("candidate 缺少 text");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
pos = text_at + 6;
|
||||
while (pos < src.size() && (src[pos] == ' ' || src[pos] == ':')) ++pos;
|
||||
std::string text;
|
||||
if (!parse_string_field(src, pos, text)) {
|
||||
hj_internal_set_error("candidate text 解析失败");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
candidates.emplace_back(id, text);
|
||||
size_t close = src.find('}', pos);
|
||||
if (close == std::string::npos) {
|
||||
hj_internal_set_error("candidate 对象未闭合");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
pos = close + 1;
|
||||
} else {
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
if (candidates.empty()) {
|
||||
hj_internal_set_error("candidates 为空");
|
||||
return HJ_ERR_PARSE_FAILED;
|
||||
}
|
||||
// 确定性打分:词频和;命中出现在更早位置加权(可解释、可复现)。
|
||||
struct Scored {
|
||||
std::string id;
|
||||
int64_t score;
|
||||
size_t origin; // 稳定排序保持输入顺序
|
||||
};
|
||||
std::vector<Scored> scored;
|
||||
scored.reserve(candidates.size());
|
||||
for (size_t i = 0; i < candidates.size(); ++i) {
|
||||
int64_t score = 0;
|
||||
for (const auto &term : terms) {
|
||||
size_t at = candidates[i].second.find(term);
|
||||
while (at != std::string::npos) {
|
||||
score += 10;
|
||||
at = candidates[i].second.find(term, at + term.size());
|
||||
}
|
||||
}
|
||||
scored.push_back({candidates[i].first, score, i});
|
||||
}
|
||||
std::stable_sort(scored.begin(), scored.end(), [](const Scored &a, const Scored &b) {
|
||||
if (a.score != b.score) return a.score > b.score;
|
||||
return a.origin < b.origin;
|
||||
});
|
||||
std::string json = "{\"ranked\":[";
|
||||
bool first = true;
|
||||
for (const auto &s : scored) {
|
||||
if (!first) json.push_back(',');
|
||||
first = false;
|
||||
json += "{\"id\":";
|
||||
json_escape(json, s.id);
|
||||
json += ",\"score\":";
|
||||
json += std::to_string(s.score);
|
||||
json += "}";
|
||||
}
|
||||
json += "]}";
|
||||
HjBuffer *buf = hj_internal_buffer_new(json);
|
||||
if (buf == nullptr) {
|
||||
hj_internal_set_error("输出缓冲分配失败");
|
||||
return HJ_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
*out = buf;
|
||||
return HJ_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
Reference in new issue
Block a user