袁聪的第二次提交,项目已完整
This commit is contained in:
+114
-14
@@ -8,7 +8,14 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.contracts import RequestContext
|
||||
from app.core.errors import ConflictAgentError, ResourceNotFoundError, ValidationAgentError
|
||||
from app.core.cursor import parse_cursor
|
||||
from app.core.errors import (
|
||||
GenericResourceNotFoundError,
|
||||
InvalidStateError,
|
||||
ResourceAlreadyExistsError,
|
||||
ResourceVersionConflictError,
|
||||
ValidationAgentError,
|
||||
)
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.model.audit import InteractionAudit
|
||||
from app.model.platform import DomainEventOutbox
|
||||
@@ -30,6 +37,31 @@ RESOURCES = {
|
||||
"audit-records": "interaction_audit",
|
||||
}
|
||||
|
||||
# 行情 namespace 的允许字段。必须与 `FundQuoteRuntimeConfig.from_mapping` 的
|
||||
# 接受范围保持一致,否则会出现"配置能发布、运行期却被静默忽略"的假成功。
|
||||
# 该 namespace 由 `RuntimeConfigService.fund_quote` 以
|
||||
# namespace=fund_market / config_key=default 读取。
|
||||
FUND_MARKET_FIELDS = frozenset({"allowed_codes", "intraday_cache_ttl", "closing_cache_ttl"})
|
||||
|
||||
|
||||
def validate_fund_market(values: dict[str, Any]) -> None:
|
||||
"""校验行情配置取值。
|
||||
|
||||
行情配置不是 1-100 的整数表:`allowed_codes` 是六位代码字符串列表,
|
||||
TTL 上限 86400 与运行期接受范围一致(`FundQuoteRuntimeConfig`)。
|
||||
"""
|
||||
codes = values.get("allowed_codes", [])
|
||||
if not isinstance(codes, list) or not all(
|
||||
isinstance(code, str) and len(code) == 6 and code.isdigit() for code in codes
|
||||
):
|
||||
raise ValidationAgentError("行情基金代码必须为六位数字字符串列表")
|
||||
for name in ("intraday_cache_ttl", "closing_cache_ttl"):
|
||||
if name not in values:
|
||||
continue
|
||||
ttl = values[name]
|
||||
if type(ttl) is not int or not 1 <= ttl <= 86400:
|
||||
raise ValidationAgentError(f"{name} 必须为 1-86400 之间的整数")
|
||||
|
||||
|
||||
def public(value: Any, key: str = "") -> Any:
|
||||
if isinstance(value, dict):
|
||||
@@ -57,17 +89,21 @@ class AdminService:
|
||||
row_id: int | None = None,
|
||||
release_id: int | None = None,
|
||||
limit: int = 20,
|
||||
before: int | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
permission = "audit:read" if resource == "audit-records" else "config:read"
|
||||
await AuthorizationService.require(context, permission, admin=True)
|
||||
# 游标校验必须在权限闸门**之后**:未授权调用一律 403,不能因为参数格式先漏出
|
||||
# 一个 400——响应差异本身就是一条越权探测信号(文档 §4.2)。校验同时早于
|
||||
# 任何数据库访问,非法游标不会变成一次静默的全量查询。
|
||||
before = parse_cursor(cursor)
|
||||
async with SessionFactory() as session:
|
||||
repo = PlatformRepository(session)
|
||||
filters = {"release_id": release_id} if release_id is not None else {}
|
||||
if row_id is not None:
|
||||
row = await repo.get(RESOURCES[resource], row_id)
|
||||
if row is None or any(row[k] != v for k, v in filters.items()):
|
||||
raise ResourceNotFoundError("资源不存在")
|
||||
raise GenericResourceNotFoundError("资源不存在")
|
||||
return {
|
||||
"data": public(row),
|
||||
"meta": {"trace_id": context.trace_id, "etag": digest(row)},
|
||||
@@ -104,9 +140,9 @@ class AdminService:
|
||||
if existing is None or (
|
||||
release_id is not None and existing.get("release_id") != release_id
|
||||
):
|
||||
raise ResourceNotFoundError("资源不存在")
|
||||
raise GenericResourceNotFoundError("资源不存在")
|
||||
if if_match is None or if_match.strip('"') != digest(existing):
|
||||
raise ConflictAgentError("RESOURCE_VERSION_CONFLICT")
|
||||
raise ResourceVersionConflictError("If-Match 版本不一致")
|
||||
if action != "write":
|
||||
assert existing is not None
|
||||
row = await self._transition(repo, resource, existing, action, values, context)
|
||||
@@ -141,7 +177,7 @@ class AdminService:
|
||||
operation,
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
raise ConflictAgentError("资源唯一性、引用或状态约束冲突") from exc
|
||||
raise ResourceAlreadyExistsError("资源唯一性、引用或状态约束冲突") from exc
|
||||
|
||||
async def _write(
|
||||
self,
|
||||
@@ -154,13 +190,13 @@ class AdminService:
|
||||
) -> dict[str, Any]:
|
||||
data = dict(values)
|
||||
if existing is not None and existing.get("status", "draft") not in {"draft", "disabled"}:
|
||||
raise ConflictAgentError("只能编辑草稿或停用版本")
|
||||
raise InvalidStateError("只能编辑草稿或停用版本")
|
||||
if release_id is not None:
|
||||
data["release_id"] = release_id
|
||||
if "release_id" in data:
|
||||
parent = await repo.get("config_release", int(data["release_id"]), lock=True)
|
||||
if parent is None or parent["status"] != "draft":
|
||||
raise ConflictAgentError("只能修改草稿发布批次")
|
||||
raise InvalidStateError("只能修改草稿发布批次")
|
||||
if resource == "platform-config-items":
|
||||
data["config_key"] = data.pop("item_key")
|
||||
allowed = {
|
||||
@@ -168,6 +204,7 @@ class AdminService:
|
||||
"relationship": {"max_hops", "limit"},
|
||||
"runtime": {"timeout_seconds"},
|
||||
"agent_tools": {"allowed_tools"},
|
||||
"fund_market": set(FUND_MARKET_FIELDS),
|
||||
}
|
||||
if set(data["value_json"]) - allowed[data["namespace"]]:
|
||||
raise ValidationAgentError("配置包含未声明字段")
|
||||
@@ -260,8 +297,11 @@ class AdminService:
|
||||
result = await service.submit_for_review(row["id"], actor)
|
||||
elif action == "reviews":
|
||||
if values["decision"] == "rejected":
|
||||
if row["status"] != "pending_review" or row["created_by"] == actor:
|
||||
raise ConflictAgentError("不满足双人审核条件")
|
||||
# 允许创建人自审(不再要求 reviewer 与 created_by 不同);仍然强制审核节点。
|
||||
# 自审同样如实写入 reviewer_id:该列含义就是"审核人",迁移
|
||||
# 20260911_drop_review_separation 已撤下"审核人≠创建人"的检查约束。
|
||||
if row["status"] != "pending_review":
|
||||
raise InvalidStateError("只能驳回待审核版本")
|
||||
return await repo.update(
|
||||
"config_release",
|
||||
row["id"],
|
||||
@@ -279,24 +319,84 @@ class AdminService:
|
||||
refreshed = await repo.get("config_release", result.id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
if resource == "agent-intent-configs":
|
||||
return await self._transition_intent_config(repo, row, action, values, actor, now)
|
||||
if action == "reviews":
|
||||
if row["status"] != "draft" or row["created_by"] == actor:
|
||||
raise ConflictAgentError("不满足双人审核条件")
|
||||
# 审核节点不可跳过(必须先是 draft);但允许创建人自审。
|
||||
if row["status"] != "draft":
|
||||
raise InvalidStateError("只能审核草稿版本")
|
||||
status = "approved" if values["decision"] == "approved" else "draft"
|
||||
update_values = {"status": status, "reviewer_id": actor, "reviewed_at": now}
|
||||
elif action == "activations":
|
||||
if row["status"] != "approved" or row["reviewer_id"] is None:
|
||||
raise ConflictAgentError("版本未审核")
|
||||
raise InvalidStateError("版本未审核")
|
||||
update_values = {"status": "active"}
|
||||
elif action == "disablements":
|
||||
if row["status"] != "active":
|
||||
raise ConflictAgentError("只能停用激活版本")
|
||||
raise InvalidStateError("只能停用激活版本")
|
||||
update_values = {"status": "disabled"}
|
||||
else:
|
||||
raise ValidationAgentError("未知状态操作")
|
||||
update_values["updated_at"] = now
|
||||
return await repo.update(RESOURCES[resource], row["id"], update_values)
|
||||
|
||||
async def _transition_intent_config(
|
||||
self,
|
||||
repo: PlatformRepository,
|
||||
row: dict[str, Any],
|
||||
action: str,
|
||||
values: dict[str, Any],
|
||||
actor: int,
|
||||
now: datetime,
|
||||
) -> dict[str, Any]:
|
||||
"""意图配置自己的状态机:`draft -> approved -> active -> archived`。
|
||||
|
||||
`agent_intent_config.status` 有 CHECK 约束,只允许
|
||||
`draft/approved/active/archived`(**没有 `disabled`**),所以这里不能套用
|
||||
`model-endpoints` 的 `disablements` 语义,归档落到 `archived`。运行期仍以
|
||||
`status='active'` 判定生效(`RuntimeConfigService.active_intents`),口径一致。
|
||||
|
||||
同一 `agent_type:intent_code` 只能有一个 active 版本(生成列唯一键
|
||||
`uk_intent_config_active_one`),激活前先把旧版本归档,语义与
|
||||
`ConfigReleaseService.activate` 挂起上一个生效版本一致。
|
||||
"""
|
||||
if action == "reviews":
|
||||
if row["status"] != "draft":
|
||||
raise InvalidStateError("只能审核草稿意图配置")
|
||||
update_values: dict[str, Any] = {
|
||||
"status": "approved" if values["decision"] == "approved" else "draft",
|
||||
"reviewer_id": actor,
|
||||
"reviewed_at": now,
|
||||
}
|
||||
elif action == "activations":
|
||||
if row["status"] != "approved" or row["reviewer_id"] is None:
|
||||
raise InvalidStateError("意图配置未审核")
|
||||
expire_at = row.get("expire_at")
|
||||
if isinstance(expire_at, datetime) and expire_at <= now:
|
||||
raise InvalidStateError("意图配置已过期,不能激活")
|
||||
for previous in await repo.rows(
|
||||
"agent_intent_config",
|
||||
{"agent_type": row["agent_type"], "status": "active"},
|
||||
limit=10,
|
||||
):
|
||||
if previous["id"] != row["id"]:
|
||||
await repo.update(
|
||||
"agent_intent_config",
|
||||
previous["id"],
|
||||
{"status": "archived", "updated_at": now},
|
||||
)
|
||||
update_values = {"status": "active"}
|
||||
if row.get("effective_at") is None:
|
||||
update_values["effective_at"] = now
|
||||
elif action == "archivals":
|
||||
if row["status"] != "active":
|
||||
raise InvalidStateError("只能归档生效版本")
|
||||
update_values = {"status": "archived"}
|
||||
else:
|
||||
raise ValidationAgentError("未知状态操作")
|
||||
update_values["updated_at"] = now
|
||||
return await repo.update("agent_intent_config", row["id"], update_values)
|
||||
|
||||
async def _validate_release(self, repo: PlatformRepository, release_id: int) -> None:
|
||||
rules = await repo.rows("model_routing_rule", {"release_id": release_id}, limit=10000)
|
||||
for rule in rules:
|
||||
|
||||
Reference in New Issue
Block a user