feat(cfg): W-CFG-KB K3 知识包治理接线——knowledge 配置块+激活发布 Nacos+worker 订阅热物化(复用阶段二三·默认关)
把切片一知识件外置接进受治理配置集,一条「改知识件→定版→激活→worker 物化生效」链,复用配置控制面阶段二/三治理机器零重造:
game-cloud:knowledge 作 content_json 第四内容块(仿 readiness),与 routeA/routeB/readiness 互斥、sanity(packageId/
activeVersion 非空);走向相反=真下发(readiness 进程内空载荷,knowledge 跨进程),激活编成一条路 B 载荷
{knowledge.packageId,knowledge.activeVersion}、经 KNOWLEDGE 档独立 dataId gen-hot-params-knowledge publish 给 worker;
pathA=null→dispatch/reconcile 只走路 B、激活编排一行未改。加 AigcConfigTierEnum.KNOWLEDGE+KnowledgeConfigKeys+NO_ROUTE 文案。
worker:kb_store 加进程级激活版本源槽+attach/detach(对称 genconfig),resolve_active_version 取值链改为治理源(Nacos)>
env 钉版>最新 committed;新 kb_nacos.py(KbActiveVersionSource 订阅 dataId、按 packageId 匹配、激活变更即重物化)+
setup_knowledge_activation() 启动物化+(Nacos 启用时)attach,cheap_service_app 紧接 genconfig 热源挂它。复用 genconfig_nacos 启用旗+代理旁路。
默认关字节不变(TIER2_KB_ROOT 未配→不物化不订阅、read_file 回落仓根;无源→resolve 回落 env/最新);缺文件显式失败沿用
(materialize_active 照抛保留 kb_root、kb_nacos 只在 best-effort 边界接住不咬生成);全 best-effort。admin 本轮做前端契约点
sanity.ts,富编辑视图=紧接下一片(与 readiness 一致)。
主控终审:git 完整(HEAD 未动·无破坏操作)+11 文件精确+读码坐实互斥/routeB载荷/取值链/best-effort/默认关/缺文件失败没削弱
+亲跑 Java codec 34+activation 21(BUILD SUCCESS)+Python test_kb_externalize 21 全绿。真 Nacos 激活端到端排窗口。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5b631e8d5d
commit
c6d6d39a2a
@ -444,6 +444,12 @@ def main() -> None:
|
||||
from worker import genconfig_nacos # noqa: PLC0415 —— 惰性 import,默认关时不牵 nacos 依赖
|
||||
genconfig_nacos.build_and_attach_hot_source(tier="cheap")
|
||||
|
||||
# 知识包激活生效链接线(W-CFG-KB K3,与 genconfig 热源对称):TIER2_KB_ROOT 未配 → no-op(默认关、read_file
|
||||
# 字节不变);已配 → 启动期按激活版物化知识根 +(Nacos 启用时)订阅知识包 dataId,激活推送即重新物化。best-effort:
|
||||
# 任何异常只告警、不阻断 Service 启动 / 生成。
|
||||
from worker import kb_nacos # noqa: PLC0415 —— 惰性 import,默认关时不牵 nacos 依赖
|
||||
kb_nacos.setup_knowledge_activation()
|
||||
|
||||
uvicorn.run(build_cheap_app(), host=host, port=port)
|
||||
|
||||
|
||||
|
||||
@ -267,3 +267,173 @@ def test_kb_addressing_anchors():
|
||||
from worker import store as _src_store
|
||||
assert kb_store._KB_MYSQL_TABLE != _src_store._MYSQL_TABLE
|
||||
assert kb_store._KB_BUCKET_DEFAULT != "tier2-src"
|
||||
|
||||
|
||||
# ─────────────────────────── K3:治理激活版本源接线(resolve 读 Nacos 激活版 · env 回落 · 变更即物化)───────────────────────────
|
||||
|
||||
from worker import kb_nacos # noqa: E402
|
||||
|
||||
|
||||
class _FakeSource:
|
||||
"""满足 kb_store 激活版本源 get(package_id)->version|None 契约的假源(单测注入,不连 Nacos)。"""
|
||||
|
||||
def __init__(self, version, *, package_id="cheap-kb", raise_exc=None):
|
||||
self._version = version
|
||||
self._package_id = package_id
|
||||
self._raise = raise_exc
|
||||
|
||||
def get(self, package_id):
|
||||
if self._raise is not None:
|
||||
raise self._raise
|
||||
if self._package_id is not None and package_id != self._package_id:
|
||||
return None
|
||||
return self._version
|
||||
|
||||
|
||||
def test_resolve_prefers_governance_source(tmp_path, monkeypatch):
|
||||
"""K3 取值链:治理激活版本源(Nacos 下发)> env 钉版 > 最新 committed;detach 后回落。"""
|
||||
store = _store(tmp_path)
|
||||
latest = store.save([{"path": "a.md", "content": "A1"}], now_ts=_NOW, package_id="cheap-kb")
|
||||
monkeypatch.setenv("TIER2_KB_ACTIVE_VERSION", "v-env-pinned") # env 钉了别的版
|
||||
kb_store.attach_active_version_source(_FakeSource("v-gov-active"))
|
||||
try:
|
||||
# 源优先于 env 与最新 committed。
|
||||
assert resolve_active_version(store, package_id="cheap-kb") == "v-gov-active"
|
||||
finally:
|
||||
kb_store.detach_active_version_source()
|
||||
# detach 后回落 env 钉版。
|
||||
assert resolve_active_version(store, package_id="cheap-kb") == "v-env-pinned"
|
||||
monkeypatch.delenv("TIER2_KB_ACTIVE_VERSION", raising=False)
|
||||
# 无源无 env → 最新 committed(切片一行为、字节不变)。
|
||||
assert resolve_active_version(store, package_id="cheap-kb") == latest["versionId"]
|
||||
|
||||
|
||||
def test_resolve_source_none_falls_back(tmp_path, monkeypatch):
|
||||
"""源返回 None(dataId 尚无激活版)→ 回落 env,再回落最新 committed。"""
|
||||
store = _store(tmp_path)
|
||||
v = store.save([{"path": "a.md", "content": "A1"}], now_ts=_NOW, package_id="cheap-kb")
|
||||
monkeypatch.delenv("TIER2_KB_ACTIVE_VERSION", raising=False)
|
||||
kb_store.attach_active_version_source(_FakeSource(None))
|
||||
try:
|
||||
assert resolve_active_version(store, package_id="cheap-kb") == v["versionId"]
|
||||
finally:
|
||||
kb_store.detach_active_version_source()
|
||||
|
||||
|
||||
def test_resolve_source_error_is_best_effort(tmp_path, monkeypatch):
|
||||
"""源取值抛异常 → best-effort 静默回落 env/最新 committed,绝不因治理源拖垮解析(→拖垮生成)。"""
|
||||
store = _store(tmp_path)
|
||||
v = store.save([{"path": "a.md", "content": "A1"}], now_ts=_NOW, package_id="cheap-kb")
|
||||
monkeypatch.delenv("TIER2_KB_ACTIVE_VERSION", raising=False)
|
||||
kb_store.attach_active_version_source(_FakeSource(None, raise_exc=RuntimeError("nacos down")))
|
||||
try:
|
||||
assert resolve_active_version(store, package_id="cheap-kb") == v["versionId"]
|
||||
finally:
|
||||
kb_store.detach_active_version_source()
|
||||
|
||||
|
||||
def test_resolve_source_package_mismatch_falls_back(tmp_path, monkeypatch):
|
||||
"""源里 dataId 标的是别的包 → get 返 None → 回落(不冒充本包激活版)。"""
|
||||
store = _store(tmp_path)
|
||||
v = store.save([{"path": "a.md", "content": "A1"}], now_ts=_NOW, package_id="cheap-kb")
|
||||
monkeypatch.delenv("TIER2_KB_ACTIVE_VERSION", raising=False)
|
||||
kb_store.attach_active_version_source(_FakeSource("v-other", package_id="some-other-kb"))
|
||||
try:
|
||||
assert resolve_active_version(store, package_id="cheap-kb") == v["versionId"]
|
||||
finally:
|
||||
kb_store.detach_active_version_source()
|
||||
|
||||
|
||||
class _FakeNacosClient:
|
||||
"""假 v1 NacosClient:get_config 返预置内容,add_config_watcher 记回调供手动触发(不连真 Nacos)。"""
|
||||
|
||||
def __init__(self, initial=None):
|
||||
self._content = initial
|
||||
self._cb = None
|
||||
|
||||
def get_config(self, data_id, group):
|
||||
return self._content
|
||||
|
||||
def add_config_watcher(self, data_id, group, cb):
|
||||
self._cb = cb
|
||||
|
||||
def push(self, content):
|
||||
"""模拟 Nacos 长轮询推送:更新内容 + 以 v1 回调 dict 形态触发回调。"""
|
||||
self._content = content
|
||||
if self._cb is not None:
|
||||
self._cb({"data_id": "gen-hot-params-knowledge", "group": "DEFAULT_GROUP", "content": content})
|
||||
|
||||
|
||||
def _kb_dataid_content(package_id, version):
|
||||
"""组一份知识包 dataId 内容(与 game-cloud codec 产的复合键载荷逐字对齐)。"""
|
||||
import json
|
||||
return json.dumps({"knowledge.packageId": package_id, "knowledge.activeVersion": version})
|
||||
|
||||
|
||||
def test_kb_source_parse_and_get(tmp_path):
|
||||
"""KbActiveVersionSource 解析 dataId 复合键 → get 返激活版;包 id 不符返 None(初读不物化)。"""
|
||||
store = _store(tmp_path)
|
||||
kb_root = tmp_path / "kb-active"
|
||||
client = _FakeNacosClient(initial=_kb_dataid_content("cheap-kb", "v0001-aaa"))
|
||||
src = kb_nacos.KbActiveVersionSource(
|
||||
client=client, data_id="gen-hot-params-knowledge", group="DEFAULT_GROUP",
|
||||
store=store, kb_root=kb_root, package_id="cheap-kb")
|
||||
src.start() # 初读 populate 版本,不物化
|
||||
assert src.get("cheap-kb") == "v0001-aaa"
|
||||
assert src.get("some-other-kb") is None # 包 id 不符不冒充
|
||||
assert not kb_root.exists() # 初读不物化(启动物化由 setup 显式做)
|
||||
|
||||
|
||||
def test_kb_source_change_triggers_materialize(tmp_path, monkeypatch):
|
||||
"""激活推送(dataId 变更)→ 源 _on_change 触发重新物化 → read_file 读到新一版知识件。"""
|
||||
store = _store(tmp_path)
|
||||
kb_root = tmp_path / "kb-active"
|
||||
v1 = store.save([{"path": "skill.md", "content": "GOV-V1"}], now_ts=_NOW, package_id="cheap-kb")
|
||||
v2 = store.save([{"path": "skill.md", "content": "GOV-V2"}], now_ts=_NOW + 10, package_id="cheap-kb")
|
||||
|
||||
client = _FakeNacosClient(initial=_kb_dataid_content("cheap-kb", v1["versionId"]))
|
||||
src = kb_nacos.KbActiveVersionSource(
|
||||
client=client, data_id="gen-hot-params-knowledge", group="DEFAULT_GROUP",
|
||||
store=store, kb_root=kb_root, package_id="cheap-kb")
|
||||
src.start() # 初读 v1(不物化)
|
||||
kb_store.attach_active_version_source(src)
|
||||
monkeypatch.setenv("TIER2_KB_ROOT", str(kb_root))
|
||||
try:
|
||||
# 启动物化(setup 会做;这里显式模拟)→ resolve 读源 v1 → 物化 v1。
|
||||
materialize_active(store, kb_root)
|
||||
assert cheap_run.read_file("skill.md")["content"] == "GOV-V1"
|
||||
# 激活推送 v2 → 源 _on_change 触发重新物化。
|
||||
client.push(_kb_dataid_content("cheap-kb", v2["versionId"]))
|
||||
assert src.get("cheap-kb") == v2["versionId"]
|
||||
assert cheap_run.read_file("skill.md")["content"] == "GOV-V2"
|
||||
finally:
|
||||
kb_store.detach_active_version_source()
|
||||
|
||||
|
||||
def test_kb_source_materialize_best_effort_on_missing(tmp_path, monkeypatch):
|
||||
"""激活推送到一个缺文件的版本 → 物化【显式失败】被 best-effort 接住(不抛)、kb_root 保留上一份完整包。"""
|
||||
store = _store(tmp_path)
|
||||
kb_root = tmp_path / "kb-active"
|
||||
v1 = store.save([{"path": "skill.md", "content": "GOOD-V1"}], now_ts=_NOW, package_id="cheap-kb")
|
||||
v2 = store.save(
|
||||
[{"path": "skill.md", "content": "V2"}, {"path": "extra.md", "content": "E2"}],
|
||||
now_ts=_NOW + 10, package_id="cheap-kb")
|
||||
|
||||
client = _FakeNacosClient(initial=_kb_dataid_content("cheap-kb", v1["versionId"]))
|
||||
src = kb_nacos.KbActiveVersionSource(
|
||||
client=client, data_id="gen-hot-params-knowledge", group="DEFAULT_GROUP",
|
||||
store=store, kb_root=kb_root, package_id="cheap-kb")
|
||||
src.start()
|
||||
kb_store.attach_active_version_source(src)
|
||||
monkeypatch.setenv("TIER2_KB_ROOT", str(kb_root))
|
||||
try:
|
||||
materialize_active(store, kb_root) # 物化 v1
|
||||
assert cheap_run.read_file("skill.md")["content"] == "GOOD-V1"
|
||||
# 破坏 v2 的一个文件(manifest 列了但取不到)。
|
||||
victim = tmp_path / "_kb-store" / "cheap-kb" / v2["versionId"] / "files" / "extra.md"
|
||||
victim.unlink()
|
||||
# 推送 v2 → _on_change 触发物化 → 显式失败被 best-effort 接住(不抛),kb_root 保留 v1。
|
||||
client.push(_kb_dataid_content("cheap-kb", v2["versionId"])) # 绝不抛
|
||||
assert cheap_run.read_file("skill.md")["content"] == "GOOD-V1" # 仍读 v1 完整包
|
||||
finally:
|
||||
kb_store.detach_active_version_source()
|
||||
|
||||
@ -5,25 +5,48 @@
|
||||
* 少一趟「定版→激活失败」往返。这是前端便利校验、非信任边界——真兜底仍是后端激活时的 codec 校验(走查断言 4 用真后端)。
|
||||
*
|
||||
* 校验项(逐条对 codec:build / buildPathA / validateChatModelConfig):
|
||||
* 1. 至少一路:routeA 或 routeB(routeB 须非空对象);
|
||||
* 1. 至少一块:routeA / routeB(非空对象)/ readiness / knowledge(四者任一即有落点);
|
||||
* 2. 有 routeA 则 agentId/sessionId 非空;
|
||||
* 3. 有 routeA 则 prompt 非空(system_prompt 是激活前置);
|
||||
* 4. routeA.session.chat_model_config 存在且为对象,四字段 type/credential_id/model 非空、parameters 为对象;
|
||||
* 5. max_tokens > thinking_budget(两者都给时;兼容扁平 thinking_budget 与嵌套 thinking.budget_tokens)。
|
||||
* 5. max_tokens > thinking_budget(两者都给时;兼容扁平 thinking_budget 与嵌套 thinking.budget_tokens);
|
||||
* 6. knowledge 块(W-CFG-KB K3,跨档知识包激活指针):packageId/activeVersion 非空、与其余块互斥。
|
||||
*
|
||||
* 说明:readiness(W-CFG-EXT 质量评分)与 knowledge(知识包激活)是两个跨档特例块,进程内/跨进程消费、不下发生成路。
|
||||
* 本预检对 readiness 只认「块存在=有落点」(其十字段数值校验是 admin 后续片,非本前端便利校验职责);对 knowledge
|
||||
* 给最小字段预检。真兜底仍是后端激活时的 codec.build(值域 sanity + 互斥)。
|
||||
*
|
||||
* @param draftPrompt 草稿 prompt 正文
|
||||
* @param contentObj 已解析的 content_json 对象(routeA/routeB 嵌套信封)
|
||||
* @param contentObj 已解析的 content_json 对象(routeA/routeB/readiness/knowledge 信封)
|
||||
* @returns null=校验通过;否则返回首个错误的中文说明
|
||||
*/
|
||||
export function checkRouteASanity(draftPrompt: string | undefined, contentObj: any): string | null {
|
||||
const obj = contentObj && typeof contentObj === 'object' ? contentObj : {}
|
||||
const routeA = obj.routeA
|
||||
const routeB = obj.routeB
|
||||
const readiness = obj.readiness
|
||||
const knowledge = obj.knowledge
|
||||
const hasA = routeA != null && typeof routeA === 'object'
|
||||
const hasB = routeB != null && typeof routeB === 'object' && Object.keys(routeB).length > 0
|
||||
const hasReadiness = readiness != null && typeof readiness === 'object'
|
||||
const hasKnowledge = knowledge != null && typeof knowledge === 'object'
|
||||
|
||||
if (!hasA && !hasB) {
|
||||
return 'content_json 无任何可下发路由(至少需要 routeA 或非空 routeB 一路)'
|
||||
if (!hasA && !hasB && !hasReadiness && !hasKnowledge) {
|
||||
return 'content_json 无任何可下发路由(至少需要 routeA / routeB / readiness / knowledge 一块)'
|
||||
}
|
||||
|
||||
// knowledge 块(跨档知识包激活指针):与其余块互斥 + packageId/activeVersion 非空(镜像后端 codec.buildKnowledgePathB)。
|
||||
if (hasKnowledge) {
|
||||
if (hasA || hasB || hasReadiness) {
|
||||
return 'knowledge 块与 routeA/routeB/readiness 互斥:知识包配置集只下发激活知识包版本'
|
||||
}
|
||||
if (!hasText(knowledge.packageId)) {
|
||||
return 'knowledge.packageId 缺失或为空(知识包 id 必填)'
|
||||
}
|
||||
if (!hasText(knowledge.activeVersion)) {
|
||||
return 'knowledge.activeVersion 缺失或为空(激活知识包须显式钉版本)'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (hasA) {
|
||||
|
||||
@ -32,7 +32,17 @@ public enum AigcConfigTierEnum {
|
||||
* 账本指针前移」的纯账本收口。故其配置集内容与 routeA/routeB 互斥(codec 激活前置校验),也不参与按 cheap/tier2
|
||||
* 拼 Nacos dataId 的下发逻辑。
|
||||
*/
|
||||
READINESS("readiness", "质量评分");
|
||||
READINESS("readiness", "质量评分"),
|
||||
/**
|
||||
* 知识包档(W-CFG-KB K3 · 跨档特例):承载便宜档生成 agent 的知识件(设计范式 skill/黄金脚手架/few-shot)
|
||||
* 外置后的「当前激活知识包版本」指针,内容 = {@code {packageId, activeVersion}}。它与质量评分档一样不属任何
|
||||
* 生成档,但<b>与质量评分档相反——要真下发到 Nacos</b>:激活时经 {@link GenConfigContentCodec} 编成一条路 B 载荷
|
||||
* (复合键 {@code knowledge.packageId}/{@code knowledge.activeVersion}),按本档 dataId 模板 publish 到
|
||||
* {@code gen-hot-params-knowledge};便宜档 worker 起本档专用热源订阅它,读到激活版本号后据 kb_store 物化知识根,
|
||||
* 改一版知识件 = 定版 + 激活即生效、不重部署。故其配置集内容与 routeA/routeB/readiness 互斥(codec 激活前置校验),
|
||||
* 走独立 dataId、不冲便宜档/富游戏档的 gen-hot-params-cheap/tier2 预算门阈值键。
|
||||
*/
|
||||
KNOWLEDGE("knowledge", "知识包");
|
||||
|
||||
/** 档位码(落库 tier 列) */
|
||||
private final String tier;
|
||||
|
||||
@ -91,7 +91,7 @@ public interface ErrorCodeConstants {
|
||||
/** 值域 sanity 校验不通过:负预算/空模型名/阈值出域/max_tokens≤thinking/chat_model_config 不完整等(设计 §3.6,激活前置,拒绝不发任何一路)。占位符带明细。 */
|
||||
ErrorCode AIGC_CONFIG_ACTIVATE_SANITY_FAILED = new ErrorCode(1_101_004_009, "配置值域校验不通过:{}");
|
||||
/** 激活内容缺路由:content_json 未声明可激活的 routeA/routeB(无落点可下发)。 */
|
||||
ErrorCode AIGC_CONFIG_ACTIVATE_NO_ROUTE = new ErrorCode(1_101_004_010, "配置内容未声明任何可激活的下发路由(routeA/routeB/readiness 均缺)");
|
||||
ErrorCode AIGC_CONFIG_ACTIVATE_NO_ROUTE = new ErrorCode(1_101_004_010, "配置内容未声明任何可激活的下发路由(routeA/routeB/readiness/knowledge 均缺)");
|
||||
/** 双路下发失败:某路 PATCH/publish 失败 → 整批判未生效、已把成功路补偿重推回上一激活版、指针不前移、可重试(设计 §3.6)。占位符带明细。 */
|
||||
ErrorCode AIGC_CONFIG_ACTIVATE_DISPATCH_FAILED = new ErrorCode(1_101_004_011, "配置激活下发失败(已判未生效并补偿回上一激活版,可重试):{}");
|
||||
|
||||
|
||||
@ -61,6 +61,18 @@ import static com.wanxiang.huijing.framework.common.exception.util.ServiceExcept
|
||||
* 激活前置 readiness 值域 sanity(不过即拒):四维权重各 ∈[0,1] 且和 ≈1.0(±0.001)、中性/稳定性两级分 ∈[0,1]、
|
||||
* 效率预算 > 0、修复轮 good_max ≤ ok_max;并与 routeA/routeB 互斥(readiness 存在时禁二者,反之亦然)。
|
||||
*
|
||||
* <p><b>第四块 knowledge(W-CFG-KB K3 · 知识包激活指针,与其余三块互斥)</b>:承载便宜档生成 agent 的知识件外置后的
|
||||
* 「当前激活知识包版本」指针,契约:
|
||||
* <pre>
|
||||
* { "knowledge": { "packageId": "cheap-kb", "activeVersion": "v0001-abcdef123456" } }
|
||||
* </pre>
|
||||
* 它与 readiness 都是跨档特例,但走向相反:readiness 进程内消费产空载荷,knowledge 是<b>跨进程</b>(game-cloud 治理、
|
||||
* 便宜档 worker 消费),故激活时编成一条路 B 载荷 {@code {"knowledge.packageId":..,"knowledge.activeVersion":..}}
|
||||
* (复合键与 routeB 的 area.key 同扁平口径),经 KNOWLEDGE 档 {@code gen-hot-params-knowledge} dataId publish 给 worker。
|
||||
* worker 读到激活版本号后据 kb_store 物化知识根,agent 的 read_file 即读到新一版设计范式——改一版知识件 = 定版 + 激活
|
||||
* 即生效、不重部署。激活前置 sanity:packageId 与 activeVersion 非空(激活知识包须显式钉版);与 routeA/routeB/readiness
|
||||
* 互斥(知识包配置集只下发激活版本,不混生成路/评分块)。
|
||||
*
|
||||
* @author 绘境AI
|
||||
*/
|
||||
@Slf4j
|
||||
@ -88,19 +100,33 @@ public final class GenConfigContentCodec {
|
||||
JsonNode routeA = root.get("routeA");
|
||||
JsonNode routeB = root.get("routeB");
|
||||
JsonNode readiness = root.get(ReadinessConfigKeys.BLOCK);
|
||||
JsonNode knowledge = root.get(KnowledgeConfigKeys.BLOCK);
|
||||
boolean hasA = routeA != null && routeA.isObject();
|
||||
boolean hasB = routeB != null && routeB.isObject() && routeB.size() > 0;
|
||||
boolean hasReadiness = readiness != null && readiness.isObject();
|
||||
if (!hasA && !hasB && !hasReadiness) {
|
||||
boolean hasKnowledge = knowledge != null && knowledge.isObject();
|
||||
if (!hasA && !hasB && !hasReadiness && !hasKnowledge) {
|
||||
// 无任何可下发路由:没有落点(设计 §4 步骤3 交付以「双路下发」为核心,至少一路)
|
||||
throw exception(AIGC_CONFIG_ACTIVATE_NO_ROUTE);
|
||||
}
|
||||
// 互斥(W-CFG-EXT):质量评分块是跨档、进程内消费、无外投影,不与生成下发路(routeA/routeB)共存于同一配置集。
|
||||
// 互斥①(W-CFG-KB):knowledge 是跨进程、走独立 dataId 的知识包激活指针,不与生成下发路(routeA/routeB)或进程内
|
||||
// 评分块(readiness)共存于同一配置集——混配会让激活按档拼错 dataId 或让投影语义打架,故激活前置拒(与 readiness
|
||||
// 同纪律,让 KNOWLEDGE 档的 enum-value 建模对既有按档下发逻辑安全)。
|
||||
if (hasKnowledge && (hasA || hasB || hasReadiness)) {
|
||||
throw sanity("knowledge 块与 routeA/routeB/readiness 互斥:知识包配置集只下发激活知识包版本,不得混配生成路/评分块");
|
||||
}
|
||||
// 互斥②(W-CFG-EXT):质量评分块是跨档、进程内消费、无外投影,不与生成下发路(routeA/routeB)共存于同一配置集。
|
||||
// 若一个质量评分配置集误混 routeB,激活会按 tier=readiness 拼出伪 dataId(resolveDataId 不认档但仍拼)真 publish;
|
||||
// 误混 routeA 则 resolveServiceUrl 返 null 下发失败。故激活前置拒,让 enum-value 建模对既有按档下发逻辑安全。
|
||||
if (hasReadiness && (hasA || hasB)) {
|
||||
throw sanity("readiness 块与 routeA/routeB 互斥:质量评分配置集不下发 prompt/模型/预算/门阈值,不得混配生成路");
|
||||
}
|
||||
if (hasKnowledge) {
|
||||
// 知识包配置集:值域 sanity + 产一条路 B 载荷(复合键 knowledge.packageId/activeVersion),按 KNOWLEDGE 档
|
||||
// dataId 模板 publish 到 gen-hot-params-knowledge;便宜档 worker 订阅它、读激活版本号后据 kb_store 物化知识根
|
||||
// (激活即生效)。pathA=null → dispatch 天然只走路 B(reconcile 同理只对账路 B),激活编排一行不改。
|
||||
return new GenConfigActivationPayload(null, buildKnowledgePathB(knowledge));
|
||||
}
|
||||
if (hasReadiness) {
|
||||
// 质量评分配置集:只做值域 sanity,不产任何外投影载荷。空载荷(pathA=pathB=null)→ 现有 dispatch/reconcile
|
||||
// 天然短路成「sanity + 账本收口、无投影、恒一致」(激活编排一行不改,见 GenConfigActivationServiceImpl.dispatch)。
|
||||
@ -177,6 +203,29 @@ public final class GenConfigContentCodec {
|
||||
return v.asInt();
|
||||
}
|
||||
|
||||
// ==================== knowledge:知识包激活指针 → 路 B 载荷(W-CFG-KB K3)====================
|
||||
|
||||
/**
|
||||
* 校验 content_json.knowledge 块并编成一条路 B 载荷(激活前置,不过即拒):packageId 与 activeVersion 均须非空——
|
||||
* 激活知识包必须显式钉一个版本(不像预算旋钮可留默认),worker 据 activeVersion {@code fetch} + 物化,取不到即物化
|
||||
* 显式失败、回落上一份完整包。产出 = {@code {"knowledge.packageId":"...","knowledge.activeVersion":"..."}} 扁平 JSON
|
||||
* (与 routeB 的 area.key 复合键同口径),经 KNOWLEDGE 档 {@code gen-hot-params-knowledge} dataId publish 给 worker。
|
||||
*/
|
||||
private String buildKnowledgePathB(JsonNode knowledge) {
|
||||
String packageId = textOrNull(knowledge, KnowledgeConfigKeys.PACKAGE_ID);
|
||||
String activeVersion = textOrNull(knowledge, KnowledgeConfigKeys.ACTIVE_VERSION);
|
||||
if (!StringUtils.hasText(packageId)) {
|
||||
throw sanity("knowledge." + KnowledgeConfigKeys.PACKAGE_ID + " 缺失或为空(知识包 id 必填)");
|
||||
}
|
||||
if (!StringUtils.hasText(activeVersion)) {
|
||||
throw sanity("knowledge." + KnowledgeConfigKeys.ACTIVE_VERSION + " 缺失或为空(激活知识包须显式钉版本)");
|
||||
}
|
||||
ObjectNode out = JSON.createObjectNode();
|
||||
out.put(KnowledgeConfigKeys.NACOS_KEY_PACKAGE_ID, packageId);
|
||||
out.put(KnowledgeConfigKeys.NACOS_KEY_ACTIVE_VERSION, activeVersion);
|
||||
return writeJson(out);
|
||||
}
|
||||
|
||||
// ==================== 路 A:/agent + /session 请求体组装 + 值域校验 ====================
|
||||
|
||||
private GenConfigActivationPayload.PathA buildPathA(String prompt, JsonNode routeA) {
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
package com.wanxiang.huijing.game.module.aigc.service.config;
|
||||
|
||||
/**
|
||||
* 知识包配置集(W-CFG-KB K3)的内容契约常量:content_json 里 {@code knowledge} 块的块名与字段名、
|
||||
* 知识包配置集的约定业务键,以及下发到 Nacos 的复合键。
|
||||
*
|
||||
* <p><b>为何单独成类</b>:这批字段名是「配置集内容(doc)」「激活编解码(code)」「worker 读激活版(跨进程)」三方之间的
|
||||
* 唯一约定——{@link GenConfigContentCodec} 激活前置的 knowledge 值域 sanity 与产路 B 载荷、便宜档 worker 侧
|
||||
* {@code kb_nacos.KbActiveVersionSource} 解析 Nacos dataId 都引用它,把字段名收进一处,避免多处各写一份字符串字面量、
|
||||
* 漂移无人对账。字段<b>值</b>(具体 packageId / versionId)不在这里,来自运营在配置集里填的当前激活知识包版本;
|
||||
* 本类只管「用哪个键名读写」。
|
||||
*
|
||||
* <p><b>与质量评分(readiness)块的区别</b>:readiness 是进程内消费、无外投影(空载荷);knowledge 是<b>跨进程</b>——
|
||||
* game-cloud 治理它、便宜档生成 worker 消费它,故必须经 Nacos 下发一条路 B 载荷把激活版本号送到 worker。承载 dataId =
|
||||
* {@code gen-hot-params-knowledge}(由 KNOWLEDGE 档套 {@code gen-hot-params-{tier}} 模板得到),内容是激活版本指针、
|
||||
* 不是预算门阈值热参,故与便宜档/富游戏档的生效 dataId 天然隔离、不相互冲刷。
|
||||
*
|
||||
* @author 绘境AI(W-CFG-KB · K3 知识包治理接线)
|
||||
*/
|
||||
public final class KnowledgeConfigKeys {
|
||||
|
||||
private KnowledgeConfigKeys() {
|
||||
}
|
||||
|
||||
/** content_json 里承载知识包激活指针的块名(平级于 routeA/routeB/readiness,互斥)。 */
|
||||
public static final String BLOCK = "knowledge";
|
||||
|
||||
/**
|
||||
* 知识包稳定标识字段(一套便宜档知识语料一个 id;其下挂多版本)。与 worker 侧
|
||||
* {@code kb_store.DEFAULT_PACKAGE_ID} 对齐,默认 {@code cheap-kb}。
|
||||
*/
|
||||
public static final String PACKAGE_ID = "packageId";
|
||||
/**
|
||||
* 当前激活的知识包版本号字段。取值 = worker 侧 {@code kb_store} 落库时派生的 versionId(形如
|
||||
* {@code vXXXX-hash12});worker 据它 {@code fetch} + 物化,取不到即物化显式失败、回落上一份。
|
||||
*/
|
||||
public static final String ACTIVE_VERSION = "activeVersion";
|
||||
|
||||
/**
|
||||
* 知识包配置集的约定业务键(set_key)。跨档单例:全租户内知识包配置集用这一个业务键,
|
||||
* uk_set_key 保证至多一条,寻址无歧义。运营在配置中心新建知识包配置集时须用此业务键。
|
||||
*/
|
||||
public static final String KNOWLEDGE_SET_KEY = "knowledge-default";
|
||||
|
||||
// ---- 下发到 Nacos 的复合键(worker 侧 kb_nacos 读同名键;与 genconfig 的 area.key 复合键同扁平口径)----
|
||||
/** publish 到知识包 dataId 的「包 id」键 = {@code knowledge.packageId}。 */
|
||||
public static final String NACOS_KEY_PACKAGE_ID = BLOCK + "." + PACKAGE_ID;
|
||||
/** publish 到知识包 dataId 的「激活版本」键 = {@code knowledge.activeVersion}。 */
|
||||
public static final String NACOS_KEY_ACTIVE_VERSION = BLOCK + "." + ACTIVE_VERSION;
|
||||
|
||||
}
|
||||
@ -65,6 +65,9 @@ class GenConfigActivationServiceImplTest extends BaseMockitoUnitTest {
|
||||
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
|
||||
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
|
||||
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
|
||||
/** 知识包配置集内容(knowledge-only,W-CFG-KB K3):codec 产一条路 B 载荷、激活只 publish 到 gen-hot-params-knowledge。 */
|
||||
private static final String KNOWLEDGE_CONTENT =
|
||||
"{\"knowledge\":{\"packageId\":\"cheap-kb\",\"activeVersion\":\"v0001-abcdef123456\"}}";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@ -239,6 +242,31 @@ class GenConfigActivationServiceImplTest extends BaseMockitoUnitTest {
|
||||
verify(nacosClient, never()).publish(anyString(), any());
|
||||
}
|
||||
|
||||
// ==================== 知识包配置集(knowledge-only,W-CFG-KB K3):只走路 B(publish gen-hot-params-knowledge) ====================
|
||||
|
||||
/**
|
||||
* K3:知识包配置集(knowledge-only)激活 → codec 产一条路 B 载荷(knowledge.packageId/activeVersion)→ dispatch 只走路 B:
|
||||
* tier=knowledge 经 {@link NacosConfigPublishClient#publish}("knowledge", ...) 下发到 gen-hot-params-knowledge dataId
|
||||
* (便宜档 worker 订阅它读激活版物化知识根);路 A(Agent Service)全程零触碰;账本正常收口。
|
||||
*/
|
||||
@Test
|
||||
void testActivate_knowledgeOnly_publishOnlyPathB() {
|
||||
stubSet(1L, AigcConfigStatusEnum.PENDING_REVIEW, null, "knowledge"); // 跨档知识包档
|
||||
stubVersion(1L, 4000L, null, KNOWLEDGE_CONTENT); // 无 prompt、只有 knowledge 块
|
||||
when(genConfigService.beginActivating(1L, 4000L))
|
||||
.thenReturn(new GenConfigActivationSnapshot(AigcConfigStatusEnum.PENDING_REVIEW.getStatus(), null));
|
||||
when(nacosClient.publish(anyString(), any())).thenReturn(GenConfigDispatchResult.succeeded());
|
||||
|
||||
service.activate(1L, 4000L);
|
||||
|
||||
// 只走路 B:按 knowledge 档 publish(resolveDataId 拼 gen-hot-params-knowledge)、路 A 零触碰
|
||||
verify(nacosClient).publish(eq("knowledge"), any());
|
||||
verify(agentClient, never()).patch(anyString(), any());
|
||||
// 账本正常收口(转 ACTIVE + 前移指针),不失败复位
|
||||
verify(genConfigService).markVersionActivated(1L, 4000L);
|
||||
verify(genConfigService, never()).abortActivating(any(), any());
|
||||
}
|
||||
|
||||
// ==================== 前置拒绝:不写账本、不下发 ====================
|
||||
|
||||
/** 值域非法(负预算)→ 激活前置拒:不落 ACTIVATING、不发任何一路。 */
|
||||
|
||||
@ -305,4 +305,55 @@ class GenConfigContentCodecTest {
|
||||
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf("sys", c).getCode());
|
||||
}
|
||||
|
||||
// ==================== knowledge:知识包激活指针配置集(W-CFG-KB K3) ====================
|
||||
|
||||
/** 合法 knowledge 块(packageId + activeVersion)→ 一条路 B 载荷(复合键下发,worker 读激活版物化知识根)。 */
|
||||
@Test
|
||||
void testBuild_knowledgeOnly_pathBOnly() {
|
||||
GenConfigActivationPayload p = codec().build(null,
|
||||
"{\"knowledge\":{\"packageId\":\"cheap-kb\",\"activeVersion\":\"v0001-abcdef123456\"}}");
|
||||
assertFalse(p.hasPathA(), "知识包配置集不下发路 A");
|
||||
assertTrue(p.hasPathB(), "知识包配置集走路 B(publish gen-hot-params-knowledge)");
|
||||
assertNull(p.pathA());
|
||||
// 复合键与 area.key 同扁平口径,worker 侧 kb_nacos 读同名键
|
||||
assertTrue(p.pathBJson().contains("knowledge.packageId"));
|
||||
assertTrue(p.pathBJson().contains("knowledge.activeVersion"));
|
||||
assertTrue(p.pathBJson().contains("cheap-kb"));
|
||||
assertTrue(p.pathBJson().contains("v0001-abcdef123456"));
|
||||
}
|
||||
|
||||
/** knowledge 缺 activeVersion → sanity 拒(激活知识包须显式钉版)。 */
|
||||
@Test
|
||||
void testKnowledgeSanity_missingActiveVersion() {
|
||||
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(),
|
||||
sanityOf(null, "{\"knowledge\":{\"packageId\":\"cheap-kb\"}}").getCode());
|
||||
}
|
||||
|
||||
/** knowledge packageId 空白 → sanity 拒。 */
|
||||
@Test
|
||||
void testKnowledgeSanity_blankPackageId() {
|
||||
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(),
|
||||
sanityOf(null, "{\"knowledge\":{\"packageId\":\" \",\"activeVersion\":\"v1\"}}").getCode());
|
||||
}
|
||||
|
||||
/** 互斥:knowledge 与 routeB 共存 → sanity 拒(知识包块不得混配生成下发路)。 */
|
||||
@Test
|
||||
void testKnowledge_mutexWithRouteB() {
|
||||
String c = "{\"knowledge\":{\"packageId\":\"cheap-kb\",\"activeVersion\":\"v1\"},"
|
||||
+ "\"routeB\":{\"budget.cheap_rmb_hard_limit\":10.0}}";
|
||||
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
|
||||
}
|
||||
|
||||
/** 互斥:knowledge 与 readiness 共存 → sanity 拒(两个跨档特例块不得混配)。 */
|
||||
@Test
|
||||
void testKnowledge_mutexWithReadiness() {
|
||||
String c = "{\"knowledge\":{\"packageId\":\"cheap-kb\",\"activeVersion\":\"v1\"},"
|
||||
+ "\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
|
||||
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
|
||||
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
|
||||
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
|
||||
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
|
||||
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
241
tier2/gen-worker/worker/kb_nacos.py
Normal file
241
tier2/gen-worker/worker/kb_nacos.py
Normal file
@ -0,0 +1,241 @@
|
||||
"""kb_nacos.py —— 知识包激活版本源:把 kb_store.resolve_active_version 的读取后端接到 Nacos(W-CFG-KB K3)。
|
||||
|
||||
【这份解决什么问题】
|
||||
切片一(kb_store)已把便宜档生成 agent 的知识件外置成版本化知识包(存 MinIO + manifest 落 MySQL),并留了
|
||||
「当前激活版本」接缝 resolve_active_version(切片一只会读 env 钉版 / 最新 committed)。K3 把这条接缝接到 game-cloud
|
||||
的配置控制面:运营在配置控制台改一版知识件、定版、激活,game-cloud 把「当前激活知识包版本」publish 到一个专用
|
||||
Nacos dataId(gen-hot-params-knowledge);本模块在 worker 启动期订阅它,读到激活版本号后据 kb_store 物化知识根——
|
||||
激活推送到达即重新物化,agent 下次 read_file 就读到新一版设计范式,不重部署。
|
||||
|
||||
【与 genconfig 热源的关系:同范式、独立 dataId、独立槽】
|
||||
genconfig 的 Nacos 热源(genconfig_nacos)承载预算/门阈值旋钮(area.key 复合键),订阅便宜档 gen-hot-params-cheap;
|
||||
知识包激活版本是另一码事(激活版本指针,不是热参),故走【独立 dataId】gen-hot-params-knowledge(由 game-cloud
|
||||
KNOWLEDGE 档套 gen-hot-params-{tier} 模板得到),publish 一个不冲另一个的键。两者各 attach 各自的进程级槽:预算旋钮
|
||||
进 genconfig._hot_source,知识包激活版本进 kb_store._active_version_source。本模块复用 genconfig_nacos 的启用旗
|
||||
(TIER2_GENCONFIG_NACOS,Nacos 接入总开关)与内网代理旁路,不另造一套连接逻辑。
|
||||
|
||||
【默认关 = 现状字节不变】
|
||||
- TIER2_KB_ROOT 未配 → 知识件外置整体关闭:setup 直接返回,不物化、不订阅,read_file 回落仓根(切片一保证)。
|
||||
- TIER2_KB_ROOT 已配但 Nacos 未启用 → 只在启动期物化一次(env 钉版 / 最新 committed),不接 Nacos 激活版本源。
|
||||
- 两者都配 → 启动期物化 + 订阅 Nacos:激活推送即重新物化。
|
||||
全程 best-effort:构造/订阅/解析/物化任一步异常都只告警、不抛、不 attach —— 治理与激活绝不拖垮 worker 启动或生成。
|
||||
|
||||
【缺文件显式失败沿用】
|
||||
物化仍走 kb_store.materialize_active:激活版取不到 / manifest 列了但缺文件 / 指纹不匹配都【显式失败】,此时 kb_root
|
||||
未被触碰 = 保留上一份已物化包,agent 仍读完整旧包。本模块把该显式失败在物化处 catch 成告警(best-effort 不咬生成),
|
||||
但不削弱 materialize_active 内部的显式失败语义(它照抛,只是被这里的 best-effort 边界接住)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
# genconfig_nacos 顶层零重依赖(不牵 agentscope / nacos),安全直接 import:复用其启用旗 + 内网代理旁路。
|
||||
try:
|
||||
from . import genconfig_nacos # 包内相对导入(worker.kb_nacos)
|
||||
from . import kb_store
|
||||
except ImportError: # pragma: no cover —— PYTHONPATH=gen-worker 直跑时的绝对导入兜底
|
||||
from worker import genconfig_nacos # type: ignore
|
||||
from worker import kb_store # type: ignore
|
||||
|
||||
|
||||
# 知识包激活版本承载 dataId(与 game-cloud KNOWLEDGE 档 gen-hot-params-{tier} 模板产物逐字对齐)。
|
||||
# env TIER2_KB_NACOS_DATAID 显式覆盖(演示脚本指演示专用 dataId,不碰生产)。
|
||||
_DEFAULT_KB_DATAID = "gen-hot-params-knowledge"
|
||||
_KB_DATAID_ENV = "TIER2_KB_NACOS_DATAID"
|
||||
|
||||
# 下发到 dataId 的复合键(与 game-cloud KnowledgeConfigKeys.NACOS_KEY_* 逐字对齐;doc↔code 契约点)。
|
||||
_KEY_PACKAGE_ID = "knowledge.packageId"
|
||||
_KEY_ACTIVE_VERSION = "knowledge.activeVersion"
|
||||
|
||||
|
||||
class KbActiveVersionSource:
|
||||
"""知识包激活版本源(K3):订阅 Nacos 知识包 dataId、热读当前激活版本、变更即触发重新物化。
|
||||
|
||||
对 kb_store.resolve_active_version 暴露 get(package_id)->version|None 契约(仅当 dataId 里的 packageId 与请求
|
||||
一致才返版本,否则返 None 交回落);对 Nacos 侧订阅长轮询推送,激活版本变更即调 kb_store.materialize_active 把
|
||||
知识根物化到新版(best-effort:物化异常保留上一份已物化包,不抛)。
|
||||
|
||||
刻意不复用 nacos_hotconfig.NacosHotConfig:后者是通用只读视图(get 键值),本源要在变更时额外触发物化、且按
|
||||
packageId 匹配,语义不同;但订阅/回调/JSON 解析的骨架照 NacosHotConfig 同款(v1 watcher 后台线程 + 线程安全 +
|
||||
best-effort),行为一致、无口径漂移。
|
||||
"""
|
||||
|
||||
def __init__(self, *, client: Any, data_id: str, group: str,
|
||||
store: Any, kb_root: Path, package_id: str) -> None:
|
||||
"""注入已建好的 v1 nacos.NacosClient + 知识包 dataId + kb_store(物化用) + 知识根 + 目标包 id。
|
||||
|
||||
client 由工厂建并负责代理旁路(建 client 前把 Nacos host 塞进 NO_PROXY);本类只消费。
|
||||
"""
|
||||
self._client = client
|
||||
self._data_id = data_id
|
||||
self._group = group
|
||||
self._store = store
|
||||
self._kb_root = kb_root
|
||||
self._package_id = package_id
|
||||
self._lock = threading.RLock()
|
||||
self._package: Optional[str] = None # 当前 dataId 里的 packageId
|
||||
self._version: Optional[str] = None # 当前激活版本
|
||||
|
||||
def start(self) -> None:
|
||||
"""拉初值(populate 版本,不在初读时物化——启动物化由工厂显式做一次,避免双物化)+ 订阅推送(变更即物化)。"""
|
||||
self._load_once()
|
||||
try:
|
||||
# v1 add_config_watcher(data_id, group, cb):第 3 位是回调,内部自起后台守护轮询线程,变更即回调。
|
||||
self._client.add_config_watcher(self._data_id, self._group, self._on_change)
|
||||
print(f"[kb-nacos] 已订阅知识包激活版本 dataId={self._data_id} group={self._group}", flush=True)
|
||||
except Exception as e: # noqa: BLE001 —— 订阅失败:留初值、不热更,但绝不炸(best-effort)
|
||||
print(f"[kb-nacos] 订阅失败(留初值、不热更,best-effort):{type(e).__name__}: {e}", flush=True)
|
||||
|
||||
def _load_once(self) -> None:
|
||||
"""经 get_config 拉一次初值(内建三级兜底);失败则版本停在 None、交 resolve 回落。初读不物化。"""
|
||||
try:
|
||||
content = self._client.get_config(self._data_id, self._group)
|
||||
self._apply(content, materialize=False)
|
||||
print(f"[kb-nacos] 初读完成 dataId={self._data_id} package={self._package} version={self._version}",
|
||||
flush=True)
|
||||
except Exception as e: # noqa: BLE001 —— Nacos 连兜底都没有:版本留 None,resolve 回落 env/最新
|
||||
print(f"[kb-nacos] 初读失败(版本留空、resolve 回落 env/最新,best-effort):"
|
||||
f"{type(e).__name__}: {e}", flush=True)
|
||||
|
||||
def _on_change(self, event: Any) -> None:
|
||||
"""v1 watcher 回调(后台 ThreadPool 线程):event 是 dict,取 content 解析;版本变了就重新物化。"""
|
||||
content = event.get("content") if isinstance(event, dict) else event
|
||||
self._apply(content, materialize=True)
|
||||
|
||||
def _apply(self, content: Any, *, materialize: bool) -> None:
|
||||
"""解析 dataId 内容(area.key 扁平 JSON),更新缓存版本;materialize=True 且版本变了则触发重新物化。
|
||||
|
||||
best-effort:空内容 / 脏 JSON / 非对象都保留旧值、不抛。
|
||||
"""
|
||||
if not content:
|
||||
return # 空内容(get_config 三级全空返 None):dataId 尚无激活版,保留旧值(resolve 回落)
|
||||
try:
|
||||
parsed = json.loads(content)
|
||||
except Exception as e: # noqa: BLE001 —— 脏内容不覆盖当前值,不抛
|
||||
print(f"[kb-nacos] 解析激活版本内容失败(保留旧值,best-effort):{type(e).__name__}: {e}", flush=True)
|
||||
return
|
||||
if not isinstance(parsed, dict):
|
||||
print(f"[kb-nacos] 忽略非对象激活版本内容(保留旧值):{type(parsed).__name__}", flush=True)
|
||||
return
|
||||
pkg = parsed.get(_KEY_PACKAGE_ID)
|
||||
ver = parsed.get(_KEY_ACTIVE_VERSION)
|
||||
pkg = str(pkg).strip() if pkg and str(pkg).strip() else None
|
||||
ver = str(ver).strip() if ver and str(ver).strip() else None
|
||||
changed = False
|
||||
with self._lock:
|
||||
if pkg != self._package or ver != self._version:
|
||||
self._package, self._version = pkg, ver
|
||||
changed = True
|
||||
if changed:
|
||||
print(f"[kb-nacos] 知识包激活版本更新:package={pkg} version={ver}", flush=True)
|
||||
# 变更 + 有版本 + 属本包(或 dataId 未标包 id)才物化;非本包的推送不误触发本 worker 物化。
|
||||
if materialize and changed and ver and (pkg is None or pkg == self._package_id):
|
||||
self._materialize(ver)
|
||||
|
||||
def _materialize(self, version: str) -> None:
|
||||
"""据激活版本把知识根物化到新版(显式传 version,不回查自身,避免 resolve 再绕一圈)。
|
||||
|
||||
best-effort 边界:materialize_active 内部对缺文件/取不到/指纹不符仍【显式失败】抛
|
||||
KnowledgeMaterializeError(kb_root 保留上一份),这里 catch 成告警——不削弱其显式失败语义,只保证
|
||||
「激活版物化失败绝不拖垮生成」(agent 继续读上一份完整包 / 仓根)。
|
||||
"""
|
||||
try:
|
||||
res = kb_store.materialize_active(
|
||||
self._store, self._kb_root, package_id=self._package_id, active_version=version)
|
||||
print(f"[kb-nacos] 激活推送触发物化: {res}", flush=True)
|
||||
except Exception as e: # noqa: BLE001 —— 含 KnowledgeMaterializeError:保留上一份,不抛(不咬生成)
|
||||
print(f"[kb-nacos] ⚠ 激活版本物化失败(保留上一份已物化包/仓根,best-effort):"
|
||||
f"{type(e).__name__}: {e}", flush=True)
|
||||
|
||||
def get(self, package_id: str) -> Optional[str]:
|
||||
"""供 kb_store.resolve_active_version 查:返回当前激活版本;dataId 里的包 id 与请求不一致则返 None(交回落)。"""
|
||||
with self._lock:
|
||||
if self._package is not None and package_id != self._package:
|
||||
return None # dataId 标了别的包 → 不冒充本包激活版
|
||||
return self._version
|
||||
|
||||
|
||||
def _resolve_data_id() -> str:
|
||||
"""知识包 dataId:env TIER2_KB_NACOS_DATAID 显式覆盖 > 默认 gen-hot-params-knowledge。"""
|
||||
override = os.environ.get(_KB_DATAID_ENV, "").strip()
|
||||
return override or _DEFAULT_KB_DATAID
|
||||
|
||||
|
||||
def build_and_attach_active_version_source(store: Any, kb_root: Path, *,
|
||||
package_id: Optional[str] = None) -> Optional[KbActiveVersionSource]:
|
||||
"""worker 启动期:若 Nacos 已启用(复用 genconfig_nacos 总开关),构造知识包激活版本源、start()、attach 进 kb_store。
|
||||
|
||||
Returns:
|
||||
已 attach 的 KbActiveVersionSource;Nacos 未启用(默认)或构造失败 → None(resolve 回落 env/最新 committed)。
|
||||
全程 best-effort:任何异常只告警、不抛、不 attach。
|
||||
"""
|
||||
package_id = package_id or kb_store.DEFAULT_PACKAGE_ID
|
||||
if not genconfig_nacos.is_enabled():
|
||||
# 默认关:不接 Nacos,resolve_active_version 走切片一链(env / 最新 committed)。
|
||||
return None
|
||||
|
||||
nacos_addr = os.environ.get("NACOS_SERVER_ADDR", "100.64.0.8:8848")
|
||||
namespace = os.environ.get("NACOS_NAMESPACE", "") # 默认公共空间 = 空串(勿写 public;前置任务口径)
|
||||
username = os.environ.get("NACOS_USERNAME", "nacos")
|
||||
password = os.environ.get("NACOS_PASSWORD", "")
|
||||
group = os.environ.get("TIER2_GENCONFIG_NACOS_GROUP", "DEFAULT_GROUP")
|
||||
data_id = _resolve_data_id()
|
||||
|
||||
# 内网铁律:必须在【建 client 前】把 Nacos host 塞进 NO_PROXY(复用 genconfig_nacos 的旁路,不另写一份)。
|
||||
genconfig_nacos._bypass_proxy_for(nacos_addr.split(":")[0])
|
||||
|
||||
try:
|
||||
import nacos # noqa: PLC0415 —— 惰性 import:默认关时不牵 nacos 依赖
|
||||
client = nacos.NacosClient(nacos_addr, namespace=namespace, username=username, password=password)
|
||||
src = KbActiveVersionSource(client=client, data_id=data_id, group=group,
|
||||
store=store, kb_root=kb_root, package_id=package_id)
|
||||
src.start() # 初读 populate 版本(不物化;启动物化由 setup 显式做一次)+ 订阅(变更即物化)
|
||||
kb_store.attach_active_version_source(src)
|
||||
print(f"[kb-nacos] 已接知识包激活版本源:dataId={data_id} group={group} "
|
||||
f"namespace={namespace!r} package={package_id} nacos={nacos_addr}", flush=True)
|
||||
return src
|
||||
except Exception as e: # noqa: BLE001 —— best-effort:接线失败不 attach、不抛,resolve 回落 env/最新
|
||||
print(f"[kb-nacos] 接知识包激活版本源失败(resolve 回落 env/最新 committed,best-effort):"
|
||||
f"{type(e).__name__}: {e}", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
def setup_knowledge_activation() -> Optional[KbActiveVersionSource]:
|
||||
"""worker(便宜档 Service / tier2)启动期一键接线知识包激活生效链(K3),与 genconfig 热源接线对称。
|
||||
|
||||
做三件事(全 best-effort,任何异常只告警、绝不阻断 worker 启动 / 生成):
|
||||
1. TIER2_KB_ROOT 未配 → 知识件外置关闭,直接返回 None(默认关、read_file 字节不变);
|
||||
2. 已配 → 先(Nacos 启用时)attach 激活版本源,让启动物化就能读到治理激活版;
|
||||
3. 启动期物化一次:接了源就物化 Nacos 激活版,否则物化 env 钉版 / 最新 committed。
|
||||
|
||||
cheap_service_app.main / tier2 service.app.main 在起 uvicorn 前调它即可。
|
||||
"""
|
||||
try:
|
||||
kb_root = kb_store.kb_root_from_env()
|
||||
if kb_root is None:
|
||||
# 默认关:知识件外置未开,不物化、不订阅(现状字节不变)。
|
||||
return None
|
||||
store = kb_store.default_kb_store()
|
||||
# 先 attach Nacos 激活版本源(若启用):下面这次启动物化就能读到治理激活版(经 resolve → 源)。
|
||||
src = build_and_attach_active_version_source(store, kb_root)
|
||||
# 启动期物化一次(源已 attach → 读 Nacos 激活版;否则 env 钉版 / 最新 committed)。
|
||||
try:
|
||||
res = kb_store.materialize_active(store, kb_root)
|
||||
print(f"[kb-nacos] 启动期物化知识根: {res}", flush=True)
|
||||
except Exception as e: # noqa: BLE001 —— 含 KnowledgeMaterializeError:保留上一份/仓根,不阻断启动
|
||||
print(f"[kb-nacos] ⚠ 启动期物化失败(保留上一份已物化包/回落仓根,best-effort):"
|
||||
f"{type(e).__name__}: {e}", flush=True)
|
||||
return src
|
||||
except Exception as e: # noqa: BLE001 —— 整条接线异常也不阻断 worker 启动
|
||||
print(f"[kb-nacos] ⚠ 知识包激活接线异常(跳过,best-effort):{type(e).__name__}: {e}", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
def detach() -> None:
|
||||
"""卸载知识包激活版本源(K3 回退开关的显式入口;转调 kb_store.detach_active_version_source)。"""
|
||||
kb_store.detach_active_version_source()
|
||||
@ -43,7 +43,7 @@ import os
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
# 纯内容寻址工具复用 store.py 的单一实现(整包指纹 / 版本 id 派生 / 相对路径安全校验)——不另抄一份,
|
||||
# 保证知识包与源工程用同一套「同内容 → 同指纹」「改源 → 新 versionId」语义。store.py 顶层只 import 标准库,
|
||||
@ -627,17 +627,56 @@ def _minio_reader(client, bucket: str, prefix: str):
|
||||
return _read
|
||||
|
||||
|
||||
# ── 当前激活版本解析(切片一简版;K3 的接缝点)────────────────────────────────────────────
|
||||
# ── 当前激活版本解析 + 治理激活版本源(K3 接缝点落地)──────────────────────────────────────
|
||||
# 进程级单例槽:worker 启动期由 kb_nacos.build_and_attach_active_version_source() 注入一个「激活版本源」
|
||||
# (读 Nacos 下发的当前激活知识包版本);resolve_active_version 取值链最前端先查它。默认 None(未 attach)=
|
||||
# 切片一行为:env 钉版 > 最新 committed(字节不变)。attach/detach 对称于 genconfig.attach_hot_source,
|
||||
# 放本模块(纯 dict 槽、不牵 nacos 依赖,cheap venv 可 import);真正建 client + 订阅 + 变更即物化在 kb_nacos。
|
||||
_active_version_source: Any = None
|
||||
|
||||
|
||||
def attach_active_version_source(source: Any) -> None:
|
||||
"""worker 启动期注入知识包激活版本源(K3);此后 resolve_active_version 最先查它。
|
||||
|
||||
source 只需满足 get(package_id)->version|None 契约(kb_nacos.KbActiveVersionSource 即是),本模块不依赖其
|
||||
内部实现——故单测可注入任意 fake 源。幂等可重入:重复 attach 覆盖为最新源。
|
||||
"""
|
||||
global _active_version_source
|
||||
_active_version_source = source
|
||||
|
||||
|
||||
def detach_active_version_source() -> None:
|
||||
"""卸载激活版本源,resolve_active_version 回落 env/最新 committed(K3 回退开关的进程内落点)。"""
|
||||
global _active_version_source
|
||||
_active_version_source = None
|
||||
|
||||
|
||||
def resolve_active_version(store: KnowledgePackageStore, *,
|
||||
package_id: str = DEFAULT_PACKAGE_ID) -> Optional[str]:
|
||||
"""解析「当前激活的知识包版本」:env TIER2_KB_ACTIVE_VERSION 显式钉版 > 最新 committed 版本。
|
||||
"""解析「当前激活的知识包版本」:治理激活版本源(Nacos 下发) > env TIER2_KB_ACTIVE_VERSION 钉版 > 最新 committed。
|
||||
|
||||
这是 W-CFG-KB 治理面(K3)的接缝点:切片一用 env / 最新版这条简单来源;后续 K3 把它换成「读 yudao 配置集
|
||||
当前激活的知识包版本」,并由激活推送触发 materialize_active。返回 None = 无激活版(物化 no-op,读回落仓根)。
|
||||
W-CFG-KB 治理面(K3)已把接缝落地:worker 启动期若接了知识包配置集的 Nacos 激活版本源(kb_nacos),这里最先查它
|
||||
——改一版知识件经配置控制台定版 + 激活,激活推送到 Nacos,worker 读到新版本号即据它物化。未接源(默认 / TIER2_KB_ROOT
|
||||
未配 / Nacos 未启用)则回落切片一行为:env 钉版 > 最新 committed(字节不变)。
|
||||
|
||||
治理源取值 best-effort:源不可达 / 无此包激活版 / 取值异常都静默回落 env/最新 committed,绝不让治理源拖垮物化
|
||||
(→拖垮生成)。返回 None = 无激活版(物化 no-op,read_file 回落仓根)。
|
||||
"""
|
||||
# ⓪ 治理激活版本源(K3 最高优先级):Nacos 下发的当前激活知识包版本。
|
||||
src = _active_version_source
|
||||
if src is not None:
|
||||
try:
|
||||
gov = src.get(package_id)
|
||||
if gov and str(gov).strip():
|
||||
return str(gov).strip()
|
||||
except Exception as e: # noqa: BLE001 —— 治理源异常绝不连累物化:静默回落 env/最新 committed
|
||||
print(f"[kb-store] ⚠ 读知识包激活版本源失败(回落 env/最新 committed): "
|
||||
f"{type(e).__name__}: {e}", flush=True)
|
||||
# ① env 显式钉版。
|
||||
pinned = os.environ.get(KB_ACTIVE_VERSION_ENV)
|
||||
if pinned and pinned.strip():
|
||||
return pinned.strip()
|
||||
# ② 最新 committed 版本。
|
||||
return store.latest_committed_version(package_id=package_id)
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user