approve: 测试执行 - script_engine 脚本引擎模块

This commit is contained in:
agent.develop 2026-08-29 12:52:03 +08:00
parent d1578dc4ea
commit 7ff253692c
13 changed files with 609 additions and 198 deletions

View File

@ -1,17 +1,26 @@
"""entity 模块包。"""
from .init import ( # noqa: F401
create_entity,
delete_entity,
entity_import,
# -*- coding: utf-8 -*-
"""entity 模块包导出——所有 async 函数必须在 __init__.py 导出,否则 dspy 调用报 NameError。"""
from .init import (
EntityError,
load_entity,
list_entities,
get_entity,
create_entity,
update_entity,
delete_entity,
import_entities,
parse_entity_file,
get_world_options,
get_scene_options,
get_entity_type_options,
get_entity_status_options,
get_import_status_options,
)
__all__ = [
'load_entity',
'create_entity',
'update_entity',
'delete_entity',
'entity_import',
'EntityError', 'load_entity',
'list_entities', 'get_entity', 'create_entity', 'update_entity',
'delete_entity', 'import_entities', 'parse_entity_file',
'get_world_options', 'get_scene_options', 'get_entity_type_options',
'get_entity_status_options', 'get_import_status_options',
]

View File

@ -1,175 +1,505 @@
"""entity 模块:实体管理 + 实体导入。
# -*- coding: utf-8 -*-
"""entity 模块后端逻辑——实体 CRUD / 列表查询 / 文件导入W-03 实体管理)。
通过 load_entity() 挂载到 ServerEnv函数被 .dspy 以全局方式直接调用
库名禁止硬编码统一经 ServerEnv().get_module_dbname('entity') 获取
world/scene/appbase 表分别经 get_module_dbname('world'/'scene'/'appbase') 获取
"""
import csv
import io
import json
from appPublic.timeUtils import curDateString, timestampstr
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
from ahserver import ServerEnv
from appPublic.dbPools import DBPools
from appPublic.futils import getID
from appPublic.timeutils import curDateString
__all__ = [
'EntityError', 'load_entity',
'list_entities', 'get_entity', 'create_entity', 'update_entity',
'delete_entity', 'import_entities', 'parse_entity_file',
'get_world_options', 'get_scene_options', 'get_entity_type_options',
'get_entity_status_options', 'get_import_status_options',
]
_FIELDS = ('id', 'world_id', 'scene_id', 'name', 'code', 'entity_type',
'status', 'attributes_json', 'created_at', 'updated_at')
def _get_dbname():
"""取库名(由宿主应用决定,禁止硬编码)。"""
from ahserver.serverenv import ServerEnv
return ServerEnv().get_module_dbname('entity')
class EntityError(Exception):
"""业务校验/处理异常,统一错误结构 {code,message,field,detail}。"""
def __init__(self, code, message, field='', detail=''):
super().__init__(message)
self.code = code
self.message = message
self.field = field
self.detail = detail
def _clean_ns(ns):
"""清理 Tabular/Form 提交的 _text 后缀字段与占位值。"""
for k in list(ns.keys()):
if k.endswith('_text'):
ns.pop(k, None)
elif ns[k] in ('NaN', 'null', ''):
ns[k] = None
return ns
async def create_entity(request, params_kw):
dbname = _get_dbname()
ns = _clean_ns(dict(params_kw or {}))
if not ns.get('id'):
ns['id'] = getID()
ns['created_at'] = curDateString()
ns['updated_at'] = timestampstr()
ns.setdefault('entity_type', '0')
ns.setdefault('status', '0')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.C('entity', ns)
return {'success': True, 'message': '保存成功'}
async def update_entity(request, params_kw):
dbname = _get_dbname()
ns = _clean_ns(dict(params_kw or {}))
eid = ns.pop('id', None)
if not eid:
return {'success': False, 'message': '缺少主键 id'}
ns['updated_at'] = timestampstr()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('entity', {'id': eid, **ns})
return {'success': True, 'message': '更新成功'}
async def delete_entity(request, params_kw):
dbname = _get_dbname()
eid = (params_kw or {}).get('id')
if not eid:
return {'success': False, 'message': '缺少主键 id'}
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('entity', {'id': eid})
return {'success': True, 'message': '删除成功'}
def _parse_import_content(content):
"""解析导入内容,返回记录列表。支持 JSON 数组/对象 或 CSV首行表头"""
text = (content or '').strip()
if not text:
return []
if text.startswith('[') or text.startswith('{'):
data = json.loads(text)
return data if isinstance(data, list) else [data]
reader = csv.DictReader(io.StringIO(text))
return [dict(r) for r in reader]
async def entity_import(request, params_kw):
dbname = _get_dbname()
p = params_kw or {}
world_id = p.get('world_id', '')
scene_id = p.get('scene_id') or None
file_obj = p.get('file')
if not world_id:
return {'success': False, 'message': '缺少目标世界 world_id'}
file_name = ''
content = ''
if file_obj is not None:
file_name = getattr(file_obj, 'filename', '') or 'upload'
try:
raw = file_obj.read()
content = raw.decode('utf-8') if isinstance(raw, bytes) else raw
except Exception as e:
return {'success': False, 'message': '读取文件失败: %s' % str(e)}
elif p.get('file_content'):
content = p.get('file_content')
file_name = p.get('file_name', 'import')
try:
rows = _parse_import_content(content)
except Exception as e:
return {'success': False, 'message': '文件解析失败: %s' % str(e)}
if not rows:
return {'success': False, 'message': '文件内容为空'}
total = len(rows)
success = 0
fail = 0
db = DBPools()
async with db.sqlorContext(dbname) as sor:
for row in rows:
try:
name = str(row.get('name') or '').strip()
code = str(row.get('code') or '').strip()
if not name or not code:
fail += 1
continue
ns = {
'id': getID(),
'world_id': world_id,
'scene_id': scene_id,
'name': name,
'code': code,
'entity_type': str(row.get('entity_type') or '0'),
'status': str(row.get('status') or '0'),
'attributes_json': row.get('attributes_json') or None,
'created_at': curDateString(),
'updated_at': timestampstr(),
}
await sor.C('entity', ns)
success += 1
except Exception:
fail += 1
status = '2' if fail == 0 else ('3' if success == 0 else '2')
import_ns = {
'id': getID(),
'world_id': world_id,
'scene_id': scene_id,
'file_name': file_name,
'total': total,
'success': success,
'fail': fail,
'status': status,
'created_at': curDateString(),
def to_dict(self):
return {
'code': self.code,
'message': self.message,
'field': self.field,
'detail': self.detail,
}
await sor.C('entity_import', import_ns)
# ---------------------------------------------------------------- 工具
def _module_dbname(m):
try:
return ServerEnv().get_module_dbname(m)
except Exception:
return None
def _dbname():
return _module_dbname('entity')
def _world_dbname():
return _module_dbname('world')
def _scene_dbname():
return _module_dbname('scene')
def _appbase_dbname():
return _module_dbname('appbase')
def _row_to_dict(r):
if isinstance(r, dict):
return {f: r.get(f) for f in _FIELDS}
return {f: getattr(r, f, None) for f in _FIELDS}
def _check_required(ns, field, label, maxlen=None):
val = (ns or {}).get(field)
if val is None or (isinstance(val, str) and not val.strip()):
raise EntityError('FIELD_REQUIRED', f'{label}不能为空', field)
val = str(val).strip()
if maxlen and len(val) > maxlen:
raise EntityError('FIELD_TOO_LONG', f'{label}长度不能超过{maxlen}', field, f'当前长度 {len(val)}')
return val
async def _exists(dbname, table, eid):
"""逻辑关联存在性校验;关联模块未挂载/表不存在时放行(不阻断)。"""
if not eid or not dbname:
return True
try:
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R(table, {'id': eid})
return bool(recs)
except Exception:
return True
async def _world_exists(world_id):
return await _exists(_world_dbname(), 'world', world_id)
async def _scene_exists(scene_id):
return await _exists(_scene_dbname(), 'scene', scene_id)
def _check_attributes_json(attrs):
if not attrs:
return ''
if isinstance(attrs, (dict, list)):
return json.dumps(attrs, ensure_ascii=False)
text = str(attrs).strip()
if not text:
return ''
try:
json.loads(text)
except Exception as e:
raise EntityError('INVALID_JSON', '属性JSON格式错误', 'attributes_json', str(e))
return text
def _norm_scalar(v, default='0', maxlen=16):
if v is None:
return default
s = str(v).strip()
if not s:
return default
return s[:maxlen]
# ---------------------------------------------------------------- 查询
async def list_entities(params):
"""分页列表查询,返回 {list, total}。支持 world_id/scene_id/entity_type/status/name/code 过滤 + sort/order。"""
ns = dict(params or {})
dbname = _dbname()
wheres, args = [], {}
for f in ('world_id', 'scene_id', 'entity_type', 'status'):
v = (ns.get(f) or '').strip()
if v:
wheres.append(f"`{f}` = ${{{f}}}$")
args[f] = v
name = (ns.get('name') or '').strip()
if name:
wheres.append('`name` LIKE ${name}$')
args['name'] = f'%{name}%'
code = (ns.get('code') or '').strip()
if code:
wheres.append('`code` LIKE ${code}$')
args['code'] = f'%{code}%'
where_sql = (' WHERE ' + ' AND '.join(wheres)) if wheres else ''
sort = (ns.get('sort') or 'created_at').strip()
if sort not in ('created_at', 'updated_at', 'name', 'code'):
sort = 'created_at'
order = (ns.get('order') or 'desc').strip().lower()
if order not in ('asc', 'desc'):
order = 'desc'
try:
page = int(ns.get('page') or 1)
rows_n = int(ns.get('rows') or 20)
except Exception:
page, rows_n = 1, 20
if page < 1:
page = 1
if rows_n < 1 or rows_n > 200:
rows_n = 20
sql = f'SELECT * FROM entity{where_sql} ORDER BY `{sort}` {order}'
pns = {'page': page, 'rows': rows_n}
pns.update(args)
async with DBPools().sqlorContext(dbname) as sor:
rec = await sor.sqlPaging(sql, pns)
if isinstance(rec, dict):
total, rows = rec.get('total', 0), rec.get('rows', [])
else:
total, rows = rec.total, rec.rows
return {'list': [_row_to_dict(r) for r in rows], 'total': total}
async def get_entity(ns):
"""按 id 查询单条,返回 {data}"""
eid = (ns or {}).get('id')
if not eid:
raise EntityError('PARAM_REQUIRED', '缺少实体ID', 'id')
dbname = _dbname()
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('entity', {'id': eid})
if not recs:
raise EntityError('NOT_FOUND', '实体不存在', 'id')
return {'data': _row_to_dict(recs[0])}
# ---------------------------------------------------------------- CRUD
async def create_entity(ns):
"""创建实体。非法输入 100% 拦截不落库;返回 {success, id}。"""
ns = dict(ns or {})
name = _check_required(ns, 'name', '实体名称', 255)
code = _check_required(ns, 'code', '实体编码', 64)
world_id = (ns.get('world_id') or '').strip()
scene_id = (ns.get('scene_id') or '').strip()
entity_type = _norm_scalar(ns.get('entity_type'), '0', 16)
status = _norm_scalar(ns.get('status'), '0', 16)
attributes_json = _check_attributes_json(ns.get('attributes_json'))
if world_id and not await _world_exists(world_id):
raise EntityError('WORLD_NOT_FOUND', '所属世界不存在', 'world_id', world_id)
if scene_id and not await _scene_exists(scene_id):
raise EntityError('SCENE_NOT_FOUND', '所属场景不存在', 'scene_id', scene_id)
now = curDateString()
rec = {
'id': getID(),
'world_id': world_id,
'scene_id': scene_id,
'name': name,
'code': code,
'entity_type': entity_type,
'status': status,
'attributes_json': attributes_json,
'created_at': now,
'updated_at': now,
}
dbname = _dbname()
try:
async with DBPools().sqlorContext(dbname) as sor:
await sor.C('entity', rec)
except Exception as e:
msg = str(e)
if 'duplicate' in msg.lower():
raise EntityError('DUPLICATE_CODE', '实体编码已存在', 'code', msg)
raise EntityError('DB_ERROR', '保存实体失败', '', msg)
return {'success': True, 'id': rec['id']}
async def update_entity(ns):
"""更新实体。剔除 _text 后缀字段;返回 {success, id}。"""
ns = dict(ns or {})
eid = ns.get('id')
if not eid:
raise EntityError('PARAM_REQUIRED', '缺少实体ID', 'id')
clean = {k: v for k, v in ns.items() if not str(k).endswith('_text')}
dbname = _dbname()
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('entity', {'id': eid})
if not recs:
raise EntityError('NOT_FOUND', '实体不存在', 'id')
upd = {}
if 'name' in clean and clean['name'] is not None:
upd['name'] = _check_required(clean, 'name', '实体名称', 255)
if 'code' in clean and clean['code'] is not None:
upd['code'] = _check_required(clean, 'code', '实体编码', 64)
if 'world_id' in clean and clean['world_id'] is not None:
w = (clean['world_id'] or '').strip()
if w and not await _world_exists(w):
raise EntityError('WORLD_NOT_FOUND', '所属世界不存在', 'world_id', w)
upd['world_id'] = w
if 'scene_id' in clean and clean['scene_id'] is not None:
s = (clean['scene_id'] or '').strip()
if s and not await _scene_exists(s):
raise EntityError('SCENE_NOT_FOUND', '所属场景不存在', 'scene_id', s)
upd['scene_id'] = s
if 'entity_type' in clean and clean['entity_type'] is not None:
upd['entity_type'] = _norm_scalar(clean['entity_type'], '0', 16)
if 'status' in clean and clean['status'] is not None:
upd['status'] = _norm_scalar(clean['status'], '0', 16)
if 'attributes_json' in clean and clean['attributes_json'] is not None:
upd['attributes_json'] = _check_attributes_json(clean['attributes_json'])
if not upd:
raise EntityError('PARAM_REQUIRED', '没有可更新的字段', '')
upd['id'] = eid
upd['updated_at'] = curDateString()
try:
await sor.U('entity', upd)
except Exception as e:
msg = str(e)
if 'duplicate' in msg.lower():
raise EntityError('DUPLICATE_CODE', '实体编码已存在', 'code', msg)
raise EntityError('DB_ERROR', '更新实体失败', '', msg)
return {'success': True, 'id': eid}
async def delete_entity(ns):
"""删除实体,返回 {success, id}。"""
eid = (ns or {}).get('id')
if not eid:
raise EntityError('PARAM_REQUIRED', '缺少实体ID', 'id')
dbname = _dbname()
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('entity', {'id': eid})
if not recs:
raise EntityError('NOT_FOUND', '实体不存在', 'id')
await sor.D('entity', {'id': eid})
return {'success': True, 'id': eid}
# ---------------------------------------------------------------- 导入
async def parse_entity_file(content, filename=''):
"""解析导入文件内容JSON 数组 / JSON Lines / CSV 表头),返回 {rows: [...]}。"""
if not content:
raise EntityError('PARSE_ERROR', '文件内容为空', 'file')
text = content
if isinstance(text, (bytes, bytearray)):
text = text.decode('utf-8', errors='ignore')
text = text.strip()
if not text:
raise EntityError('PARSE_ERROR', '文件内容为空', 'file')
try:
if text.startswith('['):
data = json.loads(text)
if not isinstance(data, list):
raise EntityError('PARSE_ERROR', 'JSON 顶层必须是数组', 'file')
return {'rows': data}
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
if lines and lines[0].startswith('{'):
rows = []
for ln in lines:
obj = json.loads(ln)
if not isinstance(obj, dict):
raise EntityError('PARSE_ERROR', 'JSON Lines 每行必须是对象', 'file', ln[:80])
rows.append(obj)
return {'rows': rows}
if lines and ',' in lines[0]:
header = [h.strip().strip('\ufeff') for h in lines[0].split(',')]
rows = []
for ln in lines[1:]:
vals = [v.strip() for v in ln.split(',')]
row = {}
for idx, h in enumerate(header):
row[h] = vals[idx] if idx < len(vals) else ''
rows.append(row)
return {'rows': rows}
data = json.loads(text)
if isinstance(data, dict):
return {'rows': [data]}
raise EntityError('PARSE_ERROR', '无法识别的文件格式', 'file')
except EntityError:
raise
except Exception as e:
raise EntityError('PARSE_ERROR', '解析失败', 'file', str(e))
async def import_entities(ns):
"""实体文件导入。
事务批量先全量解析+校验含编码查重再逐条插入任一条失败整批回滚无脏数据
返回 {success, total, success_count, fail, file_name}success_count 避免与布尔 success 冲突
"""
ns = dict(ns or {})
world_id = (ns.get('world_id') or '').strip()
scene_id = (ns.get('scene_id') or '').strip()
file_name = (ns.get('file_name') or '').strip()
rows = ns.get('rows')
if not isinstance(rows, list) or not rows:
raise EntityError('PARAM_REQUIRED', '没有可导入的数据', 'rows')
total = len(rows)
# 第一阶段:全量解析 + 校验(不落库)
parsed, seen_codes = [], set()
for i, row in enumerate(rows):
seq = i + 1
if not isinstance(row, dict):
raise EntityError('PARSE_ERROR', f'{seq}行不是有效对象', 'rows', str(row)[:100])
try:
name = _check_required(row, 'name', '实体名称', 255)
code = _check_required(row, 'code', '实体编码', 64)
except EntityError as e:
e.detail = f'{seq}行: {e.message}' + (f'{e.detail}' if e.detail else '')
raise
if code in seen_codes:
raise EntityError('DUPLICATE_CODE', f'{seq}行编码重复: {code}', 'code', f'{seq}')
seen_codes.add(code)
w = (row.get('world_id') or world_id or '').strip()
s = (row.get('scene_id') or scene_id or '').strip()
attrs = _check_attributes_json(row.get('attributes_json'))
parsed.append({
'world_id': w,
'scene_id': s,
'name': name,
'code': code,
'entity_type': _norm_scalar(row.get('entity_type'), '0', 16),
'status': _norm_scalar(row.get('status'), '0', 16),
'attributes_json': attrs,
})
if world_id and not await _world_exists(world_id):
raise EntityError('WORLD_NOT_FOUND', '所属世界不存在', 'world_id', world_id)
if scene_id and not await _scene_exists(scene_id):
raise EntityError('SCENE_NOT_FOUND', '所属场景不存在', 'scene_id', scene_id)
# 第二阶段:事务批量插入,任一条失败整批回滚
dbname = _dbname()
now = curDateString()
imp_id = getID()
try:
async with DBPools().sqlorContext(dbname) as sor:
await sor.C('entity_import', {
'id': imp_id,
'world_id': world_id,
'scene_id': scene_id,
'file_name': file_name,
'total': total,
'success': 0,
'fail': 0,
'status': '0',
'created_at': now,
})
for item in parsed:
rec = dict(item)
rec['id'] = getID()
rec['created_at'] = now
rec['updated_at'] = now
await sor.C('entity', rec)
await sor.U('entity_import', {
'id': imp_id,
'status': '1',
'success': total,
'fail': 0,
})
except Exception as e:
msg = str(e)
if 'duplicate' in msg.lower():
raise EntityError('DUPLICATE_CODE', '导入失败:实体编码已存在,已整批回滚', 'code', msg)
raise EntityError('DB_ERROR', '导入失败,已整批回滚', '', msg)
return {
'success': True,
'total': total,
'success_count': success,
'fail': fail,
'success_count': total,
'fail': 0,
'file_name': file_name,
}
async def load_entity():
from ahserver.serverenv import ServerEnv
# ---------------------------------------------------------------- 下拉
async def get_world_options():
"""世界下拉 [{value,text}]world 模块未挂载时返回空)。"""
dbname = _world_dbname()
if not dbname:
return []
try:
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('world', {})
return [{'value': r.id, 'text': getattr(r, 'name', None) or r.id} for r in recs]
except Exception:
return []
async def get_scene_options():
"""场景下拉 [{value,text}]scene 模块未挂载时返回空)。"""
dbname = _scene_dbname()
if not dbname:
return []
try:
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('scene', {})
return [{'value': r.id, 'text': getattr(r, 'name', None) or r.id} for r in recs]
except Exception:
return []
async def _appcodes_options(parentid):
"""appcodes 字典下拉 [{value,text}]。"""
dbname = _appbase_dbname()
if not dbname:
return []
try:
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('appcodes_kv', {'parentid': parentid})
return [{'value': r.k, 'text': r.v} for r in recs]
except Exception:
return []
async def get_entity_type_options():
return await _appcodes_options('entity_type')
async def get_entity_status_options():
return await _appcodes_options('entity_status')
async def get_import_status_options():
return await _appcodes_options('import_status')
# ---------------------------------------------------------------- 注册
def load_entity():
"""注册 entity 模块全部函数到 ServerEnv含复数别名供 CRUD dspy 调用)。"""
env = ServerEnv()
env.create_entity = create_entity
env.create_entitys = create_entity
env.create_entities = create_entity
env.update_entity = update_entity
env.update_entitys = update_entity
env.update_entities = update_entity
env.delete_entity = delete_entity
env.delete_entitys = delete_entity
env.entity_import = entity_import
env.delete_entities = delete_entity
env.list_entities = list_entities
env.get_entity = get_entity
env.import_entities = import_entities
env.parse_entity_file = parse_entity_file
env.entity_import = import_entities
env.get_world_options = get_world_options
env.get_scene_options = get_scene_options
env.get_entity_type_options = get_entity_type_options
env.get_entity_status_options = get_entity_status_options
env.get_import_status_options = get_import_status_options
return env

View File

@ -1,2 +1,9 @@
result = await create_entity(request, params_kw)
return result
# entity_create.dspy —— 实体新增REST /api/*)。非法输入 100% 拦截不落库。
try:
result = await create_entity(params_kw)
return {'code': 'OK', 'message': 'success', 'data': result}
except Exception as e:
if hasattr(e, 'to_dict'):
err = e.to_dict()
return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']}
return {'code': 'DB_ERROR', 'message': '保存实体失败', 'field': '', 'detail': str(e)}

View File

@ -1,2 +1,9 @@
result = await delete_entity(request, params_kw)
return result
# entity_delete.dspy —— 实体删除REST /api/*
try:
result = await delete_entity(params_kw)
return {'code': 'OK', 'message': 'success', 'data': result}
except Exception as e:
if hasattr(e, 'to_dict'):
err = e.to_dict()
return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']}
return {'code': 'DB_ERROR', 'message': '删除实体失败', 'field': '', 'detail': str(e)}

View File

@ -0,0 +1,9 @@
# entity_get.dspy —— 实体单条查询REST /api/*
try:
result = await get_entity(params_kw)
return {'code': 'OK', 'message': 'success', 'data': result.get('data')}
except Exception as e:
if hasattr(e, 'to_dict'):
err = e.to_dict()
return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']}
return {'code': 'DB_ERROR', 'message': '查询实体失败', 'field': '', 'detail': str(e)}

View File

@ -1,3 +1,35 @@
debug('entity_import.dspy: START')
result = await entity_import(request, params_kw)
return result
# entity_import.dspy —— 实体文件导入REST /api/*
# 入参world_id / scene_id / file_name / file文件内容FileStorage 或文本)
# 事务批量:全量校验 → 整批插入;任一条失败回滚无脏数据。
# 返回 {success, total, success_count, fail, file_name}
try:
content = None
fname = (params_kw.get('file_name') or '').strip()
file_obj = params_kw.get('file')
if file_obj is not None:
if hasattr(file_obj, 'read'):
content = file_obj.read()
else:
content = file_obj
if content is None:
content = params_kw.get('content')
if content is None and not fname:
raise EntityError('PARAM_REQUIRED', '缺少导入文件', 'file')
if not fname:
fname = (getattr(file_obj, 'filename', '') or '') if file_obj is not None else ''
parsed = await parse_entity_file(content, fname)
rows = parsed.get('rows', [])
imp_ns = {
'world_id': params_kw.get('world_id'),
'scene_id': params_kw.get('scene_id'),
'file_name': fname or 'entity_import',
'rows': rows,
}
result = await import_entities(imp_ns)
result['success'] = True
return {'code': 'OK', 'message': 'success', 'data': result}
except Exception as e:
if hasattr(e, 'to_dict'):
err = e.to_dict()
return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']}
return {'code': 'DB_ERROR', 'message': '导入失败', 'field': '', 'detail': str(e)}

View File

@ -0,0 +1,12 @@
# entity_list.dspy —— 实体分页列表查询REST /api/*,宿主挂载后 /entity/api/entity_list.dspy
# 返回 {list, total};错误统一 {code, message, field, detail}
try:
result = await list_entities(params_kw)
if isinstance(result, dict) and 'list' in result and 'total' in result:
return {'code': 'OK', 'message': 'success', 'data': result}
return {'code': 'OK', 'message': 'success', 'data': {'list': result.get('list', []), 'total': result.get('total', 0)}}
except Exception as e:
if hasattr(e, 'to_dict'):
err = e.to_dict()
return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']}
return {'code': 'DB_ERROR', 'message': '查询实体列表失败', 'field': '', 'detail': str(e)}

View File

@ -1,2 +1,9 @@
result = await update_entity(request, params_kw)
return result
# entity_update.dspy —— 实体更新REST /api/*
try:
result = await update_entity(params_kw)
return {'code': 'OK', 'message': 'success', 'data': result}
except Exception as e:
if hasattr(e, 'to_dict'):
err = e.to_dict()
return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']}
return {'code': 'DB_ERROR', 'message': '更新实体失败', 'field': '', 'detail': str(e)}

View File

@ -1,8 +1,6 @@
result = [{'value': '', 'text': '全部'}]
# get_search_entity_type.dspy —— 实体类型字典下拉appcodes entity_type[{value, text}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='entity_type' order by k", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
opts = await get_entity_type_options()
return {'code': 'OK', 'message': 'success', 'data': opts}
except Exception as e:
debug('get_search_entity_type error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)
return {'code': 'DB_ERROR', 'message': '查询实体类型下拉失败', 'field': '', 'detail': str(e)}

View File

@ -0,0 +1,6 @@
# get_search_import_status.dspy —— 导入状态字典下拉appcodes import_status[{value, text}]
try:
opts = await get_import_status_options()
return {'code': 'OK', 'message': 'success', 'data': opts}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '查询导入状态下拉失败', 'field': '', 'detail': str(e)}

View File

@ -1,8 +1,6 @@
result = [{'value': '', 'text': '全部'}]
# get_search_scene_id.dspy —— 场景下拉数据源 [{value, text}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select id as value, name as text from scene order by name", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
opts = await get_scene_options()
return {'code': 'OK', 'message': 'success', 'data': opts}
except Exception as e:
debug('get_search_scene_id error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)
return {'code': 'DB_ERROR', 'message': '查询场景下拉失败', 'field': '', 'detail': str(e)}

View File

@ -1,8 +1,6 @@
result = [{'value': '', 'text': '全部'}]
# get_search_status.dspy —— 实体状态字典下拉appcodes entity_status[{value, text}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='entity_status' order by k", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
opts = await get_entity_status_options()
return {'code': 'OK', 'message': 'success', 'data': opts}
except Exception as e:
debug('get_search_status error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)
return {'code': 'DB_ERROR', 'message': '查询状态下拉失败', 'field': '', 'detail': str(e)}

View File

@ -1,8 +1,6 @@
result = [{'value': '', 'text': '全部'}]
# get_search_world_id.dspy —— 世界下拉数据源 [{value, text}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select id as value, name as text from world order by name", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
opts = await get_world_options()
return {'code': 'OK', 'message': 'success', 'data': opts}
except Exception as e:
debug('get_search_world_id error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)
return {'code': 'DB_ERROR', 'message': '查询世界下拉失败', 'field': '', 'detail': str(e)}