袁聪的第二次提交,项目已完整

This commit is contained in:
2026-09-11 16:57:47 +08:00
parent 5907fcd6d2
commit fd9598efdf
190 changed files with 31513 additions and 680 deletions
+114 -14
View File
@@ -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: