- 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 输入在四端输出字节相同(跨端一致性首次全语言验证)
341 lines
10 KiB
C++
341 lines
10 KiB
C++
// 小周天(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"
|