fix: sageapi local deployment and test server
- Fix health check sqlExe calls: add missing ns parameter - Fix accounting ID generation: use getID() instead of uuid4 (VARCHAR length) - Fix accounting request_id: normalize empty to NULL to avoid UNIQUE constraint violation - Fix test_server balance/update route: was incorrectly pointing to accounting handler - Add test_server.py: standalone aiohttp test server for local development - Update conf/config.yaml: local MySQL credentials (test/test) - Update db/schema.sql and scripts/generate_ddl.py for local testing - Fix router sync_status sqlExe call: add missing ns parameter - Fix sync uapi_sync: use correct table/column names
This commit is contained in:
parent
acb9674375
commit
40c480e488
@ -11,8 +11,8 @@ database:
|
|||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
port: 3306
|
port: 3306
|
||||||
dbname: "sageapi_db"
|
dbname: "sageapi_db"
|
||||||
user: "root"
|
user: "test"
|
||||||
password: ""
|
password: "test"
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
upstream:
|
upstream:
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
-- SageAPI DDL (auto-generated)
|
-- SageAPI DDL (auto-generated)
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `accounting_records` (
|
CREATE TABLE IF NOT EXISTS `accounting_records` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键',
|
||||||
`customer_id` VARCHAR(32) NOT NULL DEFAULT COMMENT '客户ID',
|
`customer_id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '客户ID',
|
||||||
`llmid` VARCHAR(32) NULL DEFAULT COMMENT '模型ID',
|
`llmid` VARCHAR(32) NULL DEFAULT '' COMMENT '模型ID',
|
||||||
`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称',
|
`model_name` VARCHAR(128) NULL DEFAULT '' COMMENT '模型名称',
|
||||||
`pricing_id` VARCHAR(32) NULL DEFAULT COMMENT '定价ID',
|
`pricing_id` VARCHAR(32) NULL DEFAULT '' COMMENT '定价ID',
|
||||||
`input_tokens` BIGINT NULL COMMENT '输入token数',
|
`input_tokens` BIGINT NULL COMMENT '输入token数',
|
||||||
`output_tokens` BIGINT NULL COMMENT '输出token数',
|
`output_tokens` BIGINT NULL COMMENT '输出token数',
|
||||||
`total_tokens` BIGINT NULL COMMENT '总token数',
|
`total_tokens` BIGINT NULL COMMENT '总token数',
|
||||||
`quantity` DECIMAL(15,4) NULL COMMENT '用量(图片数/分钟数等)',
|
`quantity` DECIMAL(15,4) NULL COMMENT '用量(图片数/分钟数等)',
|
||||||
`amount` DECIMAL(15,6) NOT NULL DEFAULT 0.0 COMMENT '金额',
|
`amount` DECIMAL(15,6) NOT NULL DEFAULT 0.0 COMMENT '金额',
|
||||||
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
|
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
|
||||||
`request_id` VARCHAR(64) NULL DEFAULT COMMENT '请求ID(幂等键)',
|
`request_id` VARCHAR(64) NULL DEFAULT '' COMMENT '请求ID(幂等键)',
|
||||||
`transno` VARCHAR(64) NULL DEFAULT COMMENT '事务号',
|
`transno` VARCHAR(64) NULL DEFAULT '' COMMENT '事务号',
|
||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/accounted/failed',
|
`status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/accounted/failed',
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
@ -31,14 +31,14 @@ ALTER TABLE `accounting_records` ADD INDEX `idx_status` (`status`);
|
|||||||
ALTER TABLE `accounting_records` ADD INDEX `idx_created_at` (`created_at`);
|
ALTER TABLE `accounting_records` ADD INDEX `idx_created_at` (`created_at`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `customer_balance` (
|
CREATE TABLE IF NOT EXISTS `customer_balance` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,即 customer_id',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键,即 customer_id',
|
||||||
`balance` DECIMAL(15,4) NOT NULL DEFAULT 0.0 COMMENT '当前余额',
|
`balance` DECIMAL(15,4) NOT NULL DEFAULT 0.0 COMMENT '当前余额',
|
||||||
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
|
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
|
||||||
`credit_limit` DECIMAL(15,4) NULL COMMENT '信用额度',
|
`credit_limit` DECIMAL(15,4) NULL COMMENT '信用额度',
|
||||||
`last_recharge` DATETIME NULL COMMENT '最后充值时间',
|
`last_recharge` DATETIME NULL COMMENT '最后充值时间',
|
||||||
`last_consumption` DATETIME NULL COMMENT '最后消费时间',
|
`last_consumption` DATETIME NULL COMMENT '最后消费时间',
|
||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/suspended/arrears',
|
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/suspended/arrears',
|
||||||
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
|
`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号',
|
||||||
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存更新时间',
|
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存更新时间',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@ -48,16 +48,16 @@ ALTER TABLE `customer_balance` ADD INDEX `idx_status` (`status`);
|
|||||||
ALTER TABLE `customer_balance` ADD INDEX `idx_balance` (`balance`);
|
ALTER TABLE `customer_balance` ADD INDEX `idx_balance` (`balance`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `llmage_cache` (
|
CREATE TABLE IF NOT EXISTS `llmage_cache` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键',
|
||||||
`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID',
|
`llmid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '关联模型ID',
|
||||||
`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称',
|
`model_name` VARCHAR(128) NULL DEFAULT '' COMMENT '模型名称',
|
||||||
`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID',
|
`upappid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上游应用ID',
|
||||||
`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称',
|
`apiname` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'API名称',
|
||||||
`api_url` VARCHAR(512) NULL DEFAULT COMMENT 'API端点URL',
|
`api_url` VARCHAR(512) NULL DEFAULT '' COMMENT 'API端点URL',
|
||||||
`api_params` TEXT NULL COMMENT 'API参数配置JSON',
|
`api_params` TEXT NULL COMMENT 'API参数配置JSON',
|
||||||
`model_params` TEXT NULL COMMENT '模型参数配置JSON(max_tokens, temperature等)',
|
`model_params` TEXT NULL COMMENT '模型参数配置JSON(max_tokens, temperature等)',
|
||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive',
|
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive',
|
||||||
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
|
`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号',
|
||||||
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@ -69,10 +69,10 @@ ALTER TABLE `llmage_cache` ADD INDEX `idx_upappid` (`upappid`);
|
|||||||
ALTER TABLE `llmage_cache` ADD INDEX `idx_apiname` (`apiname`);
|
ALTER TABLE `llmage_cache` ADD INDEX `idx_apiname` (`apiname`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `pricing_cache` (
|
CREATE TABLE IF NOT EXISTS `pricing_cache` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 pricing_program id (ppid)',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键,对应 pricing_program id (ppid)',
|
||||||
`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID',
|
`llmid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '关联模型ID',
|
||||||
`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称',
|
`model_name` VARCHAR(128) NULL DEFAULT '' COMMENT '模型名称',
|
||||||
`pricing_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '计费类型: token/image/video/audio',
|
`pricing_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '计费类型: token/image/video/audio',
|
||||||
`input_price` DECIMAL(10,6) NULL COMMENT '输入单价(每千token)',
|
`input_price` DECIMAL(10,6) NULL COMMENT '输入单价(每千token)',
|
||||||
`output_price` DECIMAL(10,6) NULL COMMENT '输出单价(每千token)',
|
`output_price` DECIMAL(10,6) NULL COMMENT '输出单价(每千token)',
|
||||||
`unit_price` DECIMAL(10,6) NULL COMMENT '统一单价(按次/按图/按分钟等)',
|
`unit_price` DECIMAL(10,6) NULL COMMENT '统一单价(按次/按图/按分钟等)',
|
||||||
@ -80,7 +80,7 @@ CREATE TABLE IF NOT EXISTS `pricing_cache` (
|
|||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/deprecated',
|
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/deprecated',
|
||||||
`effective_from` DATETIME NULL COMMENT '生效时间',
|
`effective_from` DATETIME NULL COMMENT '生效时间',
|
||||||
`effective_to` DATETIME NULL COMMENT '失效时间',
|
`effective_to` DATETIME NULL COMMENT '失效时间',
|
||||||
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
|
`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号',
|
||||||
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@ -92,11 +92,11 @@ ALTER TABLE `pricing_cache` ADD INDEX `idx_pricing_type` (`pricing_type`);
|
|||||||
ALTER TABLE `pricing_cache` ADD INDEX `idx_status` (`status`);
|
ALTER TABLE `pricing_cache` ADD INDEX `idx_status` (`status`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `sync_state` (
|
CREATE TABLE IF NOT EXISTS `sync_state` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键',
|
||||||
`entity_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '实体类型: users/pricing/llmage/uapi',
|
`entity_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '实体类型: users/pricing/llmage/uapi',
|
||||||
`entity_id` VARCHAR(64) NULL DEFAULT COMMENT '实体标识(全量同步时为空)',
|
`entity_id` VARCHAR(64) NULL DEFAULT '' COMMENT '实体标识(全量同步时为空)',
|
||||||
`last_sync_time` DATETIME NULL COMMENT '最后同步时间',
|
`last_sync_time` DATETIME NULL COMMENT '最后同步时间',
|
||||||
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT 'Sage返回的版本标识',
|
`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT 'Sage返回的版本标识',
|
||||||
`sync_status` VARCHAR(16) NOT NULL DEFAULT 'success' COMMENT '同步状态: success/pending/failed',
|
`sync_status` VARCHAR(16) NOT NULL DEFAULT 'success' COMMENT '同步状态: success/pending/failed',
|
||||||
`error_msg` TEXT NULL COMMENT '失败原因',
|
`error_msg` TEXT NULL COMMENT '失败原因',
|
||||||
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
|
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||||
@ -112,16 +112,16 @@ ALTER TABLE `sync_state` ADD UNIQUE `idx_entity_type_id` (`entity_type`,`entity_
|
|||||||
ALTER TABLE `sync_state` ADD INDEX `idx_sync_status` (`sync_status`);
|
ALTER TABLE `sync_state` ADD INDEX `idx_sync_status` (`sync_status`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `uapi_cache` (
|
CREATE TABLE IF NOT EXISTS `uapi_cache` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键',
|
||||||
`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID',
|
`upappid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上游应用ID',
|
||||||
`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称',
|
`apiname` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'API名称',
|
||||||
`method` VARCHAR(16) NULL DEFAULT 'POST' COMMENT 'HTTP方法',
|
`method` VARCHAR(16) NULL DEFAULT 'POST' COMMENT 'HTTP方法',
|
||||||
`endpoint` VARCHAR(512) NULL DEFAULT COMMENT 'API端点',
|
`endpoint` VARCHAR(512) NULL DEFAULT '' COMMENT 'API端点',
|
||||||
`auth_type` VARCHAR(32) NULL DEFAULT 'bearer' COMMENT '认证类型',
|
`auth_type` VARCHAR(32) NULL DEFAULT 'bearer' COMMENT '认证类型',
|
||||||
`rate_limit` INT NULL COMMENT '速率限制(次/分钟)',
|
`rate_limit` INT NULL COMMENT '速率限制(次/分钟)',
|
||||||
`description` TEXT NULL COMMENT 'API描述',
|
`description` TEXT NULL COMMENT 'API描述',
|
||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态',
|
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态',
|
||||||
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
|
`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号',
|
||||||
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@ -131,16 +131,16 @@ ALTER TABLE `uapi_cache` ADD UNIQUE `idx_upappid_apiname` (`upappid`,`apiname`);
|
|||||||
ALTER TABLE `uapi_cache` ADD INDEX `idx_status` (`status`);
|
ALTER TABLE `uapi_cache` ADD INDEX `idx_status` (`status`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `users_cache` (
|
CREATE TABLE IF NOT EXISTS `users_cache` (
|
||||||
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 users 表 id',
|
`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键,对应 users 表 id',
|
||||||
`username` VARCHAR(128) NOT NULL DEFAULT COMMENT '用户名',
|
`username` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||||
`orgid` VARCHAR(32) NULL DEFAULT COMMENT '组织ID',
|
`orgid` VARCHAR(32) NULL DEFAULT '' COMMENT '组织ID',
|
||||||
`orgname` VARCHAR(255) NULL DEFAULT COMMENT '组织名称',
|
`orgname` VARCHAR(255) NULL DEFAULT '' COMMENT '组织名称',
|
||||||
`email` VARCHAR(128) NULL DEFAULT COMMENT '邮箱',
|
`email` VARCHAR(128) NULL DEFAULT '' COMMENT '邮箱',
|
||||||
`phone` VARCHAR(32) NULL DEFAULT COMMENT '手机号',
|
`phone` VARCHAR(32) NULL DEFAULT '' COMMENT '手机号',
|
||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/suspended',
|
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/suspended',
|
||||||
`created_at` DATETIME NULL COMMENT '创建时间',
|
`created_at` DATETIME NULL COMMENT '创建时间',
|
||||||
`updated_at` DATETIME NULL COMMENT '更新时间',
|
`updated_at` DATETIME NULL COMMENT '更新时间',
|
||||||
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
|
`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号',
|
||||||
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -9,10 +9,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import uuid
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from appPublic.log import debug, error
|
from appPublic.log import debug, error
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
from sqlor.dbpools import DBPools
|
from sqlor.dbpools import DBPools
|
||||||
from ahserver.serverenv import ServerEnv
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
@ -41,9 +41,13 @@ async def create_accounting_record(
|
|||||||
result['error'] = 'No database configured for sageapi module'
|
result['error'] = 'No database configured for sageapi module'
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
record_id = request_id or str(uuid.uuid4())
|
record_id = request_id or getID()
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Normalize empty request_id to None to avoid UNIQUE constraint violation
|
||||||
|
if not request_id:
|
||||||
|
request_id = None
|
||||||
|
|
||||||
# Check idempotency
|
# Check idempotency
|
||||||
if request_id:
|
if request_id:
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
|||||||
@ -47,7 +47,7 @@ async def readiness_check() -> str:
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
rows = await sor.sqlExe('SELECT 1 as ping')
|
rows = await sor.sqlExe('SELECT 1 as ping', {})
|
||||||
result['checks']['cache_db'] = {
|
result['checks']['cache_db'] = {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'dbname': dbname,
|
'dbname': dbname,
|
||||||
@ -63,7 +63,7 @@ async def readiness_check() -> str:
|
|||||||
try:
|
try:
|
||||||
from sqlor.dbpools import get_sor_context
|
from sqlor.dbpools import get_sor_context
|
||||||
async with get_sor_context(env, 'sage') as sor:
|
async with get_sor_context(env, 'sage') as sor:
|
||||||
rows = await sor.sqlExe('SELECT 1 as ping')
|
rows = await sor.sqlExe('SELECT 1 as ping', {})
|
||||||
result['checks']['sage_db'] = {'status': 'ok'}
|
result['checks']['sage_db'] = {'status': 'ok'}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'readiness_check sage_db error: {e}')
|
error(f'readiness_check sage_db error: {e}')
|
||||||
@ -80,7 +80,7 @@ async def readiness_check() -> str:
|
|||||||
FROM sync_state
|
FROM sync_state
|
||||||
ORDER BY last_sync_time DESC
|
ORDER BY last_sync_time DESC
|
||||||
"""
|
"""
|
||||||
rows = await sor.sqlExe(sql)
|
rows = await sor.sqlExe(sql, {})
|
||||||
if isinstance(rows, list):
|
if isinstance(rows, list):
|
||||||
result['checks']['sync_status'] = {
|
result['checks']['sync_status'] = {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
|
|||||||
@ -121,7 +121,7 @@ async def _sync_status() -> str:
|
|||||||
dbname = env.get_module_dbname('sageapi')
|
dbname = env.get_module_dbname('sageapi')
|
||||||
sql = "SELECT entity_type, sync_status, last_sync_time, error_msg FROM sync_state ORDER BY entity_type"
|
sql = "SELECT entity_type, sync_status, last_sync_time, error_msg FROM sync_state ORDER BY entity_type"
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
rows = await sor.sqlExe(sql)
|
rows = await sor.sqlExe(sql, {})
|
||||||
result['data'] = rows if isinstance(rows, list) else rows.get('rows', [])
|
result['data'] = rows if isinstance(rows, list) else rows.get('rows', [])
|
||||||
result['success'] = True
|
result['success'] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Binary file not shown.
@ -16,7 +16,7 @@ from .base_sync import BaseSync
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UAPISync(BaseSync):
|
class UapiSync(BaseSync):
|
||||||
MODULE_NAME = "uapi"
|
MODULE_NAME = "uapi"
|
||||||
SOURCE_DBNAME = "sage"
|
SOURCE_DBNAME = "sage"
|
||||||
CACHE_DBNAME = "sageapi"
|
CACHE_DBNAME = "sageapi"
|
||||||
|
|||||||
@ -25,10 +25,15 @@ def generate_ddl(models_dir='models', output_path='db/schema.sql'):
|
|||||||
nullable = 'NULL' if field.get('nullable', True) else 'NOT NULL'
|
nullable = 'NULL' if field.get('nullable', True) else 'NOT NULL'
|
||||||
default = ''
|
default = ''
|
||||||
if field.get('default') is not None:
|
if field.get('default') is not None:
|
||||||
if isinstance(field['default'], str) and field['default'] not in ('CURRENT_TIMESTAMP', 'NULL', ''):
|
d = field['default']
|
||||||
default = f"DEFAULT '{field['default']}'"
|
if d == 'CURRENT_TIMESTAMP':
|
||||||
|
default = "DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
elif d == '':
|
||||||
|
default = "DEFAULT ''"
|
||||||
|
elif isinstance(d, str):
|
||||||
|
default = f"DEFAULT '{d}'"
|
||||||
else:
|
else:
|
||||||
default = f"DEFAULT {field['default']}"
|
default = f"DEFAULT {d}"
|
||||||
comment = f"COMMENT '{field.get('comment', '')}'"
|
comment = f"COMMENT '{field.get('comment', '')}'"
|
||||||
fname = f'{btick}{field["name"]}{btick}'
|
fname = f'{btick}{field["name"]}{btick}'
|
||||||
col_defs.append(f' {fname} {field["type"]} {nullable} {default} {comment}'.strip())
|
col_defs.append(f' {fname} {field["type"]} {nullable} {default} {comment}'.strip())
|
||||||
|
|||||||
171
test_server.py
Normal file
171
test_server.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Standalone test server for SageAPI.
|
||||||
|
|
||||||
|
Uses aiohttp (already in Sage venv) to expose all SageAPI endpoints
|
||||||
|
without requiring the full Sage framework. Uses local MySQL (127.0.0.1).
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Setup paths
|
||||||
|
SAGE_DIR = '/home/hermesai/repos/sage'
|
||||||
|
SAGEAPI_DIR = '/home/hermesai/repos/sageapi'
|
||||||
|
sys.path.insert(0, SAGEAPI_DIR)
|
||||||
|
sys.path.insert(0, SAGE_DIR)
|
||||||
|
os.chdir(SAGE_DIR)
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from appPublic.folderUtils import ProgramPath
|
||||||
|
ProgramPath()
|
||||||
|
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from appPublic.dictObject import DictObject
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Build a local database config that works on this machine
|
||||||
|
# sage DB: override host from 'db' to '127.0.0.1'
|
||||||
|
# sageapi_db: add our test database
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
config = getConfig('.')
|
||||||
|
|
||||||
|
# config.databases is already a DictObject from JsonConfig, so .kwargs access works
|
||||||
|
# Just override the sage DB host to use local MySQL
|
||||||
|
sage_db = config.databases.get('sage')
|
||||||
|
if sage_db and 'kwargs' in sage_db:
|
||||||
|
sage_db['kwargs']['host'] = '127.0.0.1'
|
||||||
|
# Override password to encrypted 'test' (the original encrypted pw decodes to empty)
|
||||||
|
sage_db['kwargs']['password'] = 'xGatnL1idCnFRCe4FaIWRQ=='
|
||||||
|
|
||||||
|
# Add sageapi_db as DictObject (nested dicts need DictObject for .kwargs access)
|
||||||
|
# Password must be encrypted (sqlor.SQLor.unpassword decrypts it)
|
||||||
|
sageapi_db_cfg = DictObject(
|
||||||
|
driver='mysql',
|
||||||
|
kwargs=DictObject(
|
||||||
|
host='127.0.0.1',
|
||||||
|
port=3306,
|
||||||
|
user='test',
|
||||||
|
password='xGatnL1idCnFRCe4FaIWRQ==', # encrypted 'test'
|
||||||
|
db='sageapi_db',
|
||||||
|
charset='utf8mb4',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
config.databases['sageapi_db'] = sageapi_db_cfg
|
||||||
|
|
||||||
|
# Initialize DBPools with the (now modified) config
|
||||||
|
DBPools(config.databases)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Monkey-patch ServerEnv.get_module_dbname
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
_serverenv_cls = getattr(ServerEnv, 'klass', ServerEnv)
|
||||||
|
|
||||||
|
def _patched_get_module_dbname(self, module):
|
||||||
|
if module == 'sageapi':
|
||||||
|
return 'sageapi_db'
|
||||||
|
return 'sage'
|
||||||
|
|
||||||
|
_serverenv_cls.get_module_dbname = _patched_get_module_dbname
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Import sageapi modules (must be after DBPools init and patching)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from sageapi.api.health import health_check, readiness_check
|
||||||
|
from sageapi.api.balance import get_customer_balance, update_customer_balance
|
||||||
|
from sageapi.api.accounting import create_accounting_record, query_accounting_records
|
||||||
|
from sageapi.api.users import query_users, get_user_by_id
|
||||||
|
from sageapi.api.pricing import query_pricing, get_pricing_by_llmid
|
||||||
|
|
||||||
|
_START_TIME = time.time()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Route handlers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
async def handle_health(request):
|
||||||
|
result = await health_check()
|
||||||
|
return web.Response(text=result, content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_readiness(request):
|
||||||
|
result = await readiness_check()
|
||||||
|
return web.Response(text=result, content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_balance_get(request):
|
||||||
|
customer_id = request.query.get('customer_id', '')
|
||||||
|
result = await get_customer_balance(customer_id=customer_id)
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_accounting_create(request):
|
||||||
|
body = await request.json()
|
||||||
|
result = await create_accounting_record(**body)
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_balance_update(request):
|
||||||
|
body = await request.json()
|
||||||
|
customer_id = body.get('customer_id', '')
|
||||||
|
balance = body.get('balance', 0.0)
|
||||||
|
result = await update_customer_balance(customer_id=customer_id, balance=balance)
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_accounting_query(request):
|
||||||
|
customer_id = request.query.get('customer_id', '')
|
||||||
|
result = await query_accounting_records(customer_id=customer_id)
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_users_query(request):
|
||||||
|
result = await query_users()
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_pricing_query(request):
|
||||||
|
result = await query_pricing()
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_sync_trigger(request):
|
||||||
|
from sageapi.sync.base_sync import run_all_syncs
|
||||||
|
result = await run_all_syncs()
|
||||||
|
return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json')
|
||||||
|
|
||||||
|
async def handle_sync_status(request):
|
||||||
|
result = {'success': False, 'data': []}
|
||||||
|
try:
|
||||||
|
env = ServerEnv()
|
||||||
|
dbname = env.get_module_dbname('sageapi')
|
||||||
|
sql = "SELECT entity_type, sync_status, last_sync_time, error_msg FROM sync_state ORDER BY entity_type"
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
rows = await sor.sqlExe(sql, {})
|
||||||
|
result['data'] = rows if isinstance(rows, list) else rows.get('rows', [])
|
||||||
|
result['success'] = True
|
||||||
|
except Exception as e:
|
||||||
|
result['error'] = str(e)
|
||||||
|
return web.Response(text=json.dumps(result, ensure_ascii=False, default=str), content_type='application/json')
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Setup routes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def setup_app():
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get('/api/v1/health', handle_health)
|
||||||
|
app.router.add_get('/api/v1/health/ready', handle_readiness)
|
||||||
|
app.router.add_get('/api/v1/balance', handle_balance_get)
|
||||||
|
app.router.add_post('/api/v1/balance/update', handle_balance_update)
|
||||||
|
app.router.add_post('/api/v1/accounting', handle_accounting_create)
|
||||||
|
app.router.add_get('/api/v1/accounting', handle_accounting_query)
|
||||||
|
app.router.add_get('/api/v1/users', handle_users_query)
|
||||||
|
app.router.add_get('/api/v1/pricing', handle_pricing_query)
|
||||||
|
app.router.add_post('/api/v1/admin/sync', handle_sync_trigger)
|
||||||
|
app.router.add_get('/api/v1/admin/sync/status', handle_sync_status)
|
||||||
|
return app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = setup_app()
|
||||||
|
print("Starting SageAPI test server on http://127.0.0.1:18080")
|
||||||
|
print("Endpoints:")
|
||||||
|
for route in app.router.routes():
|
||||||
|
methods = route.method if hasattr(route, 'method') else 'GET'
|
||||||
|
path = route.resource.canonical if route.resource else str(route)
|
||||||
|
print(f" {methods} {path}")
|
||||||
|
web.run_app(app, host='127.0.0.1', port=18080, print=None)
|
||||||
Loading…
x
Reference in New Issue
Block a user