Files
mini-openclaw-v2/backend/services/catalog_configuration.py

85 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""BR72–75 scoped catalog availability, creation and read-only assistant answers."""
from services import assistant_store as store, component_registry as registry
CARD_KINDS = ('data_card', 'skill', 'rule_set', 'template')
def hidden(db, component_id):
try: return store.get(db, 'component_availability', component_id)['hidden']
except KeyError: return False
def available_plan(db, plan):
refs=[plan.get('component_id') or plan['id'], plan.get('template')]+plan.get('skills',[])+plan.get('rules',[])+plan.get('cards',[])
return not any(hidden(db,key) for key in refs if key)
def set_visibility(db, card, actor, value):
component_id=card.get('component_id') or card['id']
if card.get('status')!='published': raise ValueError('仅已发布卡片可设置隐藏;草稿需先评测发布')
result=store.put(db,'component_availability',{'id':component_id,'hidden':value,'updated_by':actor,'updated_at':store.now()})
store.event(db,card.get('project_id',''),'component.visibility',actor,{'component_id':component_id,'hidden':value})
return result
def catalog(db, project_id, domain, admin=False):
rows=[registry.resolve(db,p['id']) for p in store.listing(db,'release')]
if admin:
published={(row['id'],row['version']) for row in rows}
rows += [row for row in store.listing(db,'component_draft',project_id) if (row.get('component_id'),row['version']) not in published]
result=[]
for row in rows:
if row.get('kind') not in CARD_KINDS or row.get('scope')!=domain or row.get('project_id') not in (None,'',project_id):continue
if row.get('id')=='review.analysis' or row.get('audience')==['reviewer']:continue
row=dict(row,hidden=hidden(db,row.get('component_id') or row['id']))
if admin or not row['hidden']:result.append(row)
return result
def create(db, context, actor, kind, name, content):
if kind not in ('data_card','template'):raise ValueError('该入口仅支持数据卡与模板卡')
allowed={'data_card':{'source_id','mapping','sql','tables','alignment','parameter','field_units','unit'},'template':{'sections','instructions'}}[kind]
if set(content)-allowed:raise ValueError('配置包含未支持的字段')
if kind=='template':
sections=content.get('sections')
if not isinstance(sections,list) or not 1<=len(sections)<=12:raise ValueError('模板需提供1至12个章节')
if any(not isinstance(item,dict) or set(item)-{'id','title','dependencies'} or not isinstance(item.get('id'),str) or not isinstance(item.get('title'),str) or not item['id'] or not item['title'] or len(item['title'])>120 or not isinstance(item.get('dependencies',[]),list) for item in sections):raise ValueError('模板章节格式不合法')
else:
from services.assistant_materials import FIELDS
mapping=content.get('mapping')
if not isinstance(mapping,dict) or not mapping or set(mapping)-set(FIELDS.values()) or any(not isinstance(value,str) or not value or len(value)>120 for value in mapping.values()):raise ValueError('数据卡字段映射需使用标准字段')
if not isinstance(content.get('sql'),str):raise ValueError('数据卡需要只读SQL')
body={**content,'id':store.uid('manual.'+kind),'kind':kind,'name':name,'version':'1.0.0','scope':context['data_domain'],'project_id':context['id'],'origin':'manual','edited_by':actor}
if kind=='data_card':
source=registry.resolve(db,body.get('source_id',''))
if source.get('scope')!=context['data_domain'] or source.get('project_id') not in (None,'',context['id']):raise ValueError('数据源范围不匹配')
from api.risk_agent_connectors import validate_query
dialect={'PostgreSQL':'postgres','MySQL':'mysql'}.get(source['type'],'sqlite')
validate_query(body.get('sql',''),source.get('allowed_tables',[]),dialect)
return registry.draft(db,body,actor)
def answer(db, project_id, domain, role, message):
rows=catalog(db,project_id,domain,role=='admin')
text=message.lower();kind=next((k for k,words in [('skill',('skill','技能')),('rule_set',('rule','规则')),('template',('模板',)),('data_card',('数据卡',))] if any(w in text for w in words)),None)
if kind:rows=[r for r in rows if r['kind']==kind]
if '金融' in text or '租赁' in text:
rows=[r for r in rows if any(w in str(r) for w in ('financial.','lease.','corporate.','租赁','现金流','财务','金租'))]
names=[r['name']+'('+('隐藏' if r['hidden'] else '已发布' if r.get('status')=='published' else '草稿')+')' for r in rows]
return {'intent':'catalog_query','message':f'当前项目可访问的'+(kind or '卡片')+f'共 {len(rows)} 张。'+('\n'+'\n'.join(names) if names else ' 当前没有匹配卡片。'),'items':[{'id':r['id'],'name':r['name'],'kind':r['kind'],'status':r['status'],'hidden':r['hidden']} for r in rows],'url':f'/configuration-center?project={project_id}' if role=='admin' else f'/prescreen-workbench?project={project_id}'}
def seed_fact_source(db):
source_id='source.project-facts'
try:registry.resolve(db,source_id);seed_fact_card(db);return
except KeyError:pass
body=registry.normalize({'id':source_id,'kind':'data_source','name':'当前项目已确认事实(只读)','type':'demo','scope':'demo','status':'published','version':'1.0.0','allowed_tables':['project','finance','credit','assets']},'system-demo')
registry.validate(body);version_id=source_id+'@1.0.0'
store.put(db,'component_version',dict(body,id=version_id,component_id=source_id),immutable=True)
store.put(db,'release',{'id':source_id,'version_id':version_id,'scope':'demo'})
store.event(db,'','source.demo_registered','system-demo',{'source_id':source_id,'purpose':'controlled-project-facts'})
seed_fact_card(db)
def seed_fact_card(db):
card_id='data.initial-finance'
try:registry.resolve(db,card_id);return
except KeyError:pass
fields=('financial.total_assets','financial.total_liabilities','financial.operating_cashflow')
body=registry.normalize({'id':card_id,'kind':'data_card','name':'初步财务与融资事实','description':'只读取当前项目已确认事实,保留字段口径与项目范围。','status':'published','scope':'demo','version':'1.0.0','source_id':'source.project-facts','sql':'SELECT * FROM finance WHERE project_id=:project_id','tables':['finance'],'parameter':'project_id','mapping':{'financial.total_assets':'assets','financial.total_liabilities':'liabilities','financial.operating_cashflow':'cashflow'},'alignment':{'project_field':'project_id','selection':'single'},'field_units':{field:'万元' for field in fields}},'system-demo')
registry.validate(body);version_id=card_id+'@1.0.0'
store.put(db,'component_version',dict(body,id=version_id,component_id=card_id),immutable=True)
store.put(db,'release',{'id':card_id,'version_id':version_id,'scope':'demo'})
store.event(db,'','card.demo_registered','system-demo',{'card_id':card_id,'purpose':'confirmed-project-facts'})