Files
hunjing/engine/src/modules.cpp
T
chenxin a39cec5287 ✨ 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 输入在四端输出字节相同(跨端一致性首次全语言验证)
2026-09-12 13:54:12 +08:00

341 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 小周天(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"