diff --git a/b/cntoai/get_user_balance.dspy b/b/cntoai/get_user_balance.dspy index 53c6ce8..1a646d8 100644 --- a/b/cntoai/get_user_balance.dspy +++ b/b/cntoai/get_user_balance.dspy @@ -35,6 +35,8 @@ async def get_user_balance(ns={}): } orgid = await sor.R('organization', {'id': user[0]['orgid']}) balance = await getCustomerBalance(sor, orgid[0]['id']) + ns['balance'] = balance + debug(ns) return { 'status': 'ok', 'balance': balance diff --git a/b/cntoai/sync_model_to_llm.dspy b/b/cntoai/sync_model_to_llm.dspy index ac187af..c119057 100644 --- a/b/cntoai/sync_model_to_llm.dspy +++ b/b/cntoai/sync_model_to_llm.dspy @@ -1,5 +1,11 @@ async def sync_model_to_llm(ns={}): import aiohttp + + async def get_sync_orgid(sor): + if ns.get('orgid'): + return ns.get('orgid') + user_li = await sor.R('users', {'id': await get_user()}) + return user_li[0].get('orgid') if user_li else '' # 从数据库读取domain和Bearer token db = DBPools() @@ -13,6 +19,13 @@ async def sync_model_to_llm(ns={}): } domain = domain_li[0]['pvalue'] bearer_token = user_key[0]['pvalue'] + # sync_orgid = await get_sync_orgid(sor) + sync_orgid = "mIWUHBeeDM8mwAFPIQ8pS" # 固定写 + if not sync_orgid: + return { + 'status': False, + 'msg': '未找到当前用户所属机构' + } url = f"{domain}/llmage/list_llms" header = { @@ -33,29 +46,103 @@ async def sync_model_to_llm(ns={}): db = DBPools() async with db.sqlorContext('kboss') as sor: new_llms_count = 0 + sync_products_count = 0 new_llms_list = [] for category_list in result: for item in category_list.get('llms', []): + provider_orgcode = item.get('providerid') + provider_name = item.get('provider_name') or item.get('providerid') or '模型供应商' + if not provider_orgcode: + continue + + provider_ns = { + 'orgid': sync_orgid, + 'orgcode': provider_orgcode, + 'orgname': provider_name, + 'name': provider_name, + 'description': provider_name, + 'appid': item.get('upappid') or '', + 'providerkey': provider_orgcode, + 'state': '1', + 'start_date': '2026-07-01', + 'end_date': '9999-12-31', + 'settle_flowid': '0', + 'salemode': '0', + 'settle_mode': '3', + 'settle_datep': 'M00', + 'contactor': '连战', + 'contactor_phone': '13800138000', + 'emailaddress': 'lianzhan@cntoai.com', + 'address': '北京市海淀区', + 'org_type': '4' + } + provider_res = await path_call('../provider/providerAdd.dspy', provider_ns) + if not provider_res.get('status'): + return { + 'status': False, + 'msg': '供应商同步失败, %s, %s' % (provider_name, provider_res.get('msg')) + } + provider_data = provider_res.get('data') or {} + provider_orgid = provider_data.get('provider_orgid') or provider_data.get('orgid') + protocolid = provider_data.get('protocolid') + if not provider_orgid or not protocolid: + return { + 'status': False, + 'msg': '供应商同步失败, 未返回provider_orgid或protocolid, %s' % provider_name + } + + product_ns = { + 'providerid': provider_orgid, + 'providerpid': item.get('id'), + 'protocolid': protocolid, + 'name': item.get('name'), + 'product_code': item.get('model'), + 'reseller_orgid': sync_orgid, + 'label': item.get('catelogname') or category_list.get('catelogname'), + 'classify': 'A', + 'ptype': '21', + 'unit': 'hour', + 'discount': '1', + 'product_area': '北京' + } + product_res = await path_call('../product/productAdd.dspy', product_ns) + if not product_res.get('status'): + return { + 'status': False, + 'msg': '产品同步失败, %s, %s' % (item.get('name'), product_res.get('msg')) + } + sync_products_count += 1 + # 查找数据库中是否已经存在,不存在就插入 exist_llm = await sor.R('llm', {'model': item.get('model')}) if exist_llm: + # 更新 + exits_id = exist_llm[0].get('id') + await sor.U('llm', {'id': exits_id}, item) 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', + 'ownerid': item.get('ownerid'), 'enabled_date': item.get('enabled_date'), 'expired_date': item.get('expired_date'), - 'query_apiname': item.get('query_apiname') or '', + 'min_balance': item.get('min_balance'), + 'status': item.get('status'), + 'catelogname': item.get('catelogname'), + 'catelog_id': item.get('catelog_id'), + 'apiname': item.get('apiname'), + 'query_apiname': item.get('query_apiname'), 'query_period': item.get('query_period'), 'ppid': item.get('ppid'), + 'provider_name': item.get('provider_name'), + 'pricing_display': item.get('pricing_display'), + 'provider_iconurl': item.get('provider_iconurl') } new_llms_count += 1 new_llms_list.append(new_llms.get('model')) @@ -63,7 +150,7 @@ async def sync_model_to_llm(ns={}): return { 'status': True, - 'msg': f"sync_llm_list同步模型成功,共插入{new_llms_count}个模型,模型列表: {new_llms_list}" + 'msg': f"sync_llm_list同步模型成功,共同步{sync_products_count}个产品,共插入{new_llms_count}个模型,模型列表: {new_llms_list}" } except Exception as e: diff --git a/b/llm.sql b/b/llm.sql new file mode 100644 index 0000000..7bd517c --- /dev/null +++ b/b/llm.sql @@ -0,0 +1,145 @@ +# 创建表格命令 +CREATE TABLE llm ( + id VARCHAR(64) NOT NULL COMMENT '唯一标识', + name VARCHAR(50) NOT NULL COMMENT '模型名称', + model VARCHAR(50) NOT NULL COMMENT '模型代号', + description TEXT COMMENT '模型描述', + iconid VARCHAR(50) COMMENT '图标标识', + upappid VARCHAR(50) COMMENT '上层应用ID', + providerid VARCHAR(64) COMMENT '供应商ID', + ownerid VARCHAR(20) DEFAULT '0' COMMENT '所有者ID', + enabled_date DATE COMMENT '启用日期', + expired_date DATE COMMENT '过期日期', + min_balance INT DEFAULT 0 COMMENT '最低余额要求(元)', + status VARCHAR(20) COMMENT '状态(如published)', + catelogname VARCHAR(50) COMMENT '目录名称(如文生视频)', + catelog_id VARCHAR(20) COMMENT '目录ID', + apiname VARCHAR(50) COMMENT 'API名称', + query_apiname VARCHAR(50) COMMENT '查询API名称', + query_period INT COMMENT '查询周期(秒)', + ppid VARCHAR(64) COMMENT '定价计划ID', + provider_name VARCHAR(100) COMMENT '供应商名称', + pricing_display TEXT COMMENT '定价显示文本', + provider_iconurl VARCHAR(255) COMMENT '供应商图标URL', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='LLM模型信息表'; + + +# 厂商返回的部分数据示例 +[ + { + "catelogid": "2i2v", + "catelogname": "首尾帧生视频", + "llms": [ + { + "id": "r0ill7L0s6PtBS8mkmg-7", + "name": "viduq3-pro", + "model": "viduq3-pro", + "description": "vidu文生视频,viduq3-pro模型", + "iconid": "vidu", + "upappid": "vidu", + "providerid": "Bu3Z6OXANehbkD6OK5NJb", + "ownerid": "0", + "enabled_date": "2026-01-01", + "expired_date": "9999-12-31", + "min_balance": 10, + "status": "published", + "catelogname": "首尾帧生视频", + "catelog_id": "2i2v", + "apiname": "2i2v", + "query_apiname": "taskStatus", + "query_period": 30, + "ppid": "sF2gcl7UeANKtnZv8hfvL", + "provider_name": "北京生数科技有限公司", + "pricing_display": " - duration: 0.56 元/秒 ", + "provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=vidu" + } + ] + }, + { + "catelogid": "asr", + "catelogname": "语音识别", + "llms": [ + { + "id": "9glv3S-JSzxholxAblU14", + "name": "qwen3-asr-flash", + "model": "qwen3-asr-flash", + "description": "通义千问系列的录音文件识", + "iconid": "qwen", + "upappid": "tongyi-wan", + "providerid": "6fadgewjraOyvxC_EkHou", + "ownerid": "0", + "enabled_date": "2025-12-01", + "expired_date": "9999-12-31", + "min_balance": 10, + "status": "published", + "catelogname": "语音识别", + "catelog_id": "asr", + "apiname": "short_asr", + "query_apiname": null, + "query_period": null, + "ppid": "16pwEsQeHiOrxC0O6A2j6", + "provider_name": "阿里云", + "provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen" + } + ] + }, + { + "catelogid": "digital_human", + "catelogname": "数字人", + "llms": [ + { + "id": "KOsAo6YjTdU0KG7_sKUJF", + "name": "wan2.2-s2v", + "model": "wan2.2-s2v", + "description": "模型:wan2.2-s2v,输入音频和照片,用照片中的人生成数字人", + "iconid": "qwen", + "upappid": "tongyi-wan", + "providerid": "6fadgewjraOyvxC_EkHou", + "ownerid": "0", + "enabled_date": "2025-12-19", + "expired_date": "9999-12-31", + "min_balance": 10, + "status": "published", + "catelogname": "数字人", + "catelog_id": "digital_human", + "apiname": "avator", + "query_apiname": "avatorstatus", + "query_period": 10, + "ppid": "0B6aldoAej1PpZ4ydtrEZ", + "provider_name": "阿里云", + "pricing_display": " - duration: 0.5 元/秒 [分辨率=480P]\n - duration: 0.9 元/秒 [分辨率=720P]", + "provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen" + } + ] + }, + { + "catelogid": "i2v", + "catelogname": "图生视频", + "llms": [ + { + "id": "7y7kfYM0Xasizyi37WvAZ", + "name": "happyhorse-1.0-i2v", + "model": "happyhorse-1.0-i2v", + "description": "HappyHorse图生视频模型,以首帧图片为基础,支持通过文本描述进行引导,生成物理真实、运动流畅的视频。", + "iconid": "qwen", + "upappid": "tongyi-wan", + "providerid": "6fadgewjraOyvxC_EkHou", + "ownerid": "0", + "enabled_date": "2026-04-28", + "expired_date": "9999-12-31", + "min_balance": 10, + "status": "published", + "catelogname": "图生视频", + "catelog_id": "i2v", + "apiname": "i2v-happyhorse", + "query_apiname": "t2vstatus", + "query_period": 10, + "ppid": "knEwDP35DEz5oMAXyvwN3", + "provider_name": "阿里云", + "pricing_display": " - duration: 0.9 元/秒 [SR=720]\n - duration: 1.6 元/秒 [SR=1080]", + "provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen" + }, + ] + } +] \ No newline at end of file diff --git a/b/provider/providerAdd.dspy b/b/provider/providerAdd.dspy index b522eaa..ae3d96e 100644 --- a/b/provider/providerAdd.dspy +++ b/b/provider/providerAdd.dspy @@ -82,14 +82,6 @@ async def providerAdd(ns={}): 'status': False, 'msg': 'provider add failed, orgcode is empty' } - # filter by orgcode - res = await sor.R('organization', {}) - orgcodes = [orgc.get('orgcode') for orgc in res] - if org_code in orgcodes: - return { - 'status': False, - 'msg': 'provider add failed, orgcode has already exist' - } if not ns.get('orgid'): orgid_li = await sor.R('users', {'id': await get_user()}) orgid = orgid_li[0].get('orgid') if orgid_li else '' @@ -101,8 +93,33 @@ async def providerAdd(ns={}): else: orgid = ns.get('orgid') + # filter by orgcode; existing provider returns ids for idempotent callers + exist_org_li = await sor.R('organization', {'orgcode': org_code, 'del_flg': '0'}) + if len(exist_org_li) >= 1: + exist_org = exist_org_li[0] + exist_provider_li = await sor.R('provider', {'orgid': exist_org.get('id'), 'del_flg': '0'}) + if not exist_provider_li: + return { + 'status': False, + 'msg': 'provider add failed, orgcode has already exist but provider not found' + } + exist_protocol_li = await sor.R('saleprotocol', {'offer_orgid': exist_org.get('id'), 'bid_orgid': orgid, 'del_flg': '0'}) + if not exist_protocol_li: + exist_protocol_li = await sor.R('saleprotocol', {'offer_orgid': exist_org.get('id'), 'del_flg': '0'}) + return { + 'status': True, + 'msg': 'provider already exist', + 'data': { + 'orgid': exist_org.get('id'), + 'provider_orgid': exist_org.get('id'), + 'provider_id': exist_provider_li[0].get('id'), + 'protocolid': exist_protocol_li[0].get('id') if exist_protocol_li else None + } + } + # 供应商parentid 和 运营机构id相同 ns_org['parentid'] = orgid + ns_sale_mode['bid_orgid'] = orgid if not ns_provider.get('start_date'): ns_provider['start_date'] = time.strftime('%Y-%m-%d %H:%M:%S') @@ -115,7 +132,13 @@ async def providerAdd(ns={}): await openProviderAccounts(sor, orgid, ns_org['id']) return { "status": True, - "msg": "provider add success" + "msg": "provider add success", + "data": { + "orgid": ns_org['id'], + "provider_orgid": ns_org['id'], + "provider_id": ns_provider['id'], + "protocolid": ns_sale_mode['id'] + } } except Exception as e: raise e diff --git a/b/reseller/get_ipc_logo.dspy b/b/reseller/get_ipc_logo.dspy index 32da07f..ec9e29a 100644 --- a/b/reseller/get_ipc_logo.dspy +++ b/b/reseller/get_ipc_logo.dspy @@ -46,7 +46,7 @@ async def get_ipc_logo(ns={}): if '?domain_name=' in url_link: domain_url = url_link.split('?domain_name=')[1] # 如果是业主机构 - if ('ncmatch' in domain_url or '9527' in domain_url or '8889' in domain_url or '8891' in domain_url or domain_url in ['xterm.kaiyuancloud.cn','www.kaiyuancloud.cn', 'dev.kaiyuancloud.cn', 'dev.opencomputing.cn', 'test.kaiyuancloud.cn', 'localhost', 'www.opencomputing.cn', 'opencomputing.cn']) and '/domain/' not in url_link: + if ('ncmatch' in domain_url or 'zgcopc.opencomputing.cn' in domain_url or '9527' in domain_url or '8889' in domain_url or '8891' in domain_url or domain_url in ['xterm.kaiyuancloud.cn','www.kaiyuancloud.cn', 'dev.kaiyuancloud.cn', 'dev.opencomputing.cn', 'test.kaiyuancloud.cn', 'localhost', 'www.opencomputing.cn', 'opencomputing.cn']) and '/domain/' not in url_link: yezhu_info = (await sor.R('organization', {'org_type': '0'}))[0] domain_res = (await sor.R('params', {'pname': '业主机构域名'}))[0]['pvalue'] # yezhu = { @@ -91,6 +91,37 @@ async def get_ipc_logo(ns={}): ] } } + elif 'zgcopc.opencomputing.cn' in domain_url or '9527' in domain_url: + yezhu['logo'] = "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png" + yezhu['additional_msg'] = { + "home": { + "logoImg": "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png", + "bannerTitle": "中关村国际孵化器", + "adress": "北京市海淀区上地信息路2号", + "footerTitle": "中关村国际孵化器有限公司", + 'qrCode': 'https://zgcopc.opencomputing.cn/idfile?path=firstpagehot/ncmatch_inquiry.jpg', + 'footer_info': '京ICP备13007859号-1 北京中关村国际孵化器有限公司', + "copyright": "", + "domain_name": "zgcopc.opencomputing.cn", + "email": "zhangjian@bjzai.org.cn", + "license": "京ICP备13007859号-1", + "logo": "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png", + "mobile": "13601380912", + "publicsecurity": "11010502054007", + "businesslicense": "京ICP备13007859号-1" + }, + "about": { + "title": "开元数智", + "logo": "https://www.opencomputing.cn/idfile?path=logo_ncmatch.png", + "content": [ + "开元数智(北京)科技有限公司(以下简称“开元数智”)创立于2022年,总部位于北京中关村科技园区,现已形成以上海、南京、深圳、济南为区域枢纽的全国性服务网络。作为新型数字基础设施建设领域的创新企业,公司主营业务涵盖智能算力中心规划建设与运营、算力网络体系构建、企业级AI算力服务及大模型应用整体解决方案。", + "公司自主研发的NCMatch智能算力公共服务平台,作为行业领先的一站式人工智能算力服务解决方案,已构建云网算数智“五位一体”的服务体系。该平台深度整合算力资源调度系统、智能算法模型库及行业应用矩阵,为高等院校、科研院所、政企机构及人工智能开发企业提供专业级算力云服务,全面覆盖基础算力供给、大模型训练调优及产业智能化升级等多维度需求。", + "开元数智作为国家算力网络建设的重要参与单位,已获得工信部、教育部等部委直属机构指导,并与中关村数智人工智能产业联盟、国家超级计算中心、南京未来网络研究院等权威机构建立战略合作关系。公司深度参与全国超算互联网及全国一体化算力网国家级重点工程,在算力基础设施建设领域具备成熟的技术积累和项目实施经验。", + "开元数智(北京)科技有限公司合作的客户有:中国联通、中国电信、中国移动、未来网络、中信网络、国家超级计算济南中心、华为、阿里云、百度智能云、腾讯云、火山、智谱·AI、衔远科技、创新工场、竹间智能、深言科技、澜舟科技、商汤科技、MINMAX、云从科技、第四范式、深度求索、百川智能、Moonshot AI、昆仑万维、深蓝等。", + + ] + } + } elif 'kaiyuancloud' in domain_url or 'opencomputing' in domain_url or 'localhost' in domain_url: yezhu['additional_msg'] = { "home": { diff --git a/b/user/get_user_and_role.dspy b/b/user/get_user_and_role.dspy new file mode 100644 index 0000000..f209a11 --- /dev/null +++ b/b/user/get_user_and_role.dspy @@ -0,0 +1,90 @@ +def _parse_page(ns, default_page=1, default_size=20, max_size=200): + try: + current_page = int(ns.get('current_page') or ns.get('page') or default_page) + except (TypeError, ValueError): + current_page = default_page + try: + page_size = int(ns.get('page_size') or default_size) + except (TypeError, ValueError): + page_size = default_size + current_page = max(1, current_page) + page_size = max(1, min(page_size, max_size)) + return current_page, page_size, (current_page - 1) * page_size + + +async def get_user_and_role(ns={}): + """获取机构下所有用户及用户对应的所有角色""" + orgid = ns.get('orgid') + if not orgid: + return {'status': False, 'msg': '缺少 orgid'} + + current_page, page_size, offset = _parse_page(ns) + db = DBPools() + async with db.sqlorContext('kboss') as sor: + try: + where_sql = """ + u.del_flg='0' + AND ( + u.orgid=${orgid}$ + OR o.parentid=${orgid}$ + ) + """ + if ns.get('username'): + where_sql += " AND u.username LIKE '%%%%%s%%%%'" % ns.get('username') + if ns.get('mobile'): + where_sql += " AND u.mobile LIKE '%%%%%s%%%%'" % ns.get('mobile') + count_rows = await sor.sqlExe( + """ + SELECT COUNT(*) AS total_count + FROM users u + LEFT JOIN organization o ON u.orgid = o.id AND o.del_flg='0' + WHERE %s + """ % where_sql, + {'orgid': orgid}, + ) + total_count = count_rows[0]['total_count'] if count_rows else 0 + sql = """ + SELECT u.* + FROM users u + LEFT JOIN organization o ON u.orgid = o.id AND o.del_flg='0' + LEFT JOIN ( + SELECT userid, MAX(create_at) AS userrole_create_at + FROM userrole + WHERE del_flg='0' + GROUP BY userid + ) ur_sort ON u.id = ur_sort.userid + WHERE %s + ORDER BY ur_sort.userrole_create_at DESC, u.create_at DESC + LIMIT %d OFFSET %d + """ % (where_sql, page_size, offset) + users = await sor.sqlExe(sql, {'orgid': orgid}) + for user in users: + user['roles'] = [] + userroles = await sor.R('userrole', {'userid': user['id']}) + userroles = sorted(userroles, key=lambda item: item.get('create_at') or '', reverse=True) + for userrole in userroles: + if userrole.get('del_flg') == '1' or not userrole.get('roleid'): + continue + roles = await sor.R('role', {'id': userrole['roleid']}) + if len(roles) >= 1 and roles[0].get('del_flg') != '1': + user['roles'].append({ + 'id': roles[0].get('id'), + 'name': roles[0].get('role'), + }) + + return { + 'status': True, + 'msg': 'ok', + 'data': { + 'total_count': total_count, + 'current_page': current_page, + 'page_size': page_size, + 'rows': users, + }, + } + except Exception as e: + return {'status': False, 'msg': '获取用户角色失败, %s' % str(e)} + + +ret = await get_user_and_role(params_kw) +return ret