已合并
feat:删除旧的三层匹配策略 #469
feat:删除旧的三层匹配策略 #469
已合并
王欣创建于 8月30日
共 30 个文件变更+62-6996
@@ -5,7 +5,7 @@
5| 目录 | 说明 | 默认端口 |5| 目录 | 说明 | 默认端口 |
6|------|------|----------|6|------|------|----------|
7| `identity_center/` | 认证中心(OAuth2 / JWT、用户与组织) | `8770` |7| `identity_center/` | 认证中心(OAuth2 / JWT、用户与组织) | `8770` |
8-| `manager_server/` | 管理 API(实例、模板、策略等)+ Manager WebSocket | REST `8765`、WS `8766` |8+| `manager_server/` | 管理 API(实例、模板等)+ Manager WebSocket | REST `8765`、WS `8766` |
9| `manager_web/` | 管理面 Web 前端(React + Vite) | `5273` |9| `manager_web/` | 管理面 Web 前端(React + Vite) | `5273` |
10 10 
11本地开发时,**先启动身份中心,再启动管理 API,最后启动前端**。11本地开发时,**先启动身份中心,再启动管理 API,最后启动前端**。
@@ -107,7 +107,7 @@ npm run dev
107 107 
108| 用户名 | 密码 | 角色 | 用途 |108| 用户名 | 密码 | 角色 | 用途 |
109|--------|------|------|------|109|--------|------|------|------|
110-| `admin` | `admin` | 管理员 | 登录管理面(`/manager`),可进行实例、模板、策略等配置 |110+| `admin` | `admin` | 管理员 | 登录管理面(`/manager`),可进行实例、模板等配置 |
111| `user1` | `user1` | 普通用户 | 登录用户端(`/user` 跳转至同源 `/chat/`),无管理权限 |111| `user1` | `user1` | 普通用户 | 登录用户端(`/user` 跳转至同源 `/chat/`),无管理权限 |
112 112 
113Vite 开发服务器已配置代理,`/api` → `8765`、`/idp` → `8770`(会自动去掉 `/idp` 前缀)。113Vite 开发服务器已配置代理,`/api` → `8765`、`/idp` → `8770`(会自动去掉 `/idp` 前缀)。
@@ -1,403 +0,0 @@
1-#!/usr/bin/env python3
2-# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
3-"""验证 Gateway Runtime Management 按路由上下文加载正确的 ``service_config`` 模板。
4- 
5-与 ``RuntimeManagementAgentClient.send_request`` / ``send_request_stream`` 使用同一套
6-``load_effective_enterprise_config(..., slots=[service_config])`` 读库逻辑(不依赖 K8s 池)。
7-加载 ``service_config`` 槽位时,还会解析命中策略上的 ``service_id`` / ``agent_id`` /
8-``workspace_dir`` 模板(如 ``${group_id}::${bot_id}``、``${user_id}``),与 Runtime 转发前
9-写入 ``AgentRequest`` 的行为一致。
10- 
11-前置:目标实例已创建;已用 ``enterprise_config_demo_data_config.py`` 写入演示策略
12-(含 Agent 策略 ``workspace_dir``)。数据库连接与 Gateway 进程一致,由
13-``manager_ws_client.infrastructure.db.Database`` 按 ``GATEWAY_*`` / ``.env`` 解析
14-(支持 sqlite / mysql),脚本侧无需再传数据目录。
15- 
16-典型用法(PowerShell,项目根目录;请在仓库根 ``.env`` 中配置与 Gateway 相同的 ``GATEWAY_*``)::
17- 
18- # 单场景:alice → S1 销售组 AgentServer 池(2.5.1)+ workspace_dir=alice
19- uv run python applications/manager/manager_server/scripts/enterprise_runtime_service_config.py \\
20- --group-id g_demo_sales --bot-id bot_main --user-id alice b26bc496-dfee-488b-a2ab-8bae8ce94985
21- 
22- # 一次跑完文档 3.2.1–3.2.3 三个演示场景
23- uv run python applications/manager/manager_server/scripts/enterprise_runtime_service_config.py \\
24- --provision-json provision.json --all-scenarios
25- 
26-``--provision-json`` 可自动读取 ``jiuwenclaw_id``。
27-"""
28- 
29-from __future__ import annotations
30- 
31-import argparse
32-import asyncio
33-import hashlib
34-import json
35-import logging
36-import sys
37-from dataclasses import dataclass
38-from pathlib import Path
39-from typing import Any
40- 
41-logger = logging.getLogger(__name__)
42- 
43- 
44-@dataclass(frozen=True)
45-class ServiceConfigExpectation:
46- label: str
47- template_name: str
48- min_idle_services: int
49- max_services: int
50- policy_source: str
51- service_id: str | None
52- agent_id: str | None
53- workspace_dir: str | None
54- 
55- 
56-_DEMO_SCENARIOS: dict[tuple[str, str], ServiceConfigExpectation] = {
57- ("g_demo_sales", "alice"): ServiceConfigExpectation(
58- label="S1 销售组 AgentServer 池",
59- template_name="销售组 AgentServer 池",
60- min_idle_services=2,
61- max_services=10,
62- policy_source="2.5.1 / 2.6.1",
63- service_id="g_demo_sales::bot_main",
64- agent_id="alice",
65- workspace_dir="alice",
66- ),
67- ("g_demo_sales", "bob"): ServiceConfigExpectation(
68- label="S1 销售组 AgentServer 池",
69- template_name="销售组 AgentServer 池",
70- min_idle_services=2,
71- max_services=10,
72- policy_source="继承 2.5.1 / 2.6.2",
73- service_id="g_demo_sales::bot_main",
74- agent_id="default_agent_id_1",
75- workspace_dir="g_demo_sales::bot_main::bob",
76- ),
77- ("g_unknown", "bob"): ServiceConfigExpectation(
78- label="S2 全局兜底 AgentServer 池",
79- template_name="全局兜底 AgentServer 池",
80- min_idle_services=1,
81- max_services=5,
82- policy_source="2.7 全局兜底",
83- service_id=None,
84- agent_id=None,
85- workspace_dir=None,
86- ),
87-}
88- 
89-_ALL_SCENARIO_KEYS = (
90- ("g_demo_sales", "bot_main", "alice"),
91- ("g_demo_sales", "bot_main", "bob"),
92- ("g_unknown", "bot_main", "bob"),
93-)
94- 
95- 
96-def _configure_cli_logging() -> None:
97- root = logging.getLogger()
98- root.handlers.clear()
99- root.setLevel(logging.INFO)
100- fmt = logging.Formatter("%(message)s")
101- out = logging.StreamHandler(sys.stdout)
102- out.setLevel(logging.INFO)
103- out.setFormatter(fmt)
104- err = logging.StreamHandler(sys.stderr)
105- err.setLevel(logging.ERROR)
106- err.setFormatter(fmt)
107- root.addHandler(out)
108- root.addHandler(err)
109- 
110- 
111-def _load_jiuwenclaw_id_from_provision(path: Path) -> str:
112- raw = json.loads(path.read_text(encoding="utf-8"))
113- data = raw.get("data", raw)
114- if not isinstance(data, dict):
115- raise ValueError(f"无法在 {path} 中找到 data 对象")
116- jiuwenclaw_id = str(data.get("jiuwenclaw_id") or "").strip()
117- if not jiuwenclaw_id:
118- raise ValueError(f"无法在 {path} 中找到 data.jiuwenclaw_id")
119- return jiuwenclaw_id
120- 
121- 
122-def _bootstrap_modules() -> tuple[Any, Any, Any, Any, Any]:
123- from jiuwenclaw.infrastructure.module_importer import (
124- ensure_manager_ws_client_package,
125- import_manager_ws_client_module,
126- )
127- 
128- ext_root = ensure_manager_ws_client_package()
129- loader_mod = import_manager_ws_client_module("core.enterprise_config.loader")
130- schemas_mod = import_manager_ws_client_module("core.enterprise_config.schemas")
131- utils_mod = import_manager_ws_client_module("infrastructure.utils")
132- db_mod = import_manager_ws_client_module("infrastructure.db")
133- gateway_db_mod = import_manager_ws_client_module("core.enterprise_config.gateway_db")
134- database = db_mod.Database(relative_root=ext_root)
135- return (
136- loader_mod.load_effective_enterprise_config,
137- schemas_mod.TemplateRefSlot.SERVICE_CONFIG,
138- utils_mod.set_jiuwenclaw_id,
139- database,
140- gateway_db_mod,
141- )
142- 
143- 
144-async def _ensure_gateway_database(database: Any) -> None:
145- """与 Gateway / enterprise_config 读库相同:经 ``Database.ensure_ready`` 连接。"""
146- await database.ensure_ready(log_prefix="enterprise_runtime_verify")
147- summary = database.config_summary()
148- logger.info("[db] gateway database ready: %s", summary)
149- if summary.get("db_type") == "sqlite":
150- sqlite_path = Path(str(summary.get("sqlite_path", "")))
151- if not sqlite_path.is_file():
152- logger.warning("[db] sqlite 文件不存在: %s", sqlite_path)
153- 
154- 
155-async def _close_gateway_databases(database: Any, gateway_db_mod: Any) -> None:
156- """关闭脚本与 ``enterprise_config.gateway_db`` 各自持有的连接。"""
157- enterprise_db = getattr(gateway_db_mod, "_db", None)
158- for label, db in (
159- ("enterprise_runtime_verify", database),
160- ("enterprise_config", enterprise_db),
161- ):
162- if db is None:
163- continue
164- try:
165- await db.close()
166- except Exception as exc:
167- logger.warning("[db] %s disconnect error: %s", label, exc)
168- 
169- 
170-class _RoutingRequest:
171- def __init__(
172- self,
173- *,
174- group_id: str,
175- bot_id: str,
176- user_id: str,
177- ) -> None:
178- self.params = {
179- "group_id": group_id,
180- "bot_id": bot_id,
181- "user_id": user_id,
182- }
183- 
184- 
185-def _log_expectation(group_id: str, user_id: str) -> ServiceConfigExpectation | None:
186- expect = _DEMO_SCENARIOS.get((group_id, user_id))
187- if expect is None:
188- expect = _DEMO_SCENARIOS.get((group_id, "bob"))
189- if expect is None:
190- logger.warning("[expect] 未找到演示 seed 对照表(group_id=%s user_id=%s)", group_id, user_id)
191- return None
192- workspace_key = (
193- hashlib.md5(expect.workspace_dir.encode("utf-8")).hexdigest()
194- if expect.workspace_dir
195- else None
196- )
197- logger.info(
198- "[expect] service_config=%s (%s); template_name=%r; min_idle=%s max_services=%s; "
199- "service_id=%r agent_id=%r workspace_dir=%r workspace_key=%s path=workspace_%s/agent/…",
200- expect.label,
201- expect.policy_source,
202- expect.template_name,
203- expect.min_idle_services,
204- expect.max_services,
205- expect.service_id,
206- expect.agent_id,
207- expect.workspace_dir,
208- workspace_key,
209- workspace_key or "?",
210- )
211- return expect
212- 
213- 
214-def _entity_matches(entity: dict[str, Any], expect: ServiceConfigExpectation) -> list[str]:
215- errors: list[str] = []
216- name = str(entity.get("template_name") or "")
217- if name != expect.template_name:
218- errors.append(f"template_name={name!r} 预期 {expect.template_name!r}")
219- for field in ("min_idle_services", "max_services"):
220- actual = entity.get(field)
221- expected = getattr(expect, field)
222- try:
223- if int(actual) != int(expected):
224- errors.append(f"{field}={actual} 预期 {expected}")
225- except (TypeError, ValueError):
226- errors.append(f"{field}={actual!r} 无法解析为整数")
227- return errors
228- 
229- 
230-def _policy_ids_match(loaded: Any, expect: ServiceConfigExpectation) -> list[str]:
231- errors: list[str] = []
232- if loaded.service_id != expect.service_id:
233- errors.append(
234- f"service_id={loaded.service_id!r} 预期 {expect.service_id!r}"
235- )
236- if loaded.agent_id != expect.agent_id:
237- errors.append(
238- f"agent_id={loaded.agent_id!r} 预期 {expect.agent_id!r}"
239- )
240- actual_ws = getattr(loaded, "workspace_dir", None)
241- if actual_ws != expect.workspace_dir:
242- errors.append(
243- f"workspace_dir={actual_ws!r} 预期 {expect.workspace_dir!r}"
244- )
245- return errors
246- 
247- 
248-def _workspace_key(workspace_dir: str | None) -> str | None:
249- if not workspace_dir:
250- return None
251- return hashlib.md5(str(workspace_dir).encode("utf-8")).hexdigest()
252- 
253- 
254-async def _verify_one(
255- *,
256- jiuwenclaw_id: str,
257- group_id: str,
258- bot_id: str,
259- user_id: str,
260- load_fn: Any,
261- service_config_slot: Any,
262-) -> int:
263- expect = _log_expectation(group_id, user_id)
264- logger.info(
265- "[load] jiuwenclaw_id=%s group_id=%s bot_id=%s user_id=%s",
266- jiuwenclaw_id,
267- group_id,
268- bot_id,
269- user_id,
270- )
271- 
272- request = _RoutingRequest(group_id=group_id, bot_id=bot_id, user_id=user_id)
273- loaded = await load_fn(request, [service_config_slot])
274- if loaded is None:
275- logger.error("[fail] 未加载到 service_config(策略未命中或模板不存在)")
276- return 1
277- 
278- entities = loaded.service_config or []
279- if not entities:
280- logger.error("[fail] service_config 为空 template_ref=%s", loaded.template_ref)
281- return 1
282- 
283- entity = entities[0]
284- ws_dir = getattr(loaded, "workspace_dir", None)
285- ws_key = _workspace_key(ws_dir)
286- logger.info(
287- "[loaded] template_id=%s template_name=%s min_idle_services=%s max_services=%s "
288- "service_policy_id=%s global_policy_id=%s service_id=%s agent_id=%s "
289- "workspace_dir=%s workspace_key=%s path=workspace_%s/agent/…",
290- entity.get("template_id"),
291- entity.get("template_name"),
292- entity.get("min_idle_services"),
293- entity.get("max_services"),
294- loaded.service_policy_id,
295- loaded.global_policy_id,
296- loaded.service_id,
297- loaded.agent_id,
298- ws_dir,
299- ws_key,
300- ws_key or "?",
301- )
302- 
303- if expect is None:
304- logger.info("[ok] 已加载 service_config(无演示对照表,跳过字段断言)")
305- return 0
306- 
307- errors = _entity_matches(entity, expect)
308- errors.extend(_policy_ids_match(loaded, expect))
309- if errors:
310- for err in errors:
311- logger.error("[fail] %s", err)
312- return 1
313- 
314- logger.info("[ok] service_config 与演示 seed 预期一致(%s)", expect.label)
315- return 0
316- 
317- 
318-async def _run(args: argparse.Namespace) -> int:
319- jiuwenclaw_id = (args.jiuwenclaw_id or "").strip()
320- 
321- if args.provision_json is not None and not jiuwenclaw_id:
322- jiuwenclaw_id = _load_jiuwenclaw_id_from_provision(args.provision_json)
323- 
324- if not jiuwenclaw_id:
325- logger.error("缺少 jiuwenclaw_id( positional 或 --provision-json)")
326- return 1
327- 
328- load_fn, service_config_slot, set_jiuwenclaw_id, database, gateway_db_mod = (
329- _bootstrap_modules()
330- )
331- set_jiuwenclaw_id(jiuwenclaw_id)
332- await _ensure_gateway_database(database)
333- 
334- try:
335- if args.all_scenarios:
336- exit_code = 0
337- for group_id, bot_id, user_id in _ALL_SCENARIO_KEYS:
338- logger.info("")
339- logger.info("=== 场景 group_id=%s user_id=%s ===", group_id, user_id)
340- code = await _verify_one(
341- jiuwenclaw_id=jiuwenclaw_id,
342- group_id=group_id,
343- bot_id=bot_id,
344- user_id=user_id,
345- load_fn=load_fn,
346- service_config_slot=service_config_slot,
347- )
348- if code != 0:
349- exit_code = code
350- return exit_code
351- 
352- return await _verify_one(
353- jiuwenclaw_id=jiuwenclaw_id,
354- group_id=args.group_id,
355- bot_id=args.bot_id,
356- user_id=args.user_id,
357- load_fn=load_fn,
358- service_config_slot=service_config_slot,
359- )
360- finally:
361- await _close_gateway_databases(database, gateway_db_mod)
362- 
363- 
364-def _parse_args() -> argparse.Namespace:
365- p = argparse.ArgumentParser(
366- description="验证 Gateway Runtime Management 的 service_config / workspace_dir 企业策略加载",
367- )
368- p.add_argument(
369- "jiuwenclaw_id",
370- nargs="?",
371- default="",
372- help="目标实例 jiuwenclaw_id(positional,建议放在命令末尾;可与 --provision-json 二选一)",
373- )
374- p.add_argument("--group-id", default="g_demo_sales", help="企业策略 group_id")
375- p.add_argument("--bot-id", default="bot_main", help="企业策略 bot_id")
376- p.add_argument("--user-id", default="alice", help="企业策略 user_id")
377- p.add_argument(
378- "--provision-json",
379- type=Path,
380- help="含 jiuwenclaw_id 的 JSON 文件(读取 jiuwenclaw_id)",
381- )
382- p.add_argument(
383- "--all-scenarios",
384- action="store_true",
385- help="依次验证文档 3.2.1 alice / 3.2.2 bob / 3.2.3 g_unknown 三个 service_config / workspace_dir 场景",
386- )
387- return p.parse_args()
388- 
389- 
390-def main() -> int:
391- _configure_cli_logging()
392- args = _parse_args()
393- try:
394- return asyncio.run(_run(args))
395- except KeyboardInterrupt:
396- return 130
397- except Exception as exc:
398- logger.error("[failed] %s", exc)
399- return 1
400- 
401- 
402-if __name__ == "__main__":
403- raise SystemExit(main())
@@ -1,30 +0,0 @@
1-# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2-"""配置生效策略业务包。"""
3- 
4-from manager_server.core.config_effective_policy.config_default_template_mapping import (
5- ConfigDefaultTemplateMappingService,
6- push_template_mappings_sync_to_gateway,
7-)
8-from manager_server.core.config_effective_policy.config_effective_agent_policy import (
9- ConfigEffectiveAgentPolicyService,
10- push_agent_policies_sync_to_gateway,
11-)
12-from manager_server.core.config_effective_policy.config_effective_global_policy import (
13- ConfigEffectiveGlobalPolicyService,
14- push_global_policies_sync_to_gateway,
15-)
16-from manager_server.core.config_effective_policy.config_effective_service_policy import (
17- ConfigEffectiveServicePolicyService,
18- push_service_policies_sync_to_gateway,
19-)
20- 
21-__all__ = (
22- "ConfigDefaultTemplateMappingService",
23- "ConfigEffectiveAgentPolicyService",
24- "ConfigEffectiveGlobalPolicyService",
25- "ConfigEffectiveServicePolicyService",
26- "push_agent_policies_sync_to_gateway",
27- "push_global_policies_sync_to_gateway",
28- "push_service_policies_sync_to_gateway",
29- "push_template_mappings_sync_to_gateway",
30-)
@@ -1,387 +0,0 @@
1-"""作用域默认模板映射 config_default_template_mapping 业务逻辑。"""
2- 
3-from __future__ import annotations
4- 
5-from datetime import datetime
6-from typing import Any
7- 
8-from openjiuwen_runtime.foundation.db.handler import DBHandler
9- 
10-from manager_server.core.template.push_template_to_gateway import (
11- sync_gateway_templates_after_mapping_change,
12-)
13-from manager_server.infrastructure.common import (
14- DEFAULT_POLICY_ORDER_BY,
15- resolve_order_by,
16-)
17-from manager_server.infrastructure.jiuwenclaw_id import validate_jiuwenclaw_id
18-from manager_server.infrastructure.utils import (
19- iso_datetime,
20- new_uuid4,
21- strip_optional,
22- utc_now,
23-)
24-from manager_server.manager_config_push import gateway_request
25-from manager_server.models.config_effective_policy_models import (
26- CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF,
27-)
28-from manager_server.schemas.config_effective_policy_schemas import (
29- ConfigDefaultTemplateMappingCreateBody,
30- ConfigDefaultTemplateMappingListQuery,
31- ConfigDefaultTemplateMappingOut,
32- ConfigDefaultTemplateMappingUpdateBody,
33-)
34-from manager_server.schemas.template_slot_schemas import MAPPING_SCOPE_TYPES
35- 
36-_TEMPLATE_MAPPING_TABLE = CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF.table_name
37-_MAPPING_PATH = "/api/v1/config-default-template-mappings"
38-_LIST_ALL_CAP = 10_000
39-_PUSH_DROP_KEYS = frozenset({"id", "created_at", "updated_at", "jiuwenclaw_id"})
40-_ALLOWED_SORT_FIELDS = frozenset({
41- "policy_name",
42- "policy_desc",
43- "priority",
44- "scope_type",
45- "scope_id",
46- "template_type",
47- "template_id",
48- "updated_at",
49-})
50- 
51- 
52-def _clean_mapping_for_create(mapping: dict[str, Any]) -> dict[str, Any]:
53- return {k: v for k, v in mapping.items() if k not in _PUSH_DROP_KEYS}
54- 
55- 
56-def _matches_search(row: Any, query: str) -> bool:
57- needle = query.strip().lower()
58- if not needle:
59- return True
60- fields = [
61- str(getattr(row, "policy_id", "") or ""),
62- str(getattr(row, "policy_name", "") or ""),
63- str(getattr(row, "policy_desc", "") or ""),
64- str(getattr(row, "scope_type", "") or ""),
65- str(getattr(row, "scope_id", "") or ""),
66- str(getattr(row, "template_type", "") or ""),
67- str(getattr(row, "template_id", "") or ""),
68- str(getattr(row, "priority", "") or ""),
69- ]
70- return any(needle in field.lower() for field in fields)
71- 
72- 
73-async def push_template_mappings_sync_to_gateway(
74- handler: DBHandler,
75- jiuwenclaw_id: str,
76-) -> dict[str, Any]:
77- """Gateway 注册后:将 MDB 中该实例全部默认模板映射 bulk push 到 GDB。"""
78- jid = str(jiuwenclaw_id or "").strip()
79- if not jid:
80- raise ValueError("jiuwenclaw_id is required")
81- rows = await handler.list_records(
82- _TEMPLATE_MAPPING_TABLE,
83- {"jiuwenclaw_id": jid},
84- limit=_LIST_ALL_CAP,
85- offset=0,
86- )
87- mappings = [_row_to_out(row).model_dump(mode="json") for row in rows]
88- last: dict[str, Any] | None = None
89- for idx, mapping in enumerate(mappings):
90- if not isinstance(mapping, dict):
91- continue
92- last = await gateway_request(
93- jid,
94- "POST",
95- f"{_MAPPING_PATH}/",
96- _clean_mapping_for_create(mapping),
97- revision=f"sync-mapping:{idx}",
98- )
99- return last or {
100- "revision": "sync-mapping",
101- "success_flag": True,
102- "result": {"synced": 0},
103- "transport": "http",
104- }
105- 
106- 
107-def _mapping_pk(jiuwenclaw_id: str, mapping_id: int) -> dict[str, Any]:
108- return {"jiuwenclaw_id": jiuwenclaw_id, "id": mapping_id}
109- 
110- 
111-def _validate_scope(scope_type: str, scope_id: str) -> tuple[str, str]:
112- st = str(scope_type or "").strip().lower()
113- if st not in MAPPING_SCOPE_TYPES:
114- raise ValueError(
115- f"scope_type must be one of {sorted(MAPPING_SCOPE_TYPES)}, got {scope_type!r}"
116- )
117- sid = str(scope_id or "").strip()
118- if not sid:
119- raise ValueError("scope_id is required")
120- return st, sid
121- 
122- 
123-def _row_to_out(row: Any) -> ConfigDefaultTemplateMappingOut:
124- return ConfigDefaultTemplateMappingOut(
125- id=row.id,
126- jiuwenclaw_id=row.jiuwenclaw_id,
127- policy_id=row.policy_id,
128- policy_name=row.policy_name,
129- policy_desc=row.policy_desc,
130- scope_type=row.scope_type,
131- scope_id=row.scope_id,
132- priority=row.priority,
133- template_id=row.template_id,
134- template_type=row.template_type,
135- enabled=row.enabled,
136- data=row.data,
137- created_at=iso_datetime(row.created_at),
138- updated_at=iso_datetime(row.updated_at),
139- )
140- 
141- 
142-class ConfigDefaultTemplateMappingService:
143- def __init__(self, handler: DBHandler) -> None:
144- self._handler = handler
145- 
146- @staticmethod
147- def _mapping_dict_for_push(
148- row: dict[str, Any], *, now: datetime
149- ) -> dict[str, Any]:
150- """构建下发给 Gateway 的 mapping 对象(不含 id,由 Gateway 自增)。"""
151- return {
152- "jiuwenclaw_id": row["jiuwenclaw_id"],
153- "policy_id": row["policy_id"],
154- "policy_name": row.get("policy_name"),
155- "policy_desc": row.get("policy_desc"),
156- "scope_type": row.get("scope_type"),
157- "scope_id": row.get("scope_id"),
158- "priority": row["priority"],
159- "template_id": row["template_id"],
160- "template_type": row["template_type"],
161- "enabled": row.get("enabled", True),
162- "data": row.get("data"),
163- "created_at": iso_datetime(row.get("created_at") or now),
164- "updated_at": iso_datetime(row.get("updated_at") or now),
165- }
166- 
167- async def create(
168- self,
169- jiuwenclaw_id: str,
170- body: ConfigDefaultTemplateMappingCreateBody,
171- ) -> ConfigDefaultTemplateMappingOut:
172- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
173- scope_type, scope_id = _validate_scope(body.scope_type, body.scope_id)
174- 
175- now = utc_now()
176- row = {
177- "jiuwenclaw_id": normalized,
178- "policy_id": new_uuid4(),
179- "policy_name": body.policy_name,
180- "policy_desc": body.policy_desc,
181- "scope_type": scope_type,
182- "scope_id": scope_id,
183- "priority": body.priority,
184- "template_id": body.template_id,
185- "template_type": body.template_type,
186- "enabled": body.enabled,
187- "data": body.data,
188- "created_at": now,
189- "updated_at": now,
190- }
191- await sync_gateway_templates_after_mapping_change(
192- self._handler,
193- normalized,
194- old_template_type=None,
195- old_template_id=None,
196- new_template_type=body.template_type,
197- new_template_id=body.template_id,
198- skip_runtime_update=True,
199- )
200- ack = await gateway_request(
201- normalized,
202- "POST",
203- f"{_MAPPING_PATH}/",
204- _clean_mapping_for_create(self._mapping_dict_for_push(row, now=now)),
205- )
206- ack_result = ack.get("result") if isinstance(ack, dict) else None
207- mapping_id: int | None = None
208- if isinstance(ack_result, dict):
209- raw_id = ack_result.get("id")
210- if raw_id is not None:
211- mapping_id = int(raw_id)
212- if mapping_id is None or mapping_id < 1:
213- raise ValueError(
214- "gateway config_default_template_mappings.create returned no id"
215- )
216- 
217- payload = {**row, "id": mapping_id}
218- created = await self._handler.create(_TEMPLATE_MAPPING_TABLE, payload)
219- return _row_to_out(created)
220- 
221- async def get(
222- self, jiuwenclaw_id: str, mapping_id: int
223- ) -> ConfigDefaultTemplateMappingOut | None:
224- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
225- row = await self._handler.get(
226- _TEMPLATE_MAPPING_TABLE, _mapping_pk(normalized, mapping_id)
227- )
228- if row is None:
229- return None
230- return _row_to_out(row)
231- 
232- async def list_mappings(
233- self,
234- jiuwenclaw_id: str,
235- query: ConfigDefaultTemplateMappingListQuery,
236- ) -> dict[str, Any]:
237- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
238- template_type = query.template_type
239- 
240- page = max(query.page, 1)
241- page_size = min(max(query.page_size, 1), 200)
242- filters: dict[str, Any] = {"jiuwenclaw_id": normalized}
243- if query.scope_type:
244- filters["scope_type"] = query.scope_type.strip().lower()
245- if query.scope_id:
246- filters["scope_id"] = strip_optional(query.scope_id)
247- if template_type:
248- filters["template_type"] = template_type
249- if query.template_id:
250- filters["template_id"] = query.template_id.strip()
251- if query.enabled is not None:
252- filters["enabled"] = query.enabled
253- 
254- order_by = resolve_order_by(
255- query.sort_by,
256- query.sort_order,
257- allowed_sort_fields=_ALLOWED_SORT_FIELDS,
258- default_order_by=DEFAULT_POLICY_ORDER_BY,
259- )
260- search_query = (query.search or "").strip()
261- if search_query:
262- rows = await self._handler.list_records(
263- _TEMPLATE_MAPPING_TABLE,
264- filters,
265- limit=_LIST_ALL_CAP,
266- offset=0,
267- order_by=order_by,
268- )
269- items = [
270- _row_to_out(row).model_dump(mode="json")
271- for row in rows
272- if _matches_search(row, search_query)
273- ]
274- total = len(items)
275- offset = (page - 1) * page_size
276- page_items = items[offset:offset + page_size]
277- return {
278- "items": page_items,
279- "total": total,
280- "page": page,
281- "page_size": page_size,
282- }
283- 
284- offset = (page - 1) * page_size
285- rows = await self._handler.list_records(
286- _TEMPLATE_MAPPING_TABLE,
287- filters,
288- limit=page_size,
289- offset=offset,
290- order_by=order_by,
291- )
292- total = await self._handler.count_records(_TEMPLATE_MAPPING_TABLE, filters)
293- items = [_row_to_out(r).model_dump(mode="json") for r in rows]
294- return {
295- "items": items,
296- "total": total,
297- "page": page,
298- "page_size": page_size,
299- }
300- 
301- async def update(
302- self,
303- jiuwenclaw_id: str,
304- mapping_id: int,
305- body: ConfigDefaultTemplateMappingUpdateBody,
306- ) -> ConfigDefaultTemplateMappingOut | None:
307- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
308- 
309- updates = body.model_dump(exclude_unset=True)
310- if "scope_type" in updates and updates["scope_type"] is not None:
311- updates["scope_type"] = str(updates["scope_type"]).strip().lower()
312- if "scope_id" in updates and updates["scope_id"] is not None:
313- updates["scope_id"] = str(updates["scope_id"]).strip()
314- 
315- row = await self._handler.get(
316- _TEMPLATE_MAPPING_TABLE, _mapping_pk(normalized, mapping_id)
317- )
318- if row is None:
319- return None
320- 
321- merged_type = updates.get("scope_type", row.scope_type)
322- merged_id = updates.get("scope_id", row.scope_id)
323- _validate_scope(str(merged_type), str(merged_id))
324- 
325- if not updates:
326- return _row_to_out(row)
327- 
328- if "template_type" in updates or "template_id" in updates:
329- await sync_gateway_templates_after_mapping_change(
330- self._handler,
331- normalized,
332- old_template_type=str(getattr(row, "template_type", "") or ""),
333- old_template_id=str(getattr(row, "template_id", "") or ""),
334- new_template_type=str(
335- updates.get("template_type", getattr(row, "template_type", "") or "")
336- ),
337- new_template_id=str(
338- updates.get("template_id", getattr(row, "template_id", "") or "")
339- ),
340- skip_runtime_update=True,
341- )
342- await gateway_request(
343- normalized,
344- "PATCH",
345- f"{_MAPPING_PATH}/{mapping_id}",
346- dict(updates),
347- )
348- payload = dict(updates)
349- payload["updated_at"] = utc_now()
350- updated = await self._handler.update(
351- _TEMPLATE_MAPPING_TABLE,
352- _mapping_pk(normalized, mapping_id),
353- payload,
354- )
355- if updated is None:
356- return None
357- return _row_to_out(updated)
358- 
359- async def delete(
360- self,
361- jiuwenclaw_id: str,
362- mapping_id: int,
363- ) -> bool:
364- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
365- row = await self._handler.get(
366- _TEMPLATE_MAPPING_TABLE, _mapping_pk(normalized, mapping_id)
367- )
368- if row is None:
369- return False
370- await sync_gateway_templates_after_mapping_change(
371- self._handler,
372- normalized,
373- old_template_type=str(getattr(row, "template_type", "") or ""),
374- old_template_id=str(getattr(row, "template_id", "") or ""),
375- new_template_type=None,
376- new_template_id=None,
377- skip_runtime_update=True,
378- )
379- await gateway_request(
380- normalized,
381- "DELETE",
382- f"{_MAPPING_PATH}/{mapping_id}",
383- {},
384- )
385- return await self._handler.delete(
386- _TEMPLATE_MAPPING_TABLE, _mapping_pk(normalized, mapping_id)
387- )
@@ -1,446 +0,0 @@
1-"""配置生效 Agent 层级策略 config_effective_agent_policy 业务逻辑。"""
2- 
3-from __future__ import annotations
4- 
5-from datetime import datetime
6-from typing import Any
7- 
8-from openjiuwen_runtime.foundation.db.handler import DBHandler
9- 
10-from manager_server.core.template.push_template_to_gateway import (
11- sync_gateway_templates_after_template_ref_change,
12-)
13-from manager_server.infrastructure.common import (
14- DEFAULT_POLICY_ORDER_BY,
15- resolve_order_by,
16-)
17-from manager_server.infrastructure.jiuwenclaw_id import validate_jiuwenclaw_id
18-from manager_server.infrastructure.utils import (
19- iso_datetime,
20- new_uuid4,
21- utc_now,
22-)
23-from manager_server.manager_config_push import gateway_request
24-from manager_server.models.config_effective_policy_models import (
25- CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF,
26- CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF,
27-)
28-from manager_server.infrastructure.template_ref import (
29- apply_template_ref_to_updates,
30- normalize_template_ref,
31- read_template_ref_from_row,
32- validate_single_value_template_ref_slots,
33-)
34-from manager_server.schemas.config_effective_policy_schemas import (
35- ConfigEffectiveAgentPolicyCreateBody,
36- ConfigEffectiveAgentPolicyListQuery,
37- ConfigEffectiveAgentPolicyOut,
38- ConfigEffectiveAgentPolicyUpdateBody,
39-)
40- 
41-_AGENT_POLICY_TABLE = CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF.table_name
42-_SERVICE_POLICY_TABLE = CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF.table_name
43-_AGENT_POLICY_PATH = "/api/v1/config-effective/agent-policies"
44-_LIST_ALL_CAP = 10_000
45-_PUSH_DROP_KEYS = frozenset({"id", "created_at", "updated_at", "jiuwenclaw_id"})
46-_ALLOWED_SORT_FIELDS = frozenset({
47- "policy_name",
48- "policy_desc",
49- "service_policy_id",
50- "priority",
51- "match_expr",
52- "agent_id",
53- "workspace_dir",
54- "updated_at",
55-})
56- 
57- 
58-def _clean_policy_for_create(policy: dict[str, Any]) -> dict[str, Any]:
59- return {k: v for k, v in policy.items() if k not in _PUSH_DROP_KEYS}
60- 
61- 
62-def _matches_search(
63- row: Any,
64- query: str,
65- *,
66- service_policy_names: dict[str, str],
67-) -> bool:
68- needle = query.strip().lower()
69- if not needle:
70- return True
71- fields = [
72- str(getattr(row, "policy_id", "") or ""),
73- str(getattr(row, "policy_name", "") or ""),
74- str(getattr(row, "policy_desc", "") or ""),
75- str(getattr(row, "agent_id", "") or ""),
76- str(getattr(row, "workspace_dir", "") or ""),
77- str(getattr(row, "service_policy_id", "") or ""),
78- str(getattr(row, "priority", "") or ""),
79- str(getattr(row, "match_expr", "") or ""),
80- ]
81- linked_service_name = service_policy_names.get(
82- str(getattr(row, "service_policy_id", "") or ""),
83- "",
84- )
85- if linked_service_name:
86- fields.append(linked_service_name)
87- return any(needle in field.lower() for field in fields)
88- 
89- 
90-async def _service_policy_names_by_id(handler: DBHandler, jiuwenclaw_id: str) -> dict[str, str]:
91- rows = await handler.list_records(
92- _SERVICE_POLICY_TABLE,
93- {"jiuwenclaw_id": jiuwenclaw_id},
94- limit=_LIST_ALL_CAP,
95- offset=0,
96- )
97- return {
98- str(getattr(row, "policy_id", "") or ""): str(getattr(row, "policy_name", "") or "")
99- for row in rows
100- }
101- 
102- 
103-async def push_agent_policies_sync_to_gateway(
104- handler: DBHandler,
105- jiuwenclaw_id: str,
106-) -> dict[str, Any]:
107- """Gateway 注册后:将 MDB 中该实例全部 Agent 策略 bulk push 到 GDB。"""
108- jid = str(jiuwenclaw_id or "").strip()
109- if not jid:
110- raise ValueError("jiuwenclaw_id is required")
111- rows = await handler.list_records(
112- _AGENT_POLICY_TABLE,
113- {"jiuwenclaw_id": jid},
114- limit=_LIST_ALL_CAP,
115- offset=0,
116- )
117- policies = [_row_to_out(row).model_dump(mode="json") for row in rows]
118- last: dict[str, Any] | None = None
119- for idx, policy in enumerate(policies):
120- if not isinstance(policy, dict):
121- continue
122- last = await gateway_request(
123- jid,
124- "POST",
125- f"{_AGENT_POLICY_PATH}/",
126- _clean_policy_for_create(policy),
127- revision=f"sync-agent-policy:{idx}",
128- )
129- return last or {
130- "revision": "sync-agent-policy",
131- "success_flag": True,
132- "result": {"synced": 0},
133- "transport": "http",
134- }
135- 
136- 
137-def _agent_policy_pk(jiuwenclaw_id: str, policy_id: int) -> dict[str, Any]:
138- return {"jiuwenclaw_id": jiuwenclaw_id, "id": policy_id}
139- 
140- 
141-def _row_to_out(row: Any) -> ConfigEffectiveAgentPolicyOut:
142- return ConfigEffectiveAgentPolicyOut(
143- id=row.id,
144- jiuwenclaw_id=row.jiuwenclaw_id,
145- policy_id=row.policy_id,
146- policy_name=row.policy_name,
147- policy_desc=row.policy_desc,
148- agent_id=row.agent_id,
149- workspace_dir=row.workspace_dir,
150- service_policy_id=row.service_policy_id,
151- priority=row.priority,
152- match_expr=row.match_expr,
153- template_ref=read_template_ref_from_row(row),
154- send_file_allowed=bool(getattr(row, "send_file_allowed", True)),
155- enabled=row.enabled,
156- data=row.data,
157- created_at=iso_datetime(row.created_at),
158- updated_at=iso_datetime(row.updated_at),
159- )
160- 
161- 
162-class ConfigEffectiveAgentPolicyService:
163- def __init__(self, handler: DBHandler) -> None:
164- self._handler = handler
165- 
166- async def _validate_parent_refs(
167- self,
168- *,
169- jiuwenclaw_id: str,
170- service_policy_id: str,
171- ) -> None:
172- normalized_id = service_policy_id.strip()
173- if not normalized_id:
174- raise ValueError("service_policy_id is required")
175- sp = await self._handler.get(
176- _SERVICE_POLICY_TABLE,
177- {"jiuwenclaw_id": jiuwenclaw_id.strip(), "policy_id": normalized_id},
178- )
179- if sp is None:
180- raise ValueError(f"unknown service_policy_id={normalized_id!r}")
181- 
182- @staticmethod
183- def _policy_dict_for_push(row: dict[str, Any], *, now: datetime) -> dict[str, Any]:
184- """构建下发给 Gateway 的 policy 对象(不含 id,由 Gateway 自增)。"""
185- return {
186- "jiuwenclaw_id": row["jiuwenclaw_id"],
187- "policy_id": row["policy_id"],
188- "policy_name": row.get("policy_name"),
189- "policy_desc": row.get("policy_desc"),
190- "agent_id": row["agent_id"],
191- "workspace_dir": row.get("workspace_dir"),
192- "service_policy_id": row["service_policy_id"],
193- "priority": row.get("priority", 0),
194- "match_expr": row.get("match_expr"),
195- "template_ref": normalize_template_ref(row.get("template_ref")),
196- "send_file_allowed": bool(row.get("send_file_allowed", True)),
197- "enabled": row.get("enabled", True),
198- "data": row.get("data"),
199- "created_at": iso_datetime(row.get("created_at") or now),
200- "updated_at": iso_datetime(row.get("updated_at") or now),
201- }
202- 
203- async def create(
204- self,
205- jiuwenclaw_id: str,
206- body: ConfigEffectiveAgentPolicyCreateBody,
207- ) -> ConfigEffectiveAgentPolicyOut:
208- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
209- await self._validate_parent_refs(
210- jiuwenclaw_id=normalized,
211- service_policy_id=body.service_policy_id,
212- )
213- 
214- now = utc_now()
215- template_ref = normalize_template_ref(body.template_ref)
216- validate_single_value_template_ref_slots(template_ref)
217- workspace_dir = body.workspace_dir.strip() if body.workspace_dir else None
218- row = {
219- "jiuwenclaw_id": normalized,
220- "policy_id": new_uuid4(),
221- "policy_name": body.policy_name,
222- "policy_desc": body.policy_desc,
223- "agent_id": body.agent_id.strip(),
224- "workspace_dir": workspace_dir,
225- "service_policy_id": body.service_policy_id.strip(),
226- "priority": body.priority,
227- "match_expr": body.match_expr,
228- "template_ref": template_ref,
229- "send_file_allowed": body.send_file_allowed,
230- "enabled": body.enabled,
231- "data": body.data,
232- "created_at": now,
233- "updated_at": now,
234- }
235- await sync_gateway_templates_after_template_ref_change(
236- self._handler,
237- normalized,
238- old_template_ref={},
239- new_template_ref=row["template_ref"],
240- skip_runtime_update=True,
241- )
242- ack = await gateway_request(
243- normalized,
244- "POST",
245- f"{_AGENT_POLICY_PATH}/",
246- _clean_policy_for_create(self._policy_dict_for_push(row, now=now)),
247- )
248- ack_result = ack.get("result") if isinstance(ack, dict) else None
249- row_id: int | None = None
250- if isinstance(ack_result, dict):
251- raw_id = ack_result.get("id")
252- if raw_id is not None:
253- row_id = int(raw_id)
254- if row_id is None or row_id < 1:
255- raise ValueError(
256- "gateway config_effective_agent_policies.create returned no id"
257- )
258- 
259- payload = {**row, "id": row_id}
260- created = await self._handler.create(_AGENT_POLICY_TABLE, payload)
261- return _row_to_out(created)
262- 
263- async def get(
264- self, jiuwenclaw_id: str, policy_id: int
265- ) -> ConfigEffectiveAgentPolicyOut | None:
266- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
267- row = await self._handler.get(
268- _AGENT_POLICY_TABLE, _agent_policy_pk(normalized, policy_id)
269- )
270- if row is None:
271- return None
272- return _row_to_out(row)
273- 
274- async def list_policies(
275- self,
276- jiuwenclaw_id: str,
277- query: ConfigEffectiveAgentPolicyListQuery,
278- ) -> dict[str, Any]:
279- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
280- page = max(query.page, 1)
281- page_size = min(max(query.page_size, 1), 200)
282- filters: dict[str, Any] = {"jiuwenclaw_id": normalized}
283- if query.service_policy_id is not None:
284- filters["service_policy_id"] = query.service_policy_id.strip()
285- if query.enabled is not None:
286- filters["enabled"] = query.enabled
287- if query.send_file_allowed is not None:
288- filters["send_file_allowed"] = query.send_file_allowed
289- 
290- order_by = resolve_order_by(
291- query.sort_by,
292- query.sort_order,
293- allowed_sort_fields=_ALLOWED_SORT_FIELDS,
294- default_order_by=DEFAULT_POLICY_ORDER_BY,
295- )
296- search_query = (query.search or "").strip()
297- 
298- service_policy_names: dict[str, str] = {}
299- if search_query:
300- service_policy_names = await _service_policy_names_by_id(
301- self._handler, normalized
302- )
303- 
304- if search_query:
305- rows = await self._handler.list_records(
306- _AGENT_POLICY_TABLE,
307- filters,
308- limit=_LIST_ALL_CAP,
309- offset=0,
310- order_by=order_by,
311- )
312- matched_rows = []
313- for row in rows:
314- if _matches_search(
315- row,
316- search_query,
317- service_policy_names=service_policy_names,
318- ):
319- matched_rows.append(row)
320- rows = matched_rows
321- total = len(rows)
322- offset = (page - 1) * page_size
323- page_rows = rows[offset:offset + page_size]
324- items = [_row_to_out(row).model_dump(mode="json") for row in page_rows]
325- return {
326- "items": items,
327- "total": total,
328- "page": page,
329- "page_size": page_size,
330- }
331- 
332- offset = (page - 1) * page_size
333- rows = await self._handler.list_records(
334- _AGENT_POLICY_TABLE,
335- filters,
336- limit=page_size,
337- offset=offset,
338- order_by=order_by,
339- )
340- total = await self._handler.count_records(_AGENT_POLICY_TABLE, filters)
341- items = [_row_to_out(r).model_dump(mode="json") for r in rows]
342- return {
343- "items": items,
344- "total": total,
345- "page": page,
346- "page_size": page_size,
347- }
348- 
349- async def update(
350- self,
351- jiuwenclaw_id: str,
352- policy_id: int,
353- body: ConfigEffectiveAgentPolicyUpdateBody,
354- ) -> ConfigEffectiveAgentPolicyOut | None:
355- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
356- 
357- updates = body.model_dump(exclude_unset=True)
358- row = await self._handler.get(
359- _AGENT_POLICY_TABLE, _agent_policy_pk(normalized, policy_id)
360- )
361- if row is None:
362- return None
363- 
364- next_jiuwenclaw_id = row.jiuwenclaw_id
365- if "agent_id" in updates and updates["agent_id"] is not None:
366- updates["agent_id"] = updates["agent_id"].strip()
367- if not updates["agent_id"]:
368- raise ValueError("agent_id cannot be empty")
369- 
370- if "workspace_dir" in updates:
371- raw_ws = updates["workspace_dir"]
372- updates["workspace_dir"] = (
373- raw_ws.strip() if isinstance(raw_ws, str) and raw_ws.strip() else None
374- )
375- 
376- if "service_policy_id" in updates and updates["service_policy_id"] is not None:
377- updates["service_policy_id"] = str(updates["service_policy_id"]).strip()
378- if not updates["service_policy_id"]:
379- raise ValueError("service_policy_id cannot be empty")
380- 
381- next_service_policy_id = updates.get("service_policy_id", row.service_policy_id)
382- if "service_policy_id" in updates:
383- await self._validate_parent_refs(
384- jiuwenclaw_id=next_jiuwenclaw_id,
385- service_policy_id=next_service_policy_id,
386- )
387- 
388- if not updates:
389- return _row_to_out(row)
390- 
391- old_template_ref = read_template_ref_from_row(row)
392- updates = apply_template_ref_to_updates(updates, existing_row=row)
393- 
394- if "template_ref" in updates:
395- validate_single_value_template_ref_slots(updates["template_ref"])
396- await sync_gateway_templates_after_template_ref_change(
397- self._handler,
398- normalized,
399- old_template_ref=old_template_ref,
400- new_template_ref=updates["template_ref"],
401- skip_runtime_update=True,
402- )
403- await gateway_request(
404- normalized,
405- "PATCH",
406- f"{_AGENT_POLICY_PATH}/{policy_id}",
407- dict(updates),
408- )
409- payload = dict(updates)
410- payload["updated_at"] = utc_now()
411- updated = await self._handler.update(
412- _AGENT_POLICY_TABLE,
413- _agent_policy_pk(normalized, policy_id),
414- payload,
415- )
416- if updated is None:
417- return None
418- return _row_to_out(updated)
419- 
420- async def delete(
421- self,
422- jiuwenclaw_id: str,
423- policy_id: int,
424- ) -> bool:
425- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
426- row = await self._handler.get(
427- _AGENT_POLICY_TABLE, _agent_policy_pk(normalized, policy_id)
428- )
429- if row is None:
430- return False
431- await sync_gateway_templates_after_template_ref_change(
432- self._handler,
433- normalized,
434- old_template_ref=read_template_ref_from_row(row),
435- new_template_ref={},
436- skip_runtime_update=True,
437- )
438- await gateway_request(
439- normalized,
440- "DELETE",
441- f"{_AGENT_POLICY_PATH}/{policy_id}",
442- {},
443- )
444- return await self._handler.delete(
445- _AGENT_POLICY_TABLE, _agent_policy_pk(normalized, policy_id)
446- )
@@ -1,329 +0,0 @@
1-"""配置生效全局兜底策略 config_effective_global_policy 业务逻辑。"""
2- 
3-from __future__ import annotations
4- 
5-from datetime import datetime
6-from typing import Any
7- 
8-from openjiuwen_runtime.foundation.db.handler import DBHandler
9- 
10-from manager_server.core.template.push_template_to_gateway import (
11- sync_gateway_templates_after_template_ref_change,
12-)
13-from manager_server.infrastructure.common import (
14- DEFAULT_POLICY_ORDER_BY,
15- resolve_order_by,
16-)
17-from manager_server.infrastructure.jiuwenclaw_id import validate_jiuwenclaw_id
18-from manager_server.infrastructure.utils import (
19- iso_datetime,
20- new_uuid4,
21- utc_now,
22-)
23-from manager_server.manager_config_push import gateway_request
24-from manager_server.models.config_effective_policy_models import (
25- CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF,
26-)
27-from manager_server.infrastructure.template_ref import (
28- apply_template_ref_to_updates,
29- normalize_template_ref,
30- read_template_ref_from_row,
31- validate_single_value_template_ref_slots,
32-)
33-from manager_server.schemas.config_effective_policy_schemas import (
34- ConfigEffectiveGlobalPolicyCreateBody,
35- ConfigEffectiveGlobalPolicyListQuery,
36- ConfigEffectiveGlobalPolicyOut,
37- ConfigEffectiveGlobalPolicyUpdateBody,
38-)
39- 
40-_GLOBAL_POLICY_TABLE = CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF.table_name
41-_GLOBAL_POLICY_PATH = "/api/v1/config-effective/global-policies"
42-_LIST_ALL_CAP = 10_000
43-_PUSH_DROP_KEYS = frozenset({"id", "created_at", "updated_at", "jiuwenclaw_id"})
44-_ALLOWED_SORT_FIELDS = frozenset({"policy_name", "policy_desc", "priority", "updated_at"})
45- 
46- 
47-def _clean_policy_for_create(policy: dict[str, Any]) -> dict[str, Any]:
48- return {k: v for k, v in policy.items() if k not in _PUSH_DROP_KEYS}
49- 
50- 
51-def _matches_search(row: Any, query: str) -> bool:
52- needle = query.strip().lower()
53- if not needle:
54- return True
55- fields = [
56- str(getattr(row, "policy_id", "") or ""),
57- str(getattr(row, "policy_name", "") or ""),
58- str(getattr(row, "policy_desc", "") or ""),
59- str(getattr(row, "priority", "") or ""),
60- ]
61- return any(needle in field.lower() for field in fields)
62- 
63- 
64-async def push_global_policies_sync_to_gateway(
65- handler: DBHandler,
66- jiuwenclaw_id: str,
67-) -> dict[str, Any]:
68- """Gateway 注册后:将 MDB 中该实例全部 Global 策略 bulk push 到 GDB。"""
69- jid = str(jiuwenclaw_id or "").strip()
70- if not jid:
71- raise ValueError("jiuwenclaw_id is required")
72- rows = await handler.list_records(
73- _GLOBAL_POLICY_TABLE,
74- {"jiuwenclaw_id": jid},
75- limit=_LIST_ALL_CAP,
76- offset=0,
77- )
78- policies = [_row_to_out(row).model_dump(mode="json") for row in rows]
79- last: dict[str, Any] | None = None
80- for idx, policy in enumerate(policies):
81- if not isinstance(policy, dict):
82- continue
83- last = await gateway_request(
84- jid,
85- "POST",
86- f"{_GLOBAL_POLICY_PATH}/",
87- _clean_policy_for_create(policy),
88- revision=f"sync-global-policy:{idx}",
89- )
90- return last or {
91- "revision": "sync-global-policy",
92- "success_flag": True,
93- "result": {"synced": 0},
94- "transport": "http",
95- }
96- 
97- 
98-def _global_policy_pk(jiuwenclaw_id: str, policy_id: int) -> dict[str, Any]:
99- return {"jiuwenclaw_id": jiuwenclaw_id, "id": policy_id}
100- 
101- 
102-def _row_to_out(row: Any) -> ConfigEffectiveGlobalPolicyOut:
103- return ConfigEffectiveGlobalPolicyOut(
104- id=row.id,
105- jiuwenclaw_id=row.jiuwenclaw_id,
106- policy_id=row.policy_id,
107- policy_name=row.policy_name,
108- policy_desc=row.policy_desc,
109- priority=row.priority,
110- template_ref=read_template_ref_from_row(row),
111- enabled=row.enabled,
112- data=row.data,
113- created_at=iso_datetime(row.created_at),
114- updated_at=iso_datetime(row.updated_at),
115- )
116- 
117- 
118-class ConfigEffectiveGlobalPolicyService:
119- def __init__(self, handler: DBHandler) -> None:
120- self._handler = handler
121- 
122- @staticmethod
123- def _policy_dict_for_push(row: dict[str, Any], *, now: datetime) -> dict[str, Any]:
124- """构建下发给 Gateway 的 policy 对象(不含 id,由 Gateway 自增)。"""
125- return {
126- "jiuwenclaw_id": row["jiuwenclaw_id"],
127- "policy_id": row["policy_id"],
128- "policy_name": row.get("policy_name"),
129- "policy_desc": row.get("policy_desc"),
130- "priority": row["priority"],
131- "template_ref": normalize_template_ref(row.get("template_ref")),
132- "enabled": row.get("enabled", True),
133- "data": row.get("data"),
134- "created_at": iso_datetime(row.get("created_at") or now),
135- "updated_at": iso_datetime(row.get("updated_at") or now),
136- }
137- 
138- async def create(
139- self,
140- jiuwenclaw_id: str,
141- body: ConfigEffectiveGlobalPolicyCreateBody,
142- ) -> ConfigEffectiveGlobalPolicyOut:
143- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
144- 
145- now = utc_now()
146- template_ref = normalize_template_ref(body.template_ref)
147- validate_single_value_template_ref_slots(template_ref)
148- row = {
149- "jiuwenclaw_id": normalized,
150- "policy_id": new_uuid4(),
151- "policy_name": body.policy_name,
152- "policy_desc": body.policy_desc,
153- "priority": body.priority,
154- "template_ref": template_ref,
155- "enabled": body.enabled,
156- "data": body.data,
157- "created_at": now,
158- "updated_at": now,
159- }
160- await sync_gateway_templates_after_template_ref_change(
161- self._handler,
162- normalized,
163- old_template_ref={},
164- new_template_ref=row["template_ref"],
165- skip_runtime_update=True,
166- )
167- ack = await gateway_request(
168- normalized,
169- "POST",
170- f"{_GLOBAL_POLICY_PATH}/",
171- _clean_policy_for_create(self._policy_dict_for_push(row, now=now)),
172- )
173- ack_result = ack.get("result") if isinstance(ack, dict) else None
174- row_id: int | None = None
175- if isinstance(ack_result, dict):
176- raw_id = ack_result.get("id")
177- if raw_id is not None:
178- row_id = int(raw_id)
179- if row_id is None or row_id < 1:
180- raise ValueError(
181- "gateway config_effective_global_policies.create returned no id"
182- )
183- 
184- payload = {**row, "id": row_id}
185- created = await self._handler.create(_GLOBAL_POLICY_TABLE, payload)
186- return _row_to_out(created)
187- 
188- async def get(
189- self, jiuwenclaw_id: str, policy_id: int
190- ) -> ConfigEffectiveGlobalPolicyOut | None:
191- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
192- row = await self._handler.get(
193- _GLOBAL_POLICY_TABLE, _global_policy_pk(normalized, policy_id)
194- )
195- if row is None:
196- return None
197- return _row_to_out(row)
198- 
199- async def list_policies(
200- self,
201- jiuwenclaw_id: str,
202- query: ConfigEffectiveGlobalPolicyListQuery,
203- ) -> dict[str, Any]:
204- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
205- page = max(query.page, 1)
206- page_size = min(max(query.page_size, 1), 200)
207- filters: dict[str, Any] = {"jiuwenclaw_id": normalized}
208- if query.enabled is not None:
209- filters["enabled"] = query.enabled
210- order_by = resolve_order_by(
211- query.sort_by,
212- query.sort_order,
213- allowed_sort_fields=_ALLOWED_SORT_FIELDS,
214- default_order_by=DEFAULT_POLICY_ORDER_BY,
215- )
216- 
217- search_query = (query.search or "").strip()
218- if search_query:
219- rows = await self._handler.list_records(
220- _GLOBAL_POLICY_TABLE,
221- filters,
222- limit=_LIST_ALL_CAP,
223- offset=0,
224- order_by=order_by,
225- )
226- rows = [row for row in rows if _matches_search(row, search_query)]
227- total = len(rows)
228- offset = (page - 1) * page_size
229- page_rows = rows[offset:offset + page_size]
230- items = [_row_to_out(row).model_dump(mode="json") for row in page_rows]
231- return {
232- "items": items,
233- "total": total,
234- "page": page,
235- "page_size": page_size,
236- }
237- 
238- offset = (page - 1) * page_size
239- rows = await self._handler.list_records(
240- _GLOBAL_POLICY_TABLE,
241- filters,
242- limit=page_size,
243- offset=offset,
244- order_by=order_by,
245- )
246- total = await self._handler.count_records(_GLOBAL_POLICY_TABLE, filters)
247- items = [_row_to_out(r).model_dump(mode="json") for r in rows]
248- return {
249- "items": items,
250- "total": total,
251- "page": page,
252- "page_size": page_size,
253- }
254- 
255- async def update(
256- self,
257- jiuwenclaw_id: str,
258- policy_id: int,
259- body: ConfigEffectiveGlobalPolicyUpdateBody,
260- ) -> ConfigEffectiveGlobalPolicyOut | None:
261- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
262- 
263- updates = body.model_dump(exclude_unset=True)
264- 
265- row = await self._handler.get(
266- _GLOBAL_POLICY_TABLE, _global_policy_pk(normalized, policy_id)
267- )
268- if row is None:
269- return None
270- 
271- if not updates:
272- return _row_to_out(row)
273- 
274- old_template_ref = read_template_ref_from_row(row)
275- updates = apply_template_ref_to_updates(updates, existing_row=row)
276- 
277- if "template_ref" in updates:
278- validate_single_value_template_ref_slots(updates["template_ref"])
279- await sync_gateway_templates_after_template_ref_change(
280- self._handler,
281- normalized,
282- old_template_ref=old_template_ref,
283- new_template_ref=updates["template_ref"],
284- skip_runtime_update=True,
285- )
286- await gateway_request(
287- normalized,
288- "PATCH",
289- f"{_GLOBAL_POLICY_PATH}/{policy_id}",
290- dict(updates),
291- )
292- payload = dict(updates)
293- payload["updated_at"] = utc_now()
294- updated = await self._handler.update(
295- _GLOBAL_POLICY_TABLE,
296- _global_policy_pk(normalized, policy_id),
297- payload,
298- )
299- if updated is None:
300- return None
301- return _row_to_out(updated)
302- 
303- async def delete(
304- self,
305- jiuwenclaw_id: str,
306- policy_id: int,
307- ) -> bool:
308- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
309- row = await self._handler.get(
310- _GLOBAL_POLICY_TABLE, _global_policy_pk(normalized, policy_id)
311- )
312- if row is None:
313- return False
314- await sync_gateway_templates_after_template_ref_change(
315- self._handler,
316- normalized,
317- old_template_ref=read_template_ref_from_row(row),
318- new_template_ref={},
319- skip_runtime_update=True,
320- )
321- await gateway_request(
322- normalized,
323- "DELETE",
324- f"{_GLOBAL_POLICY_PATH}/{policy_id}",
325- {},
326- )
327- return await self._handler.delete(
328- _GLOBAL_POLICY_TABLE, _global_policy_pk(normalized, policy_id)
329- )
@@ -1,358 +0,0 @@
1-"""配置生效 Service 层级策略 config_effective_service_policy 业务逻辑。"""
2- 
3-from __future__ import annotations
4- 
5-from datetime import datetime
6-from typing import Any
7- 
8-from openjiuwen_runtime.foundation.db.handler import DBHandler
9- 
10-from manager_server.core.template.push_template_to_gateway import (
11- sync_gateway_templates_after_template_ref_change,
12-)
13-from manager_server.infrastructure.common import (
14- DEFAULT_POLICY_ORDER_BY,
15- resolve_order_by,
16-)
17-from manager_server.infrastructure.jiuwenclaw_id import validate_jiuwenclaw_id
18-from manager_server.infrastructure.utils import (
19- iso_datetime,
20- new_uuid4,
21- utc_now,
22-)
23-from manager_server.manager_config_push import gateway_request
24-from manager_server.models.config_effective_policy_models import (
25- CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF,
26- CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF,
27-)
28-from manager_server.infrastructure.template_ref import (
29- apply_template_ref_to_updates,
30- normalize_template_ref,
31- read_template_ref_from_row,
32- validate_single_value_template_ref_slots,
33-)
34-from manager_server.schemas.config_effective_policy_schemas import (
35- ConfigEffectiveServicePolicyCreateBody,
36- ConfigEffectiveServicePolicyListQuery,
37- ConfigEffectiveServicePolicyOut,
38- ConfigEffectiveServicePolicyUpdateBody,
39-)
40- 
41-_SERVICE_POLICY_TABLE = CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF.table_name
42-_AGENT_POLICY_TABLE = CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF.table_name
43-_SERVICE_POLICY_PATH = "/api/v1/config-effective/service-policies"
44-_LIST_ALL_CAP = 10_000
45-_PUSH_DROP_KEYS = frozenset({"id", "created_at", "updated_at", "jiuwenclaw_id"})
46-_ALLOWED_SORT_FIELDS = frozenset({
47- "policy_name",
48- "policy_desc",
49- "priority",
50- "match_expr",
51- "service_id",
52- "updated_at",
53-})
54- 
55- 
56-def _clean_policy_for_create(policy: dict[str, Any]) -> dict[str, Any]:
57- return {k: v for k, v in policy.items() if k not in _PUSH_DROP_KEYS}
58- 
59- 
60-def _matches_search(row: Any, query: str) -> bool:
61- needle = query.strip().lower()
62- if not needle:
63- return True
64- fields = [
65- str(getattr(row, "policy_id", "") or ""),
66- str(getattr(row, "policy_name", "") or ""),
67- str(getattr(row, "policy_desc", "") or ""),
68- str(getattr(row, "service_id", "") or ""),
69- str(getattr(row, "priority", "") or ""),
70- str(getattr(row, "match_expr", "") or ""),
71- ]
72- return any(needle in field.lower() for field in fields)
73- 
74- 
75-async def push_service_policies_sync_to_gateway(
76- handler: DBHandler,
77- jiuwenclaw_id: str,
78-) -> dict[str, Any]:
79- """Gateway 注册后:将 MDB 中该实例全部 Service 策略 bulk push 到 GDB。"""
80- jid = str(jiuwenclaw_id or "").strip()
81- if not jid:
82- raise ValueError("jiuwenclaw_id is required")
83- rows = await handler.list_records(
84- _SERVICE_POLICY_TABLE,
85- {"jiuwenclaw_id": jid},
86- limit=_LIST_ALL_CAP,
87- offset=0,
88- )
89- policies = [_row_to_out(row).model_dump(mode="json") for row in rows]
90- last: dict[str, Any] | None = None
91- for idx, policy in enumerate(policies):
92- if not isinstance(policy, dict):
93- continue
94- last = await gateway_request(
95- jid,
96- "POST",
97- f"{_SERVICE_POLICY_PATH}/",
98- _clean_policy_for_create(policy),
99- revision=f"sync-service-policy:{idx}",
100- )
101- return last or {
102- "revision": "sync-service-policy",
103- "success_flag": True,
104- "result": {"synced": 0},
105- "transport": "http",
106- }
107- 
108- 
109-def _service_policy_pk(jiuwenclaw_id: str, policy_id: int) -> dict[str, Any]:
110- return {"jiuwenclaw_id": jiuwenclaw_id, "id": policy_id}
111- 
112- 
113-def _row_to_out(row: Any) -> ConfigEffectiveServicePolicyOut:
114- return ConfigEffectiveServicePolicyOut(
115- id=row.id,
116- jiuwenclaw_id=row.jiuwenclaw_id,
117- policy_id=row.policy_id,
118- policy_name=row.policy_name,
119- policy_desc=row.policy_desc,
120- service_id=row.service_id,
121- priority=row.priority,
122- match_expr=row.match_expr,
123- template_ref=read_template_ref_from_row(row),
124- enabled=row.enabled,
125- data=row.data,
126- created_at=iso_datetime(row.created_at),
127- updated_at=iso_datetime(row.updated_at),
128- )
129- 
130- 
131-class ConfigEffectiveServicePolicyService:
132- def __init__(self, handler: DBHandler) -> None:
133- self._handler = handler
134- 
135- @staticmethod
136- def _policy_dict_for_push(row: dict[str, Any], *, now: datetime) -> dict[str, Any]:
137- """构建下发给 Gateway 的 policy 对象(不含 id,由 Gateway 自增)。"""
138- return {
139- "jiuwenclaw_id": row["jiuwenclaw_id"],
140- "policy_id": row["policy_id"],
141- "policy_name": row.get("policy_name"),
142- "policy_desc": row.get("policy_desc"),
143- "service_id": row["service_id"],
144- "priority": row["priority"],
145- "match_expr": row.get("match_expr"),
146- "template_ref": normalize_template_ref(row.get("template_ref")),
147- "enabled": row.get("enabled", True),
148- "data": row.get("data"),
149- "created_at": iso_datetime(row.get("created_at") or now),
150- "updated_at": iso_datetime(row.get("updated_at") or now),
151- }
152- 
153- async def create(
154- self,
155- jiuwenclaw_id: str,
156- body: ConfigEffectiveServicePolicyCreateBody,
157- ) -> ConfigEffectiveServicePolicyOut:
158- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
159- 
160- now = utc_now()
161- template_ref = normalize_template_ref(body.template_ref)
162- validate_single_value_template_ref_slots(template_ref)
163- row = {
164- "jiuwenclaw_id": normalized,
165- "policy_id": new_uuid4(),
166- "policy_name": body.policy_name,
167- "policy_desc": body.policy_desc,
168- "service_id": body.service_id.strip(),
169- "priority": body.priority,
170- "match_expr": body.match_expr,
171- "template_ref": template_ref,
172- "enabled": body.enabled,
173- "data": body.data,
174- "created_at": now,
175- "updated_at": now,
176- }
177- await sync_gateway_templates_after_template_ref_change(
178- self._handler,
179- normalized,
180- old_template_ref={},
181- new_template_ref=row["template_ref"],
182- skip_runtime_update=True,
183- )
184- ack = await gateway_request(
185- normalized,
186- "POST",
187- f"{_SERVICE_POLICY_PATH}/",
188- _clean_policy_for_create(self._policy_dict_for_push(row, now=now)),
189- )
190- ack_result = ack.get("result") if isinstance(ack, dict) else None
191- row_id: int | None = None
192- if isinstance(ack_result, dict):
193- raw_id = ack_result.get("id")
194- if raw_id is not None:
195- row_id = int(raw_id)
196- if row_id is None or row_id < 1:
197- raise ValueError(
198- "gateway config_effective_service_policies.create returned no id"
199- )
200- 
201- payload = {**row, "id": row_id}
202- created = await self._handler.create(_SERVICE_POLICY_TABLE, payload)
203- return _row_to_out(created)
204- 
205- async def get(
206- self, jiuwenclaw_id: str, policy_id: int
207- ) -> ConfigEffectiveServicePolicyOut | None:
208- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
209- row = await self._handler.get(
210- _SERVICE_POLICY_TABLE, _service_policy_pk(normalized, policy_id)
211- )
212- if row is None:
213- return None
214- return _row_to_out(row)
215- 
216- async def list_policies(
217- self,
218- jiuwenclaw_id: str,
219- query: ConfigEffectiveServicePolicyListQuery,
220- ) -> dict[str, Any]:
221- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
222- page = max(query.page, 1)
223- page_size = min(max(query.page_size, 1), 200)
224- filters: dict[str, Any] = {"jiuwenclaw_id": normalized}
225- if query.enabled is not None:
226- filters["enabled"] = query.enabled
227- order_by = resolve_order_by(
228- query.sort_by,
229- query.sort_order,
230- allowed_sort_fields=_ALLOWED_SORT_FIELDS,
231- default_order_by=DEFAULT_POLICY_ORDER_BY,
232- )
233- 
234- search_query = (query.search or "").strip()
235- if search_query:
236- rows = await self._handler.list_records(
237- _SERVICE_POLICY_TABLE,
238- filters,
239- limit=_LIST_ALL_CAP,
240- offset=0,
241- order_by=order_by,
242- )
243- rows = [row for row in rows if _matches_search(row, search_query)]
244- total = len(rows)
245- offset = (page - 1) * page_size
246- page_rows = rows[offset:offset + page_size]
247- items = [_row_to_out(row).model_dump(mode="json") for row in page_rows]
248- return {
249- "items": items,
250- "total": total,
251- "page": page,
252- "page_size": page_size,
253- }
254- 
255- offset = (page - 1) * page_size
256- rows = await self._handler.list_records(
257- _SERVICE_POLICY_TABLE,
258- filters,
259- limit=page_size,
260- offset=offset,
261- order_by=order_by,
262- )
263- total = await self._handler.count_records(_SERVICE_POLICY_TABLE, filters)
264- items = [_row_to_out(r).model_dump(mode="json") for r in rows]
265- return {
266- "items": items,
267- "total": total,
268- "page": page,
269- "page_size": page_size,
270- }
271- 
272- async def update(
273- self,
274- jiuwenclaw_id: str,
275- policy_id: int,
276- body: ConfigEffectiveServicePolicyUpdateBody,
277- ) -> ConfigEffectiveServicePolicyOut | None:
278- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
279- 
280- updates = body.model_dump(exclude_unset=True)
281- if "service_id" in updates and updates["service_id"] is not None:
282- updates["service_id"] = updates["service_id"].strip()
283- if not updates["service_id"]:
284- raise ValueError("service_id cannot be empty")
285- 
286- row = await self._handler.get(
287- _SERVICE_POLICY_TABLE, _service_policy_pk(normalized, policy_id)
288- )
289- if row is None:
290- return None
291- 
292- if not updates:
293- return _row_to_out(row)
294- 
295- old_template_ref = read_template_ref_from_row(row)
296- updates = apply_template_ref_to_updates(updates, existing_row=row)
297- 
298- if "template_ref" in updates:
299- validate_single_value_template_ref_slots(updates["template_ref"])
300- await sync_gateway_templates_after_template_ref_change(
301- self._handler,
302- normalized,
303- old_template_ref=old_template_ref,
304- new_template_ref=updates["template_ref"],
305- skip_runtime_update=True,
306- )
307- await gateway_request(
308- normalized,
309- "PATCH",
310- f"{_SERVICE_POLICY_PATH}/{policy_id}",
311- dict(updates),
312- )
313- payload = dict(updates)
314- payload["updated_at"] = utc_now()
315- updated = await self._handler.update(
316- _SERVICE_POLICY_TABLE,
317- _service_policy_pk(normalized, policy_id),
318- payload,
319- )
320- if updated is None:
321- return None
322- return _row_to_out(updated)
323- 
324- async def delete(
325- self,
326- jiuwenclaw_id: str,
327- policy_id: int,
328- ) -> bool:
329- normalized = await validate_jiuwenclaw_id(self._handler, jiuwenclaw_id)
330- row = await self._handler.get(
331- _SERVICE_POLICY_TABLE, _service_policy_pk(normalized, policy_id)
332- )
333- if row is None:
334- return False
335- linked_count = await self._handler.count_records(
336- _AGENT_POLICY_TABLE,
337- {"jiuwenclaw_id": normalized, "service_policy_id": row.policy_id},
338- )
339- if linked_count > 0:
340- raise ValueError(
341- f"cannot delete service policy: {linked_count} linked agent policies exist"
342- )
343- await sync_gateway_templates_after_template_ref_change(
344- self._handler,
345- normalized,
346- old_template_ref=read_template_ref_from_row(row),
347- new_template_ref={},
348- skip_runtime_update=True,
349- )
350- await gateway_request(
351- normalized,
352- "DELETE",
353- f"{_SERVICE_POLICY_PATH}/{policy_id}",
354- {},
355- )
356- return await self._handler.delete(
357- _SERVICE_POLICY_TABLE, _service_policy_pk(normalized, policy_id)
358- )
@@ -11,18 +11,6 @@ from manager_server.core.application_config.log_masking_rule import (
11 push_log_masking_rules_sync_to_gateway,11 push_log_masking_rules_sync_to_gateway,
12 seed_builtin_log_masking_rules,12 seed_builtin_log_masking_rules,
13)13)
14-from manager_server.core.config_effective_policy.config_default_template_mapping import (
15- push_template_mappings_sync_to_gateway,
16-)
17-from manager_server.core.config_effective_policy.config_effective_agent_policy import (
18- push_agent_policies_sync_to_gateway,
19-)
20-from manager_server.core.config_effective_policy.config_effective_global_policy import (
21- push_global_policies_sync_to_gateway,
22-)
23-from manager_server.core.config_effective_policy.config_effective_service_policy import (
24- push_service_policies_sync_to_gateway,
25-)
26from manager_server.core.instance.instance_service import (14from manager_server.core.instance.instance_service import (
27 _LOG_MASKING_SEEDED_KEY,15 _LOG_MASKING_SEEDED_KEY,
28 get_instance_row,16 get_instance_row,
@@ -37,12 +25,6 @@ from manager_server.core.template.push_template_to_gateway import (
37 rebuild_jid_template_ref_for_gateway,25 rebuild_jid_template_ref_for_gateway,
38 sync_referenced_templates_to_gateway,26 sync_referenced_templates_to_gateway,
39)27)
40-from manager_server.models.config_effective_policy_models import (
41- CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF,
42- CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF,
43- CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF,
44- CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF,
45-)
46from manager_server.models.jid_template_ref_models import (28from manager_server.models.jid_template_ref_models import (
47 JID_TEMPLATE_REF_TABLE_DEF,29 JID_TEMPLATE_REF_TABLE_DEF,
48)30)
@@ -58,13 +40,6 @@ logger = logging.getLogger(__name__)
58 40 
59_LIST_ALL_CAP = 10_00041_LIST_ALL_CAP = 10_000
60 42 
61-_MANAGER_POLICY_TABLES = (
62- CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF.table_name,
63- CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF.table_name,
64- CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF.table_name,
65- CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF.table_name,
66-)
67- 
68_MANAGER_INSTANCE_TABLES = (43_MANAGER_INSTANCE_TABLES = (
69 LOG_MASKING_RULE_TABLE_DEF.table_name,44 LOG_MASKING_RULE_TABLE_DEF.table_name,
70 LOGGING_CONFIG_TABLE_DEF.table_name,45 LOGGING_CONFIG_TABLE_DEF.table_name,
@@ -99,12 +74,10 @@ async def sync_data_to_gateway_on_register(
99 (见 ``maybe_full_sync_gateway_on_online``)。74 (见 ``maybe_full_sync_gateway_on_online``)。
100 75 
101 顺序说明:76 顺序说明:
102- 1. 模板(策略/映射依赖)77+ 1. 模板(Agent 资源依赖)
103 2. Agent 资源78 2. Agent 资源
104- 3. 全局 / Service / Agent 策略(Agent 依赖 Service)79+ 3. 日志脱敏规则
105- 4. 默认模板映射80+ 4. 重建 Manager 侧 jid_template_ref 索引
106- 5. 日志脱敏规则
107- 6. 重建 Manager 侧 jid_template_ref 索引
108 """81 """
109 jid = str(jiuwenclaw_id or "").strip()82 jid = str(jiuwenclaw_id or "").strip()
110 if not jid:83 if not jid:
@@ -132,10 +105,6 @@ async def sync_data_to_gateway_on_register(
132 raise105 raise
133 106 
134 for name, push_fn, before_fn in (107 for name, push_fn, before_fn in (
135- ("global_policies", push_global_policies_sync_to_gateway, None),
136- ("service_policies", push_service_policies_sync_to_gateway, None),
137- ("agent_policies", push_agent_policies_sync_to_gateway, None),
138- ("template_mappings", push_template_mappings_sync_to_gateway, None),
139 (108 (
140 "log_masking_rule",109 "log_masking_rule",
141 push_log_masking_rules_sync_to_gateway,110 push_log_masking_rules_sync_to_gateway,
@@ -177,8 +146,6 @@ def _delete_pk_for_row(table: str, row: Any, jiuwenclaw_id: str) -> dict[str, An
177 "slot": getattr(row, "slot"),146 "slot": getattr(row, "slot"),
178 "template_id": getattr(row, "template_id"),147 "template_id": getattr(row, "template_id"),
179 }148 }
180- if table in _MANAGER_POLICY_TABLES:
181- return {"id": getattr(row, "id"), "jiuwenclaw_id": jiuwenclaw_id}
182 return {"id": getattr(row, "id")}149 return {"id": getattr(row, "id")}
183 150 
184 151 
@@ -211,7 +178,7 @@ async def purge_manager_instance_data(
211 return {}178 return {}
212 179 
213 deleted_counts: dict[str, int] = {}180 deleted_counts: dict[str, int] = {}
214- for table in (*_MANAGER_POLICY_TABLES, *_MANAGER_INSTANCE_TABLES):181+ for table in _MANAGER_INSTANCE_TABLES:
215 count = await _purge_table_rows(handler, table, jid)182 count = await _purge_table_rows(handler, table, jid)
216 if count:183 if count:
217 deleted_counts[table] = count184 deleted_counts[table] = count
@@ -1,123 +0,0 @@
1-"""企业级配置策略表定义(复合主键 id + jiuwenclaw_id,与 Gateway 返回的 id 对齐)。"""
2- 
3-from __future__ import annotations
4- 
5-from openjiuwen_runtime.foundation.db.table_def import (
6- ColumnDefinition,
7- IndexDefinition,
8- TableDefinition,
9-)
10- 
11-CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF = TableDefinition(
12- table_name="config_effective_global_policy",
13- columns=[
14- ColumnDefinition(
15- "id",
16- "integer",
17- primary_key=True,
18- autoincrement=False,
19- nullable=False,
20- ),
21- ColumnDefinition("jiuwenclaw_id", "string", length=64, primary_key=True, nullable=False),
22- ColumnDefinition("policy_id", "string", length=100, nullable=False),
23- ColumnDefinition("policy_name", "string", length=128, nullable=False),
24- ColumnDefinition("policy_desc", "string", length=512, nullable=True),
25- ColumnDefinition("priority", "integer", nullable=False),
26- ColumnDefinition("template_ref", "json", nullable=False),
27- ColumnDefinition("enabled", "boolean", nullable=False, default=True),
28- ColumnDefinition("data", "json", nullable=True),
29- ColumnDefinition("created_at", "datetime", nullable=False),
30- ColumnDefinition("updated_at", "datetime", nullable=False),
31- ],
32- indexes=[
33- IndexDefinition(["jiuwenclaw_id", "policy_id"], unique=True),
34- ],
35-)
36- 
37-CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF = TableDefinition(
38- table_name="config_effective_service_policy",
39- columns=[
40- ColumnDefinition(
41- "id",
42- "integer",
43- primary_key=True,
44- autoincrement=False,
45- nullable=False,
46- ),
47- ColumnDefinition("jiuwenclaw_id", "string", length=64, primary_key=True, nullable=False),
48- ColumnDefinition("policy_id", "string", length=100, nullable=False),
49- ColumnDefinition("policy_name", "string", length=128, nullable=False),
50- ColumnDefinition("policy_desc", "string", length=512, nullable=True),
51- ColumnDefinition("service_id", "string", length=512, nullable=False),
52- ColumnDefinition("priority", "integer", nullable=False),
53- ColumnDefinition("match_expr", "string", length=8192, nullable=True),
54- ColumnDefinition("template_ref", "json", nullable=False),
55- ColumnDefinition("enabled", "boolean", nullable=False, default=True),
56- ColumnDefinition("data", "json", nullable=True),
57- ColumnDefinition("created_at", "datetime", nullable=False),
58- ColumnDefinition("updated_at", "datetime", nullable=False),
59- ],
60- indexes=[
61- IndexDefinition(["jiuwenclaw_id", "policy_id"], unique=True),
62- ],
63-)
64- 
65-CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF = TableDefinition(
66- table_name="config_effective_agent_policy",
67- columns=[
68- ColumnDefinition(
69- "id",
70- "integer",
71- primary_key=True,
72- autoincrement=False,
73- nullable=False,
74- ),
75- ColumnDefinition("jiuwenclaw_id", "string", length=64, primary_key=True, nullable=False),
76- ColumnDefinition("policy_id", "string", length=100, nullable=False),
77- ColumnDefinition("policy_name", "string", length=128, nullable=False),
78- ColumnDefinition("policy_desc", "string", length=512, nullable=True),
79- ColumnDefinition("agent_id", "string", length=512, nullable=False),
80- ColumnDefinition("workspace_dir", "string", length=512, nullable=True),
81- ColumnDefinition("service_policy_id", "string", length=100, nullable=False),
82- ColumnDefinition("priority", "integer", nullable=False, default=0),
83- ColumnDefinition("match_expr", "string", length=8192, nullable=True),
84- ColumnDefinition("template_ref", "json", nullable=False),
85- ColumnDefinition("send_file_allowed", "boolean", nullable=True, default=True),
86- ColumnDefinition("enabled", "boolean", nullable=False, default=True),
87- ColumnDefinition("data", "json", nullable=True),
88- ColumnDefinition("created_at", "datetime", nullable=False),
89- ColumnDefinition("updated_at", "datetime", nullable=False),
90- ],
91- indexes=[
92- IndexDefinition(["jiuwenclaw_id", "policy_id"], unique=True),
93- ],
94-)
95- 
96-CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF = TableDefinition(
97- table_name="config_default_template_mapping",
98- columns=[
99- ColumnDefinition(
100- "id",
101- "integer",
102- primary_key=True,
103- autoincrement=False,
104- nullable=False,
105- ),
106- ColumnDefinition("jiuwenclaw_id", "string", length=64, primary_key=True, nullable=False),
107- ColumnDefinition("policy_id", "string", length=100, nullable=False),
108- ColumnDefinition("policy_name", "string", length=128, nullable=False),
109- ColumnDefinition("policy_desc", "string", length=512, nullable=True),
110- ColumnDefinition("scope_type", "string", length=32, nullable=False),
111- ColumnDefinition("scope_id", "string", length=512, nullable=False),
112- ColumnDefinition("priority", "integer", nullable=False),
113- ColumnDefinition("template_id", "string", length=100, nullable=False),
114- ColumnDefinition("template_type", "string", length=512, nullable=False),
115- ColumnDefinition("enabled", "boolean", nullable=False, default=True),
116- ColumnDefinition("data", "json", nullable=True),
117- ColumnDefinition("created_at", "datetime", nullable=False),
118- ColumnDefinition("updated_at", "datetime", nullable=False),
119- ],
120- indexes=[
121- IndexDefinition(["jiuwenclaw_id", "policy_id"], unique=True),
122- ],
123-)
@@ -4,12 +4,6 @@ from __future__ import annotations
4 4 
5from openjiuwen_runtime.foundation.db.handler import DBHandler5from openjiuwen_runtime.foundation.db.handler import DBHandler
6 6 
7-from manager_server.models.config_effective_policy_models import (
8- CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF,
9- CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF,
10- CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF,
11- CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF,
12-)
13from manager_server.models.instance_models import INSTANCE_INFO_TABLE_DEF7from manager_server.models.instance_models import INSTANCE_INFO_TABLE_DEF
14from manager_server.models.key_models import (8from manager_server.models.key_models import (
15 INSTANCE_ENC_PUBKEY_TABLE_DEF,9 INSTANCE_ENC_PUBKEY_TABLE_DEF,
@@ -51,10 +45,6 @@ ALL_TABLE_DEFINITIONS = (
51 SKILL_WHITELIST_TEMPLATE_TABLE_DEF,45 SKILL_WHITELIST_TEMPLATE_TABLE_DEF,
52 SERVICE_CONFIG_TEMPLATE_TABLE_DEF,46 SERVICE_CONFIG_TEMPLATE_TABLE_DEF,
53 AGENT_TEMPLATE_TABLE_DEF,47 AGENT_TEMPLATE_TABLE_DEF,
54- CONFIG_DEFAULT_TEMPLATE_MAPPING_TABLE_DEF,
55- CONFIG_EFFECTIVE_GLOBAL_POLICY_TABLE_DEF,
56- CONFIG_EFFECTIVE_SERVICE_POLICY_TABLE_DEF,
57- CONFIG_EFFECTIVE_AGENT_POLICY_TABLE_DEF,
58 JID_TEMPLATE_REF_TABLE_DEF,48 JID_TEMPLATE_REF_TABLE_DEF,
59 *INSTANCE_ACCESS_TABLE_DEFINITIONS,49 *INSTANCE_ACCESS_TABLE_DEFINITIONS,
60 *INSTANCE_RESOURCE_TABLE_DEFINITIONS,50 *INSTANCE_RESOURCE_TABLE_DEFINITIONS,
@@ -1,395 +0,0 @@
1-"""配置生效策略与默认模板映射 CRUD API(按组网实例路径隔离)。"""
2- 
3-from __future__ import annotations
4- 
5-from typing import Annotated
6- 
7-from fastapi import APIRouter, Depends, HTTPException, Query
8-from openjiuwen_runtime.foundation.db.handler import DBHandler
9- 
10-from manager_server.infrastructure.db import get_db_handler
11-from manager_server.schemas.common_schemas import ResponseModel
12-from manager_server.schemas.config_effective_policy_schemas import (
13- ConfigDefaultTemplateMappingCreateBody,
14- ConfigDefaultTemplateMappingListQuery,
15- ConfigDefaultTemplateMappingUpdateBody,
16- ConfigEffectiveAgentPolicyCreateBody,
17- ConfigEffectiveAgentPolicyListQuery,
18- ConfigEffectiveAgentPolicyUpdateBody,
19- ConfigEffectiveGlobalPolicyCreateBody,
20- ConfigEffectiveGlobalPolicyListQuery,
21- ConfigEffectiveGlobalPolicyUpdateBody,
22- ConfigEffectiveServicePolicyCreateBody,
23- ConfigEffectiveServicePolicyListQuery,
24- ConfigEffectiveServicePolicyUpdateBody,
25-)
26-from manager_server.core.config_effective_policy import (
27- ConfigDefaultTemplateMappingService,
28- ConfigEffectiveAgentPolicyService,
29- ConfigEffectiveGlobalPolicyService,
30- ConfigEffectiveServicePolicyService,
31-)
32- 
33-# --- 默认模板映射 ---
34- 
35-mapping_router = APIRouter(
36- prefix="/{jiuwenclaw_id}/config-default-template-mappings",
37-)
38- 
39- 
40-def _mapping_svc(handler: DBHandler) -> ConfigDefaultTemplateMappingService:
41- return ConfigDefaultTemplateMappingService(handler)
42- 
43- 
44-@mapping_router.post("/", response_model=ResponseModel)
45-async def create_template_mapping(
46- jiuwenclaw_id: str,
47- body: ConfigDefaultTemplateMappingCreateBody,
48- handler: Annotated[DBHandler, Depends(get_db_handler)],
49-):
50- svc = _mapping_svc(handler)
51- try:
52- data = await svc.create(jiuwenclaw_id, body)
53- except ValueError as exc:
54- raise HTTPException(status_code=400, detail=str(exc)) from exc
55- return ResponseModel(code=200, message="success", data=data.model_dump())
56- 
57- 
58-@mapping_router.get("/", response_model=ResponseModel)
59-async def list_template_mappings(
60- jiuwenclaw_id: str,
61- handler: Annotated[DBHandler, Depends(get_db_handler)],
62- query: Annotated[ConfigDefaultTemplateMappingListQuery, Query()],
63-):
64- svc = _mapping_svc(handler)
65- try:
66- data = await svc.list_mappings(jiuwenclaw_id, query)
67- except ValueError as exc:
68- raise HTTPException(status_code=400, detail=str(exc)) from exc
69- return ResponseModel(code=200, message="success", data=data)
70- 
71- 
72-@mapping_router.get("/{row_id}", response_model=ResponseModel)
73-async def get_template_mapping(
74- jiuwenclaw_id: str,
75- row_id: int,
76- handler: Annotated[DBHandler, Depends(get_db_handler)],
77-):
78- svc = _mapping_svc(handler)
79- try:
80- row = await svc.get(jiuwenclaw_id, row_id)
81- except ValueError as exc:
82- raise HTTPException(status_code=400, detail=str(exc)) from exc
83- if row is None:
84- raise HTTPException(status_code=404, detail="template mapping not found")
85- return ResponseModel(code=200, message="success", data=row.model_dump())
86- 
87- 
88-@mapping_router.patch("/{row_id}", response_model=ResponseModel)
89-async def update_template_mapping(
90- jiuwenclaw_id: str,
91- row_id: int,
92- body: ConfigDefaultTemplateMappingUpdateBody,
93- handler: Annotated[DBHandler, Depends(get_db_handler)],
94-):
95- svc = _mapping_svc(handler)
96- try:
97- row = await svc.update(jiuwenclaw_id, row_id, body)
98- except ValueError as exc:
99- raise HTTPException(status_code=400, detail=str(exc)) from exc
100- if row is None:
101- raise HTTPException(status_code=404, detail="template mapping not found")
102- return ResponseModel(code=200, message="success", data=row.model_dump())
103- 
104- 
105-@mapping_router.delete("/{row_id}", response_model=ResponseModel)
106-async def delete_template_mapping(
107- jiuwenclaw_id: str,
108- row_id: int,
109- handler: Annotated[DBHandler, Depends(get_db_handler)],
110-):
111- svc = _mapping_svc(handler)
112- try:
113- ok = await svc.delete(jiuwenclaw_id, row_id)
114- except ValueError as exc:
115- raise HTTPException(status_code=400, detail=str(exc)) from exc
116- if not ok:
117- raise HTTPException(status_code=404, detail="template mapping not found")
118- return ResponseModel(
119- code=200, message="success", data={"deleted": True, "id": row_id}
120- )
121- 
122-# --- Global 兜底 ---
123- 
124-global_router = APIRouter(
125- prefix="/{jiuwenclaw_id}/config-effective/global-policies",
126-)
127- 
128- 
129-def _global_svc(handler: DBHandler) -> ConfigEffectiveGlobalPolicyService:
130- return ConfigEffectiveGlobalPolicyService(handler)
131- 
132- 
133-@global_router.post("/", response_model=ResponseModel)
134-async def create_global_policy(
135- jiuwenclaw_id: str,
136- body: ConfigEffectiveGlobalPolicyCreateBody,
137- handler: Annotated[DBHandler, Depends(get_db_handler)],
138-):
139- svc = _global_svc(handler)
140- try:
141- data = await svc.create(jiuwenclaw_id, body)
142- except ValueError as exc:
143- raise HTTPException(status_code=400, detail=str(exc)) from exc
144- return ResponseModel(code=200, message="success", data=data.model_dump())
145- 
146- 
147-@global_router.get("/", response_model=ResponseModel)
148-async def list_global_policies(
149- jiuwenclaw_id: str,
150- handler: Annotated[DBHandler, Depends(get_db_handler)],
151- query: Annotated[ConfigEffectiveGlobalPolicyListQuery, Query()],
152-):
153- svc = _global_svc(handler)
154- try:
155- data = await svc.list_policies(jiuwenclaw_id, query)
156- except ValueError as exc:
157- raise HTTPException(status_code=400, detail=str(exc)) from exc
158- return ResponseModel(code=200, message="success", data=data)
159- 
160- 
161-@global_router.get("/{row_id}", response_model=ResponseModel)
162-async def get_global_policy(
163- jiuwenclaw_id: str,
164- row_id: int,
165- handler: Annotated[DBHandler, Depends(get_db_handler)],
166-):
167- svc = _global_svc(handler)
168- try:
169- row = await svc.get(jiuwenclaw_id, row_id)
170- except ValueError as exc:
171- raise HTTPException(status_code=400, detail=str(exc)) from exc
172- if row is None:
173- raise HTTPException(status_code=404, detail="global policy not found")
174- return ResponseModel(code=200, message="success", data=row.model_dump())
175- 
176- 
177-@global_router.patch("/{row_id}", response_model=ResponseModel)
178-async def update_global_policy(
179- jiuwenclaw_id: str,
180- row_id: int,
181- body: ConfigEffectiveGlobalPolicyUpdateBody,
182- handler: Annotated[DBHandler, Depends(get_db_handler)],
183-):
184- svc = _global_svc(handler)
185- try:
186- row = await svc.update(jiuwenclaw_id, row_id, body)
187- except ValueError as exc:
188- raise HTTPException(status_code=400, detail=str(exc)) from exc
189- if row is None:
190- raise HTTPException(status_code=404, detail="global policy not found")
191- return ResponseModel(code=200, message="success", data=row.model_dump())
192- 
193- 
194-@global_router.delete("/{row_id}", response_model=ResponseModel)
195-async def delete_global_policy(
196- jiuwenclaw_id: str,
197- row_id: int,
198- handler: Annotated[DBHandler, Depends(get_db_handler)],
199-):
200- svc = _global_svc(handler)
201- try:
202- ok = await svc.delete(jiuwenclaw_id, row_id)
203- except ValueError as exc:
204- raise HTTPException(status_code=400, detail=str(exc)) from exc
205- if not ok:
206- raise HTTPException(status_code=404, detail="global policy not found")
207- return ResponseModel(
208- code=200, message="success", data={"deleted": True, "id": row_id}
209- )
210- 
211-# --- Service 层级 ---
212- 
213-service_router = APIRouter(
214- prefix="/{jiuwenclaw_id}/config-effective/service-policies",
215-)
216- 
217- 
218-def _service_svc(handler: DBHandler) -> ConfigEffectiveServicePolicyService:
219- return ConfigEffectiveServicePolicyService(handler)
220- 
221- 
222-@service_router.post("/", response_model=ResponseModel)
223-async def create_service_policy(
224- jiuwenclaw_id: str,
225- body: ConfigEffectiveServicePolicyCreateBody,
226- handler: Annotated[DBHandler, Depends(get_db_handler)],
227-):
228- svc = _service_svc(handler)
229- try:
230- data = await svc.create(jiuwenclaw_id, body)
231- except ValueError as exc:
232- raise HTTPException(status_code=400, detail=str(exc)) from exc
233- return ResponseModel(code=200, message="success", data=data.model_dump())
234- 
235- 
236-@service_router.get("/", response_model=ResponseModel)
237-async def list_service_policies(
238- jiuwenclaw_id: str,
239- handler: Annotated[DBHandler, Depends(get_db_handler)],
240- query: Annotated[ConfigEffectiveServicePolicyListQuery, Query()],
241-):
242- svc = _service_svc(handler)
243- try:
244- data = await svc.list_policies(jiuwenclaw_id, query)
245- except ValueError as exc:
246- raise HTTPException(status_code=400, detail=str(exc)) from exc
247- return ResponseModel(code=200, message="success", data=data)
248- 
249- 
250-@service_router.get("/{row_id}", response_model=ResponseModel)
251-async def get_service_policy(
252- jiuwenclaw_id: str,
253- row_id: int,
254- handler: Annotated[DBHandler, Depends(get_db_handler)],
255-):
256- svc = _service_svc(handler)
257- try:
258- row = await svc.get(jiuwenclaw_id, row_id)
259- except ValueError as exc:
260- raise HTTPException(status_code=400, detail=str(exc)) from exc
261- if row is None:
262- raise HTTPException(status_code=404, detail="service policy not found")
263- return ResponseModel(code=200, message="success", data=row.model_dump())
264- 
265- 
266-@service_router.patch("/{row_id}", response_model=ResponseModel)
267-async def update_service_policy(
268- jiuwenclaw_id: str,
269- row_id: int,
270- body: ConfigEffectiveServicePolicyUpdateBody,
271- handler: Annotated[DBHandler, Depends(get_db_handler)],
272-):
273- svc = _service_svc(handler)
274- try:
275- row = await svc.update(jiuwenclaw_id, row_id, body)
276- except ValueError as exc:
277- raise HTTPException(status_code=400, detail=str(exc)) from exc
278- if row is None:
279- raise HTTPException(status_code=404, detail="service policy not found")
280- return ResponseModel(code=200, message="success", data=row.model_dump())
281- 
282- 
283-@service_router.delete("/{row_id}", response_model=ResponseModel)
284-async def delete_service_policy(
285- jiuwenclaw_id: str,
286- row_id: int,
287- handler: Annotated[DBHandler, Depends(get_db_handler)],
288-):
289- svc = _service_svc(handler)
290- try:
291- ok = await svc.delete(jiuwenclaw_id, row_id)
292- except ValueError as exc:
293- raise HTTPException(status_code=400, detail=str(exc)) from exc
294- if not ok:
295- raise HTTPException(status_code=404, detail="service policy not found")
296- return ResponseModel(
297- code=200, message="success", data={"deleted": True, "id": row_id}
298- )
299- 
300- 
301-# --- Agent 层级 ---
302- 
303-agent_router = APIRouter(
304- prefix="/{jiuwenclaw_id}/config-effective/agent-policies",
305-)
306- 
307- 
308-def _agent_svc(handler: DBHandler) -> ConfigEffectiveAgentPolicyService:
309- return ConfigEffectiveAgentPolicyService(handler)
310- 
311- 
312-@agent_router.post("/", response_model=ResponseModel)
313-async def create_agent_policy(
314- jiuwenclaw_id: str,
315- body: ConfigEffectiveAgentPolicyCreateBody,
316- handler: Annotated[DBHandler, Depends(get_db_handler)],
317-):
318- svc = _agent_svc(handler)
319- try:
320- data = await svc.create(jiuwenclaw_id, body)
321- except ValueError as exc:
322- raise HTTPException(status_code=400, detail=str(exc)) from exc
323- return ResponseModel(code=200, message="success", data=data.model_dump())
324- 
325- 
326-@agent_router.get("/", response_model=ResponseModel)
327-async def list_agent_policies(
328- jiuwenclaw_id: str,
329- handler: Annotated[DBHandler, Depends(get_db_handler)],
330- query: Annotated[ConfigEffectiveAgentPolicyListQuery, Query()],
331-):
332- svc = _agent_svc(handler)
333- try:
334- data = await svc.list_policies(jiuwenclaw_id, query)
335- except ValueError as exc:
336- raise HTTPException(status_code=400, detail=str(exc)) from exc
337- return ResponseModel(code=200, message="success", data=data)
338- 
339- 
340-@agent_router.get("/{row_id}", response_model=ResponseModel)
341-async def get_agent_policy(
342- jiuwenclaw_id: str,
343- row_id: int,
344- handler: Annotated[DBHandler, Depends(get_db_handler)],
345-):
346- svc = _agent_svc(handler)
347- try:
348- row = await svc.get(jiuwenclaw_id, row_id)
349- except ValueError as exc:
350- raise HTTPException(status_code=400, detail=str(exc)) from exc
351- if row is None:
352- raise HTTPException(status_code=404, detail="agent policy not found")
353- return ResponseModel(code=200, message="success", data=row.model_dump())
354- 
355- 
356-@agent_router.patch("/{row_id}", response_model=ResponseModel)
357-async def update_agent_policy(
358- jiuwenclaw_id: str,
359- row_id: int,
360- body: ConfigEffectiveAgentPolicyUpdateBody,
361- handler: Annotated[DBHandler, Depends(get_db_handler)],
362-):
363- svc = _agent_svc(handler)
364- try:
365- row = await svc.update(jiuwenclaw_id, row_id, body)
366- except ValueError as exc:
367- raise HTTPException(status_code=400, detail=str(exc)) from exc
368- if row is None:
369- raise HTTPException(status_code=404, detail="agent policy not found")
370- return ResponseModel(code=200, message="success", data=row.model_dump())
371- 
372- 
373-@agent_router.delete("/{row_id}", response_model=ResponseModel)
374-async def delete_agent_policy(
375- jiuwenclaw_id: str,
376- row_id: int,
377- handler: Annotated[DBHandler, Depends(get_db_handler)],
378-):
379- svc = _agent_svc(handler)
380- try:
381- ok = await svc.delete(jiuwenclaw_id, row_id)
382- except ValueError as exc:
383- raise HTTPException(status_code=400, detail=str(exc)) from exc
384- if not ok:
385- raise HTTPException(status_code=404, detail="agent policy not found")
386- return ResponseModel(
387- code=200, message="success", data={"deleted": True, "id": row_id}
388- )
389- 
390- 
391-config_effective_policy_router = APIRouter()
392-config_effective_policy_router.include_router(mapping_router)
393-config_effective_policy_router.include_router(global_router)
394-config_effective_policy_router.include_router(service_router)
395-config_effective_policy_router.include_router(agent_router)
@@ -5,7 +5,6 @@ import os
5from fastapi import APIRouter, FastAPI5from fastapi import APIRouter, FastAPI
6 6 
7from .application_config_routers import application_config_router7from .application_config_routers import application_config_router
8-from .config_effective_policy_routers import config_effective_policy_router
9from .user_console_routers import user_console_router8from .user_console_routers import user_console_router
10from .instance_access_routers import gateway_lookup_router, instance_grant_router9from .instance_access_routers import gateway_lookup_router, instance_grant_router
11from .instance_resource_routers import instance_resource_router10from .instance_resource_routers import instance_resource_router
@@ -33,11 +32,6 @@ def router_register(app: FastAPI) -> None:
33 prefix=INSTANCES_PREFIX,32 prefix=INSTANCES_PREFIX,
34 tags=["Application Config"],33 tags=["Application Config"],
35 )34 )
36- v1_router.include_router(
37- config_effective_policy_router,
38- prefix=INSTANCES_PREFIX,
39- tags=["Config Effective Policy"],
40- )
41 35 
42 # 实例准入:用户/组织 ↔ 实例授权(instance_grant)。36 # 实例准入:用户/组织 ↔ 实例授权(instance_grant)。
43 v1_router.include_router(37 v1_router.include_router(
@@ -1,301 +0,0 @@
1-"""配置生效策略与默认模板映射 API 请求/响应模型。"""
2- 
3-from __future__ import annotations
4- 
5-from typing import Annotated, Any
6- 
7-from pydantic import BaseModel, ConfigDict, Field, BeforeValidator
8- 
9-from manager_server.infrastructure.common import (
10- coerce_routing_id,
11- coerce_routing_id_optional,
12-)
13-from manager_server.infrastructure.template_ref import (
14- normalize_template_ref,
15- normalize_template_ref_optional,
16-)
17-from manager_server.schemas.template_slot_schemas import (
18- DefaultTemplateMappingTypeLiteral,
19- MappingScopeTypeLiteral,
20- TEMPLATE_REF_SLOTS,
21-)
22- 
23-TemplateRefField = Annotated[dict[str, list[str]], BeforeValidator(normalize_template_ref)]
24-OptionalTemplateRefField = Annotated[
25- dict[str, list[str]] | None,
26- BeforeValidator(normalize_template_ref_optional),
27-]
28-RoutingIdField = Annotated[str, BeforeValidator(coerce_routing_id)]
29-OptionalRoutingIdField = Annotated[str | None, BeforeValidator(coerce_routing_id_optional)]
30- 
31- 
32-# --- config_effective_agent_policy ---
33- 
34-class ConfigEffectiveAgentPolicyCreateBody(BaseModel):
35- model_config = ConfigDict(str_strip_whitespace=True)
36- 
37- policy_name: str = Field(..., max_length=128, min_length=1)
38- policy_desc: str | None = Field(default=None, max_length=512)
39- agent_id: RoutingIdField = Field(..., max_length=512)
40- workspace_dir: OptionalRoutingIdField = Field(
41- default=None,
42- max_length=512,
43- description=(
44- "数据存储目录逻辑键;可写 ${user_id}/${group_id}/${bot_id} 或固定值;"
45- "未配置时运行时默认 ${group_id}${bot_id}${user_id}"
46- ),
47- )
48- service_policy_id: str = Field(..., max_length=100, min_length=1)
49- priority: int = Field(default=0)
50- match_expr: str | None = None
51- template_ref: TemplateRefField = Field(
52- default_factory=dict,
53- description="槽位名 -> template_id 数组;元素可为 UUID 或 ${user::…}/${group::…} or <template_id>",
54- )
55- send_file_allowed: bool = True
56- enabled: bool = True
57- data: dict[str, Any] | None = None
58- 
59- 
60-class ConfigEffectiveAgentPolicyUpdateBody(BaseModel):
61- policy_name: str | None = Field(default=None, max_length=128, min_length=1)
62- policy_desc: str | None = Field(default=None, max_length=512)
63- agent_id: OptionalRoutingIdField = Field(default=None, max_length=512)
64- workspace_dir: OptionalRoutingIdField = Field(default=None, max_length=512)
65- service_policy_id: str | None = Field(default=None, max_length=100, min_length=1)
66- priority: int | None = None
67- match_expr: str | None = None
68- template_ref: OptionalTemplateRefField = None
69- send_file_allowed: bool | None = None
70- enabled: bool | None = None
71- data: dict[str, Any] | None = None
72- 
73- 
74-class ConfigEffectiveAgentPolicyOut(BaseModel):
75- id: int
76- jiuwenclaw_id: str
77- policy_id: str
78- policy_name: str
79- policy_desc: str | None
80- agent_id: str
81- workspace_dir: str | None = None
82- service_policy_id: str
83- priority: int
84- match_expr: str | None
85- template_ref: dict[str, list[str]]
86- send_file_allowed: bool
87- enabled: bool
88- data: dict[str, Any] | None
89- created_at: str | None
90- updated_at: str | None
91- 
92- 
93-class ConfigEffectiveAgentPolicyListQuery(BaseModel):
94- """Agent 层级策略列表查询参数。"""
95- 
96- page: int = Field(1, ge=1)
97- page_size: int = Field(20, ge=1, le=200)
98- service_policy_id: str | None = Field(
99- default=None,
100- description="按服务级策略 policy_id 筛选",
101- )
102- enabled: bool | None = None
103- send_file_allowed: bool | None = None
104- search: str | None = Field(
105- default=None,
106- description="搜索策略 ID、名称、描述、Agent ID、workspace_dir、关联服务策略、优先级或匹配表达式",
107- )
108- sort_by: str | None = Field(
109- default=None,
110- description=(
111- "排序字段:policy_name、policy_desc、service_policy_id、priority、"
112- "match_expr、agent_id、workspace_dir、updated_at"
113- ),
114- )
115- sort_order: str | None = Field(default=None, description="排序方向:asc、desc")
116- 
117- 
118-# --- config_effective_service_policy ---
119- 
120-class ConfigEffectiveServicePolicyCreateBody(BaseModel):
121- model_config = ConfigDict(str_strip_whitespace=True)
122- 
123- policy_name: str = Field(..., max_length=128, min_length=1)
124- policy_desc: str | None = Field(default=None, max_length=512)
125- service_id: RoutingIdField = Field(..., max_length=512)
126- priority: int
127- match_expr: str | None = None
128- template_ref: TemplateRefField = Field(default_factory=dict)
129- enabled: bool = True
130- data: dict[str, Any] | None = None
131- 
132- 
133-class ConfigEffectiveServicePolicyUpdateBody(BaseModel):
134- policy_name: str | None = Field(default=None, max_length=128, min_length=1)
135- policy_desc: str | None = Field(default=None, max_length=512)
136- service_id: OptionalRoutingIdField = Field(default=None, max_length=512)
137- priority: int | None = None
138- match_expr: str | None = None
139- template_ref: OptionalTemplateRefField = None
140- enabled: bool | None = None
141- data: dict[str, Any] | None = None
142- 
143- 
144-class ConfigEffectiveServicePolicyOut(BaseModel):
145- id: int
146- jiuwenclaw_id: str
147- policy_id: str
148- policy_name: str
149- policy_desc: str | None
150- service_id: str
151- priority: int
152- match_expr: str | None
153- template_ref: dict[str, list[str]]
154- enabled: bool
155- data: dict[str, Any] | None
156- created_at: str | None
157- updated_at: str | None
158- 
159- 
160-class ConfigEffectiveServicePolicyListQuery(BaseModel):
161- """服务级策略列表查询参数。"""
162- 
163- page: int = Field(1, ge=1)
164- page_size: int = Field(20, ge=1, le=200)
165- enabled: bool | None = None
166- search: str | None = Field(
167- default=None,
168- description="搜索策略 ID、名称、描述、服务 ID、优先级或匹配表达式",
169- )
170- sort_by: str | None = Field(
171- default=None,
172- description="排序字段:policy_name、policy_desc、priority、match_expr、service_id、updated_at",
173- )
174- sort_order: str | None = Field(default=None, description="排序方向:asc、desc")
175- 
176- 
177-# --- config_effective_global_policy ---
178- 
179-class ConfigEffectiveGlobalPolicyCreateBody(BaseModel):
180- model_config = ConfigDict(str_strip_whitespace=True)
181- 
182- policy_name: str = Field(..., max_length=128, min_length=1)
183- policy_desc: str | None = Field(default=None, max_length=512)
184- priority: int = 0
185- template_ref: TemplateRefField = Field(default_factory=dict)
186- enabled: bool = True
187- data: dict[str, Any] | None = None
188- 
189- 
190-class ConfigEffectiveGlobalPolicyUpdateBody(BaseModel):
191- policy_name: str | None = Field(default=None, max_length=128, min_length=1)
192- policy_desc: str | None = Field(default=None, max_length=512)
193- priority: int | None = None
194- template_ref: OptionalTemplateRefField = None
195- enabled: bool | None = None
196- data: dict[str, Any] | None = None
197- 
198- 
199-class ConfigEffectiveGlobalPolicyOut(BaseModel):
200- id: int
201- jiuwenclaw_id: str
202- policy_id: str
203- policy_name: str
204- policy_desc: str | None
205- priority: int
206- template_ref: dict[str, list[str]]
207- enabled: bool
208- data: dict[str, Any] | None
209- created_at: str | None
210- updated_at: str | None
211- 
212- 
213-class ConfigEffectiveGlobalPolicyListQuery(BaseModel):
214- """全局兜底策略列表查询参数。"""
215- 
216- page: int = Field(1, ge=1)
217- page_size: int = Field(20, ge=1, le=200)
218- enabled: bool | None = None
219- search: str | None = Field(default=None, description="搜索策略 ID、名称、描述或优先级")
220- sort_by: str | None = Field(
221- default=None,
222- description="排序字段:policy_name、policy_desc、priority、updated_at",
223- )
224- sort_order: str | None = Field(default=None, description="排序方向:asc、desc")
225- 
226- 
227-# --- config_default_template_mapping ---
228- 
229- 
230-class ConfigDefaultTemplateMappingCreateBody(BaseModel):
231- model_config = ConfigDict(str_strip_whitespace=True)
232- 
233- policy_name: str = Field(..., max_length=128, min_length=1)
234- policy_desc: str | None = Field(default=None, max_length=512)
235- scope_type: MappingScopeTypeLiteral
236- scope_id: str = Field(..., max_length=512, min_length=1)
237- priority: int = 0
238- template_id: str = Field(..., max_length=100, min_length=1)
239- template_type: DefaultTemplateMappingTypeLiteral
240- enabled: bool = True
241- data: dict[str, Any] | None = None
242- 
243- 
244-class ConfigDefaultTemplateMappingUpdateBody(BaseModel):
245- model_config = ConfigDict(str_strip_whitespace=True)
246- 
247- policy_name: str | None = Field(default=None, max_length=128, min_length=1)
248- policy_desc: str | None = Field(default=None, max_length=512)
249- scope_type: MappingScopeTypeLiteral | None = None
250- scope_id: str | None = Field(default=None, max_length=512, min_length=1)
251- priority: int | None = None
252- template_id: str | None = Field(default=None, max_length=100, min_length=1)
253- template_type: DefaultTemplateMappingTypeLiteral | None = None
254- enabled: bool | None = None
255- data: dict[str, Any] | None = None
256- 
257- 
258-class ConfigDefaultTemplateMappingOut(BaseModel):
259- id: int
260- jiuwenclaw_id: str
261- policy_id: str
262- policy_name: str
263- policy_desc: str | None
264- scope_type: MappingScopeTypeLiteral
265- scope_id: str
266- priority: int
267- template_id: str
268- template_type: DefaultTemplateMappingTypeLiteral
269- enabled: bool
270- data: dict[str, Any] | None
271- created_at: str | None
272- updated_at: str | None
273- 
274- 
275-class ConfigDefaultTemplateMappingListQuery(BaseModel):
276- """默认模板映射列表查询参数。"""
277- 
278- page: int = Field(1, ge=1)
279- page_size: int = Field(20, ge=1, le=200)
280- scope_type: MappingScopeTypeLiteral | None = Field(
281- default=None, description="按 scope_type 精确筛选"
282- )
283- scope_id: str | None = Field(default=None, description="按 scope_id 精确筛选")
284- template_type: DefaultTemplateMappingTypeLiteral | None = Field(
285- default=None,
286- description=f"模板类型:{' / '.join(sorted(TEMPLATE_REF_SLOTS))}",
287- )
288- template_id: str | None = Field(default=None, description="按 template_id 精确筛选")
289- enabled: bool | None = None
290- search: str | None = Field(
291- default=None,
292- description="搜索策略 ID、名称、描述、作用域类型、作用域 ID、槽位、模板 ID 或优先级",
293- )
294- sort_by: str | None = Field(
295- default=None,
296- description=(
297- "排序字段:policy_name、policy_desc、priority、scope_type、scope_id、"
298- "template_type、template_id、updated_at"
299- ),
300- )
301- sort_order: str | None = Field(default=None, description="排序方向:asc、desc")
@@ -81,9 +81,9 @@ def test_has_sensitive():
81 81 
82 82 
83def test_unregistered_section_passthrough():83def test_unregistered_section_passthrough():
84- body = {"op": "create", "mapping": {"scope_type": "user", "scope_id": "u1"}}84+ body = {"op": "create", "template": {"name": "x"}}
85 enc_body, fields = encrypt_sensitive_fields(85 enc_body, fields = encrypt_sensitive_fields(
86- "config_default_template_mappings", body, new_dek()86+ "unknown_section_name", body, new_dek()
87 )87 )
88 assert fields == []88 assert fields == []
89 assert enc_body == body89 assert enc_body == body
Mapplications/manager/manager_web/src/i18n/locales/en.json+5-71文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_web/src/i18n/locales/zh.json+5-71文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_web/src/services/api.ts+0-173文件内容审核中,请稍后刷新重试