feat(v2): 完善配置中心与可替换轨迹质检并联动预审依据
This commit is contained in:
@@ -20,6 +20,9 @@ router = APIRouter(prefix='/api/xingbang/risk-agent/project-assistant/library',
|
||||
def service():
|
||||
svc = ProjectAssistantService()
|
||||
library.initialize(svc.db_path)
|
||||
from services.catalog_configuration import seed_fact_source
|
||||
with svc.connect() as db:
|
||||
seed_fact_source(db)
|
||||
return svc
|
||||
|
||||
|
||||
@@ -125,10 +128,15 @@ def cards(project_id: str, request: Request):
|
||||
with service().connect() as db:
|
||||
project(db, request, project_id)
|
||||
released = [registry.resolve(db, p['id']) for p in store.listing(db, 'release')]
|
||||
drafts = store.listing(db, 'component_draft', project_id)
|
||||
published = {(r['id'],r['version']) for r in released}
|
||||
drafts = [r for r in store.listing(db, 'component_draft', project_id) if (r.get('component_id'),r['version']) not in published]
|
||||
items = [r for r in released + drafts if r.get('scope') == 'demo'
|
||||
and r.get('kind') in ('skill', 'rule_set', 'data_card')
|
||||
and r.get('kind') in ('skill', 'rule_set', 'data_card', 'template')
|
||||
and r.get('project_id') in (None, '', project_id) and r.get('id') != 'review.analysis']
|
||||
from services.catalog_configuration import hidden
|
||||
items = [dict(r, hidden=hidden(db, r.get('component_id') or r['id'])) for r in items if r.get('audience') != ['reviewer']]
|
||||
if request.state.identity['role'] != 'admin':
|
||||
items = [r for r in items if r['status']=='published' and not r['hidden']]
|
||||
for row in items:
|
||||
if row['kind'] == 'skill':
|
||||
row['package'] = skill_packages.standard_package(db, row)
|
||||
@@ -210,7 +218,7 @@ def plan_components(plan_id: str, project_id: str, request: Request):
|
||||
if frozen['plan'].get('project_id'):
|
||||
check_project(request, frozen['plan']['project_id'])
|
||||
return {'plan': frozen['plan'], 'skills': frozen['skills'], 'rules': frozen['rules'],
|
||||
'cards': frozen.get('legacy_cards', []), 'content_hash': frozen['content_hash']}
|
||||
'cards': frozen.get('legacy_cards', []), 'template': frozen['template'], 'content_hash': frozen['content_hash']}
|
||||
return translate(action)
|
||||
|
||||
@router.get('/model-settings')
|
||||
|
||||
@@ -815,9 +815,9 @@ def propose_knowledge_candidate(project_id: str, payload: CandidateInput, reques
|
||||
return blocked
|
||||
|
||||
def action():
|
||||
role(request, ("admin",))
|
||||
with _db() as db:
|
||||
project = _project(db, project_id)
|
||||
# 组件 id 由服务端生成:调用方不该靠自报 id 影响治理(03 §9.4 同口径)
|
||||
body = dict(payload.content, kind=payload.kind, name=payload.title,
|
||||
id=store.uid(payload.kind + ".card"), version=payload.version,
|
||||
scope=project["data_domain"])
|
||||
@@ -840,13 +840,13 @@ def revise_knowledge_candidate(card_id: str, payload: ReviseInput, request: Requ
|
||||
不该占着 SQLite 连接。
|
||||
"""
|
||||
def action():
|
||||
role(request, ("admin",))
|
||||
with _db() as db:
|
||||
draft = knowledge_cards.current_draft(db, card_id)
|
||||
card = knowledge_cards.draft_card(db, draft["id"])
|
||||
blocked = scope(request, draft["project_id"])
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
# 模型调用放在数据库连接之外:自然语言解释可能耗时数十秒,不该占着 SQLite 连接
|
||||
changes, note = experience_extraction.edit_changes(card, payload.model_dump())
|
||||
with _db() as db:
|
||||
return knowledge_cards.revise(db, card_id, changes, _actor(request), note,
|
||||
@@ -872,13 +872,13 @@ def transition_knowledge_candidate(card_id: str, payload: TransitionInput, reque
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
if payload.action == "evaluate":
|
||||
# 角色门禁与既有 /components/{id}/actions 一致:新路由不得比既有入口宽松
|
||||
role(request, ("admin", "reviewer"))
|
||||
# 新版配置仅管理员处理;独立发布人与提议人须不同
|
||||
role(request, ("admin",))
|
||||
from services.component_evaluation import evaluate
|
||||
with _db() as db:
|
||||
return evaluate(db, draft["id"], _actor(request))
|
||||
if payload.action == "publish":
|
||||
role(request, ("admin", "reviewer"))
|
||||
role(request, ("admin",))
|
||||
with _db() as db:
|
||||
return knowledge_cards.publish(db, draft["id"], _actor(request),
|
||||
request.state.identity.get("preview", False))
|
||||
@@ -909,7 +909,6 @@ def list_review_plans(project_id: str, request: Request):
|
||||
def propose_review_plan(project_id: str, payload: ReviewPlanInput, request: Request):
|
||||
"""装配一份方案草稿。
|
||||
|
||||
这是"已发布卡片被选入方案"的入口:`skills`/`rules` 里填卡片组件 id 即可,
|
||||
但草稿或已撤回的组件会在装配与发布两处被拒(BR-27 / AC-28)。
|
||||
"""
|
||||
blocked = scope(request, project_id)
|
||||
@@ -917,6 +916,7 @@ def propose_review_plan(project_id: str, payload: ReviewPlanInput, request: Requ
|
||||
return blocked
|
||||
|
||||
def action():
|
||||
role(request, ("admin",))
|
||||
with _db() as db:
|
||||
project = _project(db, project_id)
|
||||
body = dict(payload.model_dump(), kind="plan", scope=project["data_domain"],
|
||||
@@ -938,12 +938,12 @@ def transition_review_plan(plan_ref: str, payload: TransitionInput, request: Req
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
if payload.action == "evaluate":
|
||||
role(request, ("admin", "reviewer"))
|
||||
role(request, ("admin",))
|
||||
from services.component_evaluation import evaluate
|
||||
with _db() as db:
|
||||
return evaluate(db, draft["id"], _actor(request))
|
||||
if payload.action == "publish":
|
||||
role(request, ("admin", "reviewer"))
|
||||
role(request, ("admin",))
|
||||
with _db() as db:
|
||||
return registry.release(db, draft["id"], _actor(request),
|
||||
request.state.identity.get("preview", False))
|
||||
|
||||
@@ -331,7 +331,7 @@ def business_command(payload: CommandInput, request: Request):
|
||||
source = next((r for r in project_body["runs"] if r.get("run_kind", "pre_review") == run_kind and r["status"] in ("completed", "partial")), None)
|
||||
if not source:
|
||||
raise HTTPException(409, "请先完成对应的mini-openclaw运行")
|
||||
template_id = "template.independent-review" if current_role == "reviewer" else "template.ai-pre-review"
|
||||
template_id = source.get("report_template") or ("template.independent-review" if current_role == "reviewer" else "template.ai-pre-review")
|
||||
report_body = translate(lambda: svc.create_report(payload.project_id, source["id"], request.state.identity["id"], template_id, key))
|
||||
return {"intent": "create_report", "message": "报告草稿已生成,来源版本已冻结。", "object_id": report_body["id"], "url": f"/xingbang-workbench?project={payload.project_id}&report={report_body['id']}"}
|
||||
if "提交" in message and "评审" in message:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Scoped configuration and read-only trajectory quality API (BR72–78)."""
|
||||
from typing import Literal
|
||||
from fastapi import APIRouter,Depends,Request,HTTPException
|
||||
from pydantic import BaseModel,ConfigDict,Field
|
||||
from api.risk_agent_access import authorize
|
||||
from api.project_assistant import enabled
|
||||
from api.lease_library import service,guard,project,card,translate as library_translate
|
||||
from fastapi.responses import JSONResponse
|
||||
from services import assistant_store as store,catalog_configuration as catalog,trajectory_quality as quality
|
||||
router=APIRouter(prefix='/api/xingbang/risk-agent/project-assistant/library',dependencies=[Depends(authorize),Depends(enabled)])
|
||||
def translate(call):
|
||||
def checked():
|
||||
try:return call()
|
||||
except ValueError as exc:
|
||||
text=str(exc)
|
||||
if 'COMPONENT_HIDDEN' in text:return JSONResponse(status_code=409,content={'error':{'code':'COMPONENT_HIDDEN','message':'评审方案存在隐藏组件,请更新配置后重试'}})
|
||||
return JSONResponse(status_code=422,content={'error':{'code':'CONFIGURATION_INVALID','message':'配置不合法,请检查必填项与受信模型地址'}})
|
||||
return library_translate(checked)
|
||||
|
||||
class Input(BaseModel):model_config=ConfigDict(extra='forbid')
|
||||
class Visibility(Input):hidden:bool
|
||||
class CardInput(Input):
|
||||
project_id:str=Field(min_length=1,max_length=180)
|
||||
kind:Literal['data_card','template']
|
||||
name:str=Field(min_length=1,max_length=120)
|
||||
content:dict
|
||||
class Query(Input):
|
||||
project_id:str=Field(min_length=1,max_length=180)
|
||||
message:str=Field(min_length=1,max_length=1000)
|
||||
class Settings(Input):
|
||||
provider:Literal['disabled','local','jev']
|
||||
endpoint:str=Field(default='',max_length=500)
|
||||
model:str=Field(default='',max_length=160)
|
||||
api_key:str|None=Field(default=None,max_length=4000)
|
||||
allow_external:bool=False
|
||||
class RunInput(Input):run_id:str=Field(min_length=1,max_length=180)
|
||||
@router.patch('/cards/{card_id}/visibility')
|
||||
def visibility(card_id:str,payload:Visibility,request:Request):
|
||||
actor=guard(request,True)
|
||||
def action():
|
||||
with service().connect() as db:return catalog.set_visibility(db,card(db,request,card_id),actor,payload.hidden)
|
||||
return translate(action)
|
||||
@router.post('/configuration/cards',status_code=201)
|
||||
def create_card(payload:CardInput,request:Request):
|
||||
actor=guard(request,True)
|
||||
def action():
|
||||
with service().connect() as db:return catalog.create(db,project(db,request,payload.project_id),actor,payload.kind,payload.name,payload.content)
|
||||
return translate(action)
|
||||
@router.post('/inventory')
|
||||
def inventory(payload:Query,request:Request):
|
||||
guard(request)
|
||||
def action():
|
||||
with service().connect() as db:
|
||||
context=project(db,request,payload.project_id)
|
||||
return catalog.answer(db,context['id'],context['data_domain'],request.state.identity['role'],payload.message)
|
||||
return translate(action)
|
||||
@router.get('/quality/settings')
|
||||
def settings(request:Request):
|
||||
guard(request,True);return quality.public()
|
||||
@router.put('/quality/settings')
|
||||
def save_settings(payload:Settings,request:Request):
|
||||
actor=guard(request,True)
|
||||
def action():
|
||||
result=quality.save(**payload.model_dump())
|
||||
with service().connect() as db:store.event(db,'','quality.settings_updated',actor,{'provider':result['provider'],'revision':result['revision']})
|
||||
return result
|
||||
return translate(action)
|
||||
@router.post('/quality/probe')
|
||||
def probe(request:Request):
|
||||
guard(request,True);return quality.probe()
|
||||
@router.get('/projects/{project_id}/quality')
|
||||
def history(project_id:str,request:Request):
|
||||
guard(request)
|
||||
def action():
|
||||
with service().connect() as db:
|
||||
project(db,request,project_id)
|
||||
return {'items':store.listing(db,'quality_review',project_id)}
|
||||
return translate(action)
|
||||
@router.post('/projects/{project_id}/quality',status_code=201)
|
||||
def inspect(project_id:str,payload:RunInput,request:Request):
|
||||
actor=guard(request)
|
||||
def action():
|
||||
svc=service()
|
||||
with svc.connect() as db:
|
||||
project(db,request,project_id);run=store.get(db,'run',payload.run_id)
|
||||
if run['project_id']!=project_id:raise KeyError('运行不可访问')
|
||||
result=quality.inspect(run)
|
||||
with svc.connect() as db:
|
||||
store.put(db,'quality_review',result,immutable=True)
|
||||
store.event(db,project_id,'trajectory.quality',actor,{'review_id':result['id'],'run_id':payload.run_id,'status':result['judgment']['status']})
|
||||
return result
|
||||
return translate(action)
|
||||
|
||||
@router.get('/configuration/sources')
|
||||
def sources(project_id:str,request:Request):
|
||||
guard(request,True)
|
||||
def action():
|
||||
from services import component_registry as registry
|
||||
with service().connect() as db:
|
||||
context=project(db,request,project_id)
|
||||
rows=[registry.resolve(db,p['id']) for p in store.listing(db,'release')]
|
||||
return {'items':[{'id':r['id'],'name':r['name'],'type':r['type']} for r in rows if r.get('kind')=='data_source' and r.get('scope')==context['data_domain'] and r.get('project_id') in (None,'',project_id) and not catalog.hidden(db,r['id'])]}
|
||||
return translate(action)
|
||||
|
||||
@router.get('/projects/{project_id}/runs/{run_id}/influences')
|
||||
def influences(project_id:str,run_id:str,request:Request):
|
||||
guard(request)
|
||||
def action():
|
||||
from services.prescreen_explanation import explain
|
||||
with service().connect() as db:
|
||||
project(db,request,project_id);run=store.get(db,'run',run_id)
|
||||
if run.get('project_id')!=project_id:raise KeyError('运行不可访问')
|
||||
view=store.get(db,'prescreen_run',run_id)
|
||||
return explain(run,view)
|
||||
return translate(action)
|
||||
Reference in New Issue
Block a user