diff --git a/b/cntoai/model_management_add.dspy b/b/cntoai/model_management_add.dspy index a9c0312..6ff492d 100644 --- a/b/cntoai/model_management_add.dspy +++ b/b/cntoai/model_management_add.dspy @@ -1,12 +1,15 @@ -# 可写入/更新的字段(不含 id、created_at、updated_at) +# model_management 可写入字段(不含 id、created_at、updated_at) _MODEL_FIELDS = ( 'llmid', 'provider', 'model_name', 'display_name', 'model_type', 'context_length', 'input_token_price', 'output_token_price', 'cache_hit_input_price', 'billing_method', 'billing_unit', 'capabilities', 'limitations', 'highlights', 'is_active', - 'description', 'listing_status', + 'description', 'listing_status', 'sort_order', 'experience', ) +# model_api_doc 可写入字段(不含 id、model_id、created_at、updated_at) +_API_DOC_FIELDS = ('api_url', 'curl_code', 'python_code') + def _escape(value): if value is None: @@ -24,26 +27,63 @@ def _build_model_dict(ns, include_listing_status=False): return data +def _build_api_doc_dict(ns): + data = {} + for field in _API_DOC_FIELDS: + if field in ns and ns.get(field) is not None: + data[field] = ns.get(field) + return data + + async def model_management_add(ns={}): - """新增模型,默认待上架 listing_status=0""" + """新增模型及 API 文档,provider、model_name 必传""" if not ns.get('provider') or not ns.get('model_name'): return {'status': False, 'msg': 'provider and model_name are required'} - ns_dic = _build_model_dict(ns, include_listing_status=True) - if 'listing_status' not in ns_dic: - ns_dic['listing_status'] = 0 - if 'is_active' not in ns_dic: - ns_dic['is_active'] = 1 + model_dic = _build_model_dict(ns, include_listing_status=True) + if 'listing_status' not in model_dic: + model_dic['listing_status'] = 0 + if 'is_active' not in model_dic: + model_dic['is_active'] = 1 + + api_doc_dic = _build_api_doc_dict(ns) + if api_doc_dic and not api_doc_dic.get('api_url'): + return {'status': False, 'msg': 'api_url is required when creating api doc'} db = DBPools() async with db.sqlorContext('kboss') as sor: try: - await sor.C('model_management', ns_dic) - return {'status': True, 'msg': 'create model success', 'data': ns_dic} + await sor.C('model_management', model_dic) + + id_rows = await sor.sqlExe('SELECT LAST_INSERT_ID() AS id;', {}) + model_id = id_rows[0]['id'] if id_rows else None + if not model_id: + await sor.rollback() + return {'status': False, 'msg': 'create model failed, missing model id'} + + # 未传 sort_order 时,默认与新建主键 id 相同 + if not ( + 'sort_order' in ns + and ns.get('sort_order') is not None + and ns.get('sort_order') != '' + ): + await sor.U('model_management', {'id': model_id, 'sort_order': model_id}) + model_dic['sort_order'] = model_id + + result_data = dict(model_dic) + result_data['id'] = model_id + + if api_doc_dic: + create_dic = dict(api_doc_dic) + create_dic['model_id'] = str(model_id) + await sor.C('model_api_doc', create_dic) + result_data['api_doc'] = create_dic + + return {'status': True, 'msg': 'create model success', 'data': result_data} except Exception as e: await sor.rollback() return {'status': False, 'msg': 'create model failed, %s' % str(e)} - + ret = await model_management_add(params_kw) -return ret \ No newline at end of file +return ret diff --git a/b/cntoai/model_management_search_doc.dspy b/b/cntoai/model_management_search_doc.dspy new file mode 100644 index 0000000..0f12510 --- /dev/null +++ b/b/cntoai/model_management_search_doc.dspy @@ -0,0 +1,153 @@ +# 可写入/更新的字段(不含 id、created_at、updated_at) +_MODEL_FIELDS = ( + 'llmid', 'provider', 'model_name', 'display_name', 'model_type', + 'context_length', 'input_token_price', 'output_token_price', + 'cache_hit_input_price', 'billing_method', 'billing_unit', + 'capabilities', 'limitations', 'highlights', 'is_active', + 'description', 'listing_status', 'sort_order', +) + + +def _escape(value): + if value is None: + return None + return str(value).replace("'", "''") + + +def _build_model_dict(ns, include_listing_status=False): + data = {} + for field in _MODEL_FIELDS: + if field in ns and ns.get(field) is not None and ns.get(field) != '': + data[field] = ns.get(field) + if include_listing_status and 'listing_status' not in data: + data['listing_status'] = ns.get('listing_status', 0) + return data + + +def _build_where_conditions(ns, table_alias='m'): + """构建 model_management 筛选条件(带表别名,用于 JOIN 查询)""" + prefix = '%s.' % table_alias + conditions = ['1=1'] + if ns.get('display_name'): + display_name = _escape(ns.get('display_name')) + conditions.append("%sdisplay_name LIKE '%%%%%s%%%%'" % (prefix, display_name)) + if ns.get('model_type'): + conditions.append("%smodel_type = '%s'" % (prefix, _escape(ns.get('model_type')))) + if ns.get('provider'): + conditions.append("%sprovider = '%s'" % (prefix, _escape(ns.get('provider')))) + if ns.get('listing_status') is not None and ns.get('listing_status') != '': + conditions.append("%slisting_status = '%s'" % (prefix, _escape(ns.get('listing_status')))) + return ' AND '.join(conditions) + + +def _attach_api_doc(row): + """将 JOIN 出的 API 文档字段整理为 api_doc 子对象""" + api_doc_id = row.pop('api_doc_id', None) + api_url = row.pop('api_url', None) + curl_code = row.pop('curl_code', None) + python_code = row.pop('python_code', None) + api_doc_created_at = row.pop('api_doc_created_at', None) + api_doc_updated_at = row.pop('api_doc_updated_at', None) + + if api_doc_id: + row['api_doc'] = { + 'id': api_doc_id, + 'model_id': str(row.get('id', '')), + 'api_url': api_url, + 'curl_code': curl_code, + 'python_code': python_code, + 'created_at': api_doc_created_at, + 'updated_at': api_doc_updated_at, + } + else: + row['api_doc'] = None + return row + + +async def model_management_search_doc(ns={}): + """ + 分页查询模型列表(含 API 文档),支持按 display_name / model_type / provider / listing_status 筛选。 + model_management LEFT JOIN model_api_doc(model_id = model_management.id)。 + 返回模型总数、待上架总数、已上架总数,以及厂商列表、模型类型列表;每条模型含 api_doc。 + """ + import traceback + + page_size = int(ns.get('page_size', 1000)) + current_page = int(ns.get('current_page', 1)) + offset = (current_page - 1) * page_size + where_clause = _build_where_conditions(ns) + + db = DBPools() + async with db.sqlorContext('kboss') as sor: + try: + stats_sql = """ + SELECT COUNT(*) AS total_count, + SUM(CASE WHEN listing_status = 0 THEN 1 ELSE 0 END) AS pending_count, + SUM(CASE WHEN listing_status = 1 THEN 1 ELSE 0 END) AS listed_count + FROM model_management; + """ + stats_li = await sor.sqlExe(stats_sql, {}) + stats = stats_li[0] if stats_li else {} + + provider_sql = """ + SELECT DISTINCT provider FROM model_management + WHERE provider IS NOT NULL AND provider != '' + ORDER BY provider; + """ + model_type_sql = """ + SELECT DISTINCT model_type FROM model_management + WHERE model_type IS NOT NULL AND model_type != '' + ORDER BY model_type; + """ + + count_sql = """ + SELECT COUNT(*) AS total_count + FROM model_management m + WHERE %s; + """ % where_clause + + find_sql = """ + SELECT m.*, + d.id AS api_doc_id, + d.api_url, + d.curl_code, + d.python_code, + d.created_at AS api_doc_created_at, + d.updated_at AS api_doc_updated_at + FROM model_management m + LEFT JOIN model_api_doc d ON d.model_id = CAST(m.id AS CHAR) + WHERE %s + ORDER BY m.sort_order ASC + LIMIT %s OFFSET %s; + """ % (where_clause, page_size, offset) + + provider_rows = await sor.sqlExe(provider_sql, {}) + model_type_rows = await sor.sqlExe(model_type_sql, {}) + filter_total = (await sor.sqlExe(count_sql, {}))[0]['total_count'] + model_rows = await sor.sqlExe(find_sql, {}) + model_list = [_attach_api_doc(row) for row in model_rows] + + return { + 'status': True, + 'msg': 'search model with api doc success', + 'data': { + 'total_count': stats.get('total_count', 0), + 'pending_count': int(stats.get('pending_count') or 0), + 'listed_count': int(stats.get('listed_count') or 0), + 'provider_list': [r['provider'] for r in provider_rows], + 'model_type_list': [r['model_type'] for r in model_type_rows], + 'filter_total': filter_total, + 'page_size': page_size, + 'current_page': current_page, + 'model_list': model_list, + }, + } + except Exception as e: + return { + 'status': False, + 'msg': 'search model with api doc failed, %s' % traceback.format_exc(), + } + + +ret = await model_management_search_doc(params_kw) +return ret diff --git a/b/cntoai/model_management_update.dspy b/b/cntoai/model_management_update.dspy index 854de52..c28d5b5 100644 --- a/b/cntoai/model_management_update.dspy +++ b/b/cntoai/model_management_update.dspy @@ -1,12 +1,15 @@ -# 可写入/更新的字段(不含 id、created_at、updated_at) +# model_management 可写入字段(不含 id、created_at、updated_at) _MODEL_FIELDS = ( 'llmid', 'provider', 'model_name', 'display_name', 'model_type', 'context_length', 'input_token_price', 'output_token_price', 'cache_hit_input_price', 'billing_method', 'billing_unit', 'capabilities', 'limitations', 'highlights', 'is_active', - 'description', 'listing_status', + 'description', 'listing_status', 'sort_order', 'experience', ) +# model_api_doc 可写入字段(不含 id、model_id、created_at、updated_at) +_API_DOC_FIELDS = ('api_url', 'curl_code', 'python_code') + def _escape(value): if value is None: @@ -23,23 +26,73 @@ def _build_model_dict(ns, include_listing_status=False): data['listing_status'] = ns.get('listing_status', 0) return data + +def _build_api_doc_dict(ns): + """构建 API 文档更新字典;字段在 ns 中即参与更新(含空字符串)""" + data = {} + for field in _API_DOC_FIELDS: + if field in ns and ns.get(field) is not None: + data[field] = ns.get(field) + return data + + async def model_management_update(ns={}): - """编辑模型,id 必传""" + """编辑模型及 API 文档,id(model_management 主键)必传""" model_id = ns.get('id') if not model_id: return {'status': False, 'msg': 'id is required'} - ns_dic = _build_model_dict(ns) - ns_dic['id'] = model_id + model_dic = _build_model_dict(ns) + api_doc_dic = _build_api_doc_dict(ns) + has_model_update = bool(model_dic) + has_api_doc_update = bool(api_doc_dic) + + if not has_model_update and not has_api_doc_update: + return {'status': False, 'msg': 'no fields to update'} db = DBPools() async with db.sqlorContext('kboss') as sor: try: - await sor.U('model_management', ns_dic) + if has_model_update: + model_dic['id'] = model_id + await sor.U('model_management', model_dic) + + if has_api_doc_update: + api_doc_id = ns.get('api_doc_id') + existing = None + if api_doc_id: + find_sql = ( + "SELECT id FROM model_api_doc WHERE id = '%s' AND model_id = '%s' LIMIT 1;" + % (_escape(api_doc_id), _escape(str(model_id))) + ) + existing = await sor.sqlExe(find_sql, {}) + if not existing: + find_sql = ( + "SELECT id FROM model_api_doc WHERE model_id = '%s' LIMIT 1;" + % _escape(str(model_id)) + ) + existing = await sor.sqlExe(find_sql, {}) + + if existing: + doc_update = dict(api_doc_dic) + doc_update['id'] = existing[0]['id'] + await sor.U('model_api_doc', doc_update) + else: + if not api_doc_dic.get('api_url'): + await sor.rollback() + return { + 'status': False, + 'msg': 'api_url is required when creating api doc', + } + create_dic = dict(api_doc_dic) + create_dic['model_id'] = str(model_id) + await sor.C('model_api_doc', create_dic) + return {'status': True, 'msg': 'update model success'} except Exception as e: await sor.rollback() return {'status': False, 'msg': 'update model failed, %s' % str(e)} - + + ret = await model_management_update(params_kw) -return ret \ No newline at end of file +return ret diff --git a/b/cntoai/sync_model_to_llm.dspy b/b/cntoai/sync_model_to_llm.dspy new file mode 100644 index 0000000..ac187af --- /dev/null +++ b/b/cntoai/sync_model_to_llm.dspy @@ -0,0 +1,77 @@ +async def sync_model_to_llm(ns={}): + import aiohttp + + # 从数据库读取domain和Bearer token + db = DBPools() + async with db.sqlorContext('kboss') as sor: + domain_li = await sor.R('params', {'pname': 'cntoai_domain'}) + user_key = await sor.R('params', {'pname': 'cntoai_already_sync_user_key'}) + if not domain_li or not user_key: + return { + 'status': False, + 'msg': '未找到params domain或Bearer token' + } + domain = domain_li[0]['pvalue'] + bearer_token = user_key[0]['pvalue'] + + url = f"{domain}/llmage/list_llms" + header = { + 'Authorization': f'Bearer {bearer_token}', + 'Content-Type': 'application/json', + } + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=header) as response: + result = await response.json() + if not result: + return { + 'status': False, + 'msg': '没有找到模型列表' + } + + # 插入数据库 + db = DBPools() + async with db.sqlorContext('kboss') as sor: + new_llms_count = 0 + new_llms_list = [] + for category_list in result: + for item in category_list.get('llms', []): + # 查找数据库中是否已经存在,不存在就插入 + exist_llm = await sor.R('llm', {'model': item.get('model')}) + if exist_llm: + continue + new_llms = { + 'id': item.get('id'), + 'name': item.get('name'), + 'model': item.get('model'), + 'description': item.get('description'), + 'llmcatelogid': item.get('catelog_id') or item.get('catelogid'), + 'iconid': item.get('iconid'), + 'upappid': item.get('upappid'), + 'apiname': item.get('apiname'), + 'providerid': item.get('providerid'), + 'ownerid': item.get('ownerid') or '0', + 'enabled_date': item.get('enabled_date'), + 'expired_date': item.get('expired_date'), + 'query_apiname': item.get('query_apiname') or '', + 'query_period': item.get('query_period'), + 'ppid': item.get('ppid'), + } + new_llms_count += 1 + new_llms_list.append(new_llms.get('model')) + await sor.C('llm', new_llms) + + return { + 'status': True, + 'msg': f"sync_llm_list同步模型成功,共插入{new_llms_count}个模型,模型列表: {new_llms_list}" + } + + except Exception as e: + return { + 'status': False, + 'msg': f"sync_llm_list同步模型失败, {domain}, {bearer_token}: {e}" + } + + +ret = await sync_model_to_llm(params_kw) +return ret diff --git a/b/user/loginUser.dspy b/b/user/loginUser.dspy index 8f98f93..5efdb04 100644 --- a/b/user/loginUser.dspy +++ b/b/user/loginUser.dspy @@ -152,7 +152,7 @@ async def loginUser(ns): type = 0 if type1 == 1: # 手机号验证码登录 - userreacs = await sor.R('users', {'mobile': ns.get('username')}) + userreacs = await sor.R('users', {'mobile': ns.get('mobile')}) if not userreacs: userreacs = await sor.R('users', {'username': ns.get('username')}) diff --git a/b/user/logintype.dspy b/b/user/logintype.dspy index 5d71bde..8151223 100644 --- a/b/user/logintype.dspy +++ b/b/user/logintype.dspy @@ -199,7 +199,7 @@ async def logintype(ns): return {'status': False, 'msg': '验证码不正确'} if type == 1: # 手机号登录 - users = await sor.R('users', {'mobile': ns.get('username')}) + users = await sor.R('users', {'mobile': ns.get('mobile')}) if not users: users = await sor.R('users', {'username': ns.get('username')}) else: diff --git a/f/web-kboss/src/api/model/model.js b/f/web-kboss/src/api/model/model.js index c6287ad..65e0b79 100644 --- a/f/web-kboss/src/api/model/model.js +++ b/f/web-kboss/src/api/model/model.js @@ -121,4 +121,32 @@ export const reqTokenUsage = (params = {}) => { method: 'post', params }) +} + + +// 模型信息配置添加 +export const reqModelInfoConfig = (params = {}) => { + return request({ + url: '/cntoai/model_management_add.dspy', + method: 'get', + params + }) +} + +// 模型信息配置编辑(编辑时需要额外传 id) +export const reqModelInfoConfigEdit = (params = {}) => { + return request({ + url: '/cntoai/model_management_update.dspy', + method: 'get', + params + }) +} + +// 模型信息配置列表 +export const reqModelInfoConfigList = (params = {}) => { + return request({ + url: '/cntoai/model_management_search_doc.dspy', + method: 'get', + params + }) } \ No newline at end of file diff --git a/f/web-kboss/src/router/index.js b/f/web-kboss/src/router/index.js index e877b3b..c3f628c 100644 --- a/f/web-kboss/src/router/index.js +++ b/f/web-kboss/src/router/index.js @@ -464,6 +464,32 @@ export const asyncRoutes = [ ] }, + // 运营——模型信息配置 + { + path: "/modelInfoConfig", + component: Layout, + meta: { + title: "模型信息配置", + fullPath: "/modelInfoConfig", + noCache: true, + icon: "el-icon-setting", + roles: ["运营"] + }, + children: [ + { + path: "", + component: () => import('@/views/operation/modelInfoConfig/index.vue'), + name: 'ModelInfoConfig', + meta: { + title: "模型信息配置", + fullPath: "/modelInfoConfig", + noCache: true, + roles: ["运营"] + } + }, + ] + }, + // token市集 - 一级菜单(所有登录用户都能看到) { @@ -513,29 +539,29 @@ export const asyncRoutes = [ ] }, // Token用量 - 一级菜单(所有登录用户都能看到) - { - path: "/tokenUsage", - component: Layout, - meta: { - title: "Token用量", - fullPath: "/tokenUsage", - noCache: true, - icon: "el-icon-data-line" - }, - children: [ - { - path: "", - component: () => import('@/views/tokenUsage/index.vue'), - name: 'TokenUsage', - meta: { - title: "Token用量", - fullPath: "/tokenUsage", - noCache: true, - icon: "el-icon-data-line" - } - }, - ] - }, + // { + // path: "/tokenUsage", + // component: Layout, + // meta: { + // title: "Token用量", + // fullPath: "/tokenUsage", + // noCache: true, + // icon: "el-icon-data-line" + // }, + // children: [ + // { + // path: "", + // component: () => import('@/views/tokenUsage/index.vue'), + // name: 'TokenUsage', + // meta: { + // title: "Token用量", + // fullPath: "/tokenUsage", + // noCache: true, + // icon: "el-icon-data-line" + // } + // }, + // ] + // }, // 模型体验 { path: "/modelExperience", diff --git a/f/web-kboss/src/store/modules/permission.js b/f/web-kboss/src/store/modules/permission.js index 6becf7a..719ba05 100644 --- a/f/web-kboss/src/store/modules/permission.js +++ b/f/web-kboss/src/store/modules/permission.js @@ -17,7 +17,7 @@ const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator'; const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/tokenUsage', '/modelExperience', '/modelDetail', '/modelApiDocument']; // 运营角色需要额外补出来的菜单。 -const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/operationReport']; +const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/operationReport']; // 普通客户账号默认要补出来的基础菜单。 const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement']; diff --git a/f/web-kboss/src/views/homePage/indexLast.vue b/f/web-kboss/src/views/homePage/indexLast.vue index 93dbcff..facb57e 100644 --- a/f/web-kboss/src/views/homePage/indexLast.vue +++ b/f/web-kboss/src/views/homePage/indexLast.vue @@ -16,8 +16,8 @@
  • 地址:{{ logoInfoNew.home.adress }}
  • 邮箱:{{logoInfoNew.home.email}}
  • -
  • 电话: {{logoInfoNew.home.mobile}} -
  • + +
  • diff --git a/f/web-kboss/src/views/operation/modelInfoConfig/ModelInfoDetailDialog.vue b/f/web-kboss/src/views/operation/modelInfoConfig/ModelInfoDetailDialog.vue new file mode 100644 index 0000000..1939aaa --- /dev/null +++ b/f/web-kboss/src/views/operation/modelInfoConfig/ModelInfoDetailDialog.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/f/web-kboss/src/views/operation/modelInfoConfig/ModelInfoEditDialog.vue b/f/web-kboss/src/views/operation/modelInfoConfig/ModelInfoEditDialog.vue new file mode 100644 index 0000000..90823b8 --- /dev/null +++ b/f/web-kboss/src/views/operation/modelInfoConfig/ModelInfoEditDialog.vue @@ -0,0 +1,899 @@ + + + + + diff --git a/f/web-kboss/src/views/operation/modelInfoConfig/index.vue b/f/web-kboss/src/views/operation/modelInfoConfig/index.vue new file mode 100644 index 0000000..faa41f0 --- /dev/null +++ b/f/web-kboss/src/views/operation/modelInfoConfig/index.vue @@ -0,0 +1,309 @@ + + + + +