Files
mini-openclaw-v2/backend/api/lease_library.py
T

215 lines
9.2 KiB
Python

"""SQLite lease corpus / standard Skill packages API (BR-61..65)."""
import os
from urllib.parse import urlsplit
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from api.risk_agent_access import authorize, check_project
from api.project_assistant import enabled
from contracts.lease_library import QueryInput, SelectionInput, ExtractionInput, PackageInput
from services.project_assistant import ProjectAssistantService
from services import assistant_store as store, component_registry as registry
from services import lease_case_library as library, lease_extraction, skill_packages
from services.assistant_materials import FIELDS
from services.assistant_settings import flags
from services import experience_extraction
router = APIRouter(prefix='/api/xingbang/risk-agent/project-assistant/library',
dependencies=[Depends(authorize), Depends(enabled)], tags=['兴邦对公租赁经验库'])
def service():
svc = ProjectAssistantService()
library.initialize(svc.db_path)
return svc
def guard(request, editing=False):
identity = request.state.identity
if identity['role'] not in ('manager', 'admin') or (editing and identity['role'] != 'admin'):
raise HTTPException(403, '当前身份无权进行经验加工')
if not identity.get('demo') or not flags()['demo']:
raise HTTPException(403, '本案例库属于模拟数据域,请使用本机演示身份')
return identity['id']
def project(db, request, project_id):
check_project(request, project_id)
body = store.get(db, 'project', project_id)
if body['data_domain'] != 'demo':
raise HTTPException(403, '模拟历史经验不能写入真实数据域')
return body
def translate(call):
try:
return call()
except library.LibraryError as exc:
code = 409 if exc.code == 'LIBRARY_SELECTION_INVALID' else 422
return JSONResponse(status_code=code, content={'error': {'code': exc.code, 'message': str(exc), 'retryable': False}})
except KeyError:
raise HTTPException(404, '对象不可访问或不存在') from None
except store.Conflict as exc:
raise HTTPException(409, str(exc)) from exc
except (ValueError, PermissionError) as exc:
raise HTTPException(422 if isinstance(exc, ValueError) else 403, str(exc)) from exc
@router.get('/cases')
def cases(request: Request):
guard(request)
def action():
with service().connect() as db:
rows = library.read_query(db, 'SELECT * FROM lease_cases ORDER BY id')
return {'items': rows, 'count': len(rows), 'total': len(rows), 'schema': list(library.COLUMNS),
'source': library.SOURCE, 'model_available': experience_extraction.resolve_model_config(None).get('available', False)}
return translate(action)
@router.post('/queries')
def query(payload: QueryInput, request: Request):
actor = guard(request, True)
def action():
with service().connect() as db:
return library.preview(db, actor, payload.mode, payload.text, payload.filters)
return translate(action)
@router.post('/selections', status_code=201)
def selection(payload: SelectionInput, request: Request):
actor = guard(request, True)
def action():
with service().connect() as db:
return library.confirm(db, actor, payload.preview_id, payload.selected_ids)
return translate(action)
@router.post('/extractions', status_code=201)
def extraction(payload: ExtractionInput, request: Request):
actor = guard(request, True)
def action():
svc = service()
with svc.connect() as db:
context = project(db, request, payload.project_id)
confirmed, items = library.selected_cases(db, actor, payload.selection_id)
proposal, trace, failure = None, None, None
try:
proposal, trace = lease_extraction.invoke(context, confirmed, items, payload.kind, payload.requirement)
except Exception:
# Do not return model HTTP bodies/credentials to the browser.
failure = '模型未配置、调用未完成或输出未通过契约校验'
with svc.connect() as db:
library.selected_cases(db, actor, payload.selection_id)
return lease_extraction.save(db, context, confirmed, items, actor, payload.kind,
payload.requirement, proposal, trace, failure)
return translate(action)
def card(db, request, card_id):
try:
row = store.get(db, 'component_draft' if '@' in card_id else 'release', card_id)
if '@' not in card_id:
row = registry.resolve(db, card_id)
except KeyError:
row = store.get(db, 'component_version', card_id)
if row.get('scope') != 'demo':
raise HTTPException(403, '卡片数据域不可访问')
if row.get('project_id'):
check_project(request, row['project_id'])
return row
@router.get('/cards')
def cards(project_id: str, request: Request):
guard(request)
def action():
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)
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('project_id') in (None, '', project_id) and r.get('id') != 'review.analysis']
for row in items:
if row['kind'] == 'skill':
row['package'] = skill_packages.standard_package(db, row)
return {'items': items, 'fields': sorted(set(FIELDS.values()))}
return translate(action)
@router.get('/cards/{card_id}')
def card_detail(card_id: str, request: Request):
guard(request)
def action():
with service().connect() as db:
return card(db, request, card_id)
return translate(action)
@router.post('/skills/import', status_code=201)
def import_skill(payload: PackageInput, request: Request):
actor = guard(request, True)
def action():
with service().connect() as db:
context = project(db, request, payload.project_id)
return skill_packages.import_package(db, context, actor,
[f.model_dump() for f in payload.files], payload.required_field)
return translate(action)
@router.get('/skills/{card_id}/package')
def package(card_id: str, request: Request):
guard(request)
def action():
with service().connect() as db:
component = card(db, request, card_id)
if component['kind'] != 'skill':
raise ValueError('仅 Skill 卡片具有标准文件包')
return skill_packages.content(db, component)
return translate(action)
@router.get('/skills/{card_id}/download')
def download(card_id: str, request: Request):
guard(request)
def action():
with service().connect() as db:
component = card(db, request, card_id)
if component['kind'] != 'skill':
raise ValueError('仅 Skill 可下载标准包')
name, binary = skill_packages.archive(db, component)
return Response(binary, media_type='application/zip', headers={'Content-Disposition': f'attachment; filename="{name}.zip"'})
return translate(action)
@router.post('/skills/{card_id}/open')
def open_package(card_id: str, request: Request):
actor = guard(request, True)
hostname = urlsplit('//'+request.headers.get('x-xingbang-client-host', request.headers.get('host', ''))).hostname
local_demo = request.client and request.client.host in ('127.0.0.1', '::1', 'testclient') and hostname in ('127.0.0.1', 'localhost', '::1', 'testserver') and not os.getenv('XINGBANG_AUTH_USERS') and os.getenv('XINGBANG_REQUIRE_AUTH') != '1'
if not (request.state.identity.get('preview') or local_demo):
raise HTTPException(403, '打开文件夹仅支持已确认的本机预览;其他部署请下载标准包')
def action():
with service().connect() as db:
result = skill_packages.open_folder(db, card(db, request, card_id))
store.event(db, '', 'skill.folder_open', actor, {'card_id': card_id})
return result
return translate(action)
@router.get('/plans/{plan_id}')
def plan_components(plan_id: str, project_id: str, request: Request):
guard(request)
def action():
from services import review_plan
from services.assistant_data import load_catalog
with service().connect() as db:
context = project(db, request, project_id)
frozen = review_plan.assemble(db, plan_id, load_catalog())
if frozen['plan']['scope'] != context['data_domain']:
raise HTTPException(403, '方案数据域不匹配')
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']}
return translate(action)