Compare commits

...

2 Commits

Author SHA1 Message Date
Hermes Agent
781216e11a refactor(models): convert to json format per database-table-definition-spec 2026-05-27 13:23:31 +08:00
Hermes Agent
40c480e488 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
2026-05-20 22:46:05 +08:00
18 changed files with 1113 additions and 856 deletions

View File

@ -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:

View File

@ -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 '模型参数配置JSONmax_tokens, temperature等', `model_params` TEXT NULL COMMENT '模型参数配置JSONmax_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;

View File

@ -3,119 +3,134 @@
{ {
"name": "accounting_records", "name": "accounting_records",
"title": "记账记录", "title": "记账记录",
"primary": "id", "primary": [
"id"
],
"classification": "business" "classification": "business"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键" "comment": "主键",
"length": 32
}, },
{ {
"name": "customer_id", "name": "customer_id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "客户ID" "comment": "客户ID",
"length": 32
}, },
{ {
"name": "llmid", "name": "llmid",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "模型ID" "comment": "模型ID",
"length": 32
}, },
{ {
"name": "model_name", "name": "model_name",
"type": "VARCHAR(128)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "模型名称" "comment": "模型名称",
"length": 128
}, },
{ {
"name": "pricing_id", "name": "pricing_id",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "定价ID" "comment": "定价ID",
"length": 32
}, },
{ {
"name": "input_tokens", "name": "input_tokens",
"type": "BIGINT", "type": "long",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "输入token数" "comment": "输入token数"
}, },
{ {
"name": "output_tokens", "name": "output_tokens",
"type": "BIGINT", "type": "long",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "输出token数" "comment": "输出token数"
}, },
{ {
"name": "total_tokens", "name": "total_tokens",
"type": "BIGINT", "type": "long",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "总token数" "comment": "总token数"
}, },
{ {
"name": "quantity", "name": "quantity",
"type": "DECIMAL(15,4)", "type": "double",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "用量(图片数/分钟数等)" "comment": "用量(图片数/分钟数等)",
"length": 15,
"dec": 4
}, },
{ {
"name": "amount", "name": "amount",
"type": "DECIMAL(15,6)", "type": "double",
"nullable": false, "nullable": false,
"default": 0.0, "default": 0.0,
"comment": "金额" "comment": "金额",
"length": 15,
"dec": 6
}, },
{ {
"name": "currency", "name": "currency",
"type": "VARCHAR(8)", "type": "str",
"nullable": false, "nullable": false,
"default": "CNY", "default": "CNY",
"comment": "货币单位" "comment": "货币单位",
"length": 8
}, },
{ {
"name": "request_id", "name": "request_id",
"type": "VARCHAR(64)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "请求ID幂等键" "comment": "请求ID幂等键",
"length": 64
}, },
{ {
"name": "transno", "name": "transno",
"type": "VARCHAR(64)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "事务号" "comment": "事务号",
"length": 64
}, },
{ {
"name": "status", "name": "status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "pending", "default": "pending",
"comment": "状态: pending/accounted/failed" "comment": "状态: pending/accounted/failed",
"length": 16
}, },
{ {
"name": "created_at", "name": "created_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "创建时间" "comment": "创建时间"
}, },
{ {
"name": "updated_at", "name": "updated_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "更新时间" "comment": "更新时间"

View File

@ -3,70 +3,80 @@
{ {
"name": "customer_balance", "name": "customer_balance",
"title": "客户余额缓存", "title": "客户余额缓存",
"primary": "id", "primary": [
"id"
],
"classification": "cache" "classification": "cache"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键,即 customer_id" "comment": "主键,即 customer_id",
"length": 32
}, },
{ {
"name": "balance", "name": "balance",
"type": "DECIMAL(15,4)", "type": "double",
"nullable": false, "nullable": false,
"default": 0.0, "default": 0.0,
"comment": "当前余额" "comment": "当前余额",
"length": 15,
"dec": 4
}, },
{ {
"name": "currency", "name": "currency",
"type": "VARCHAR(8)", "type": "str",
"nullable": false, "nullable": false,
"default": "CNY", "default": "CNY",
"comment": "货币单位" "comment": "货币单位",
"length": 8
}, },
{ {
"name": "credit_limit", "name": "credit_limit",
"type": "DECIMAL(15,4)", "type": "double",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "信用额度" "comment": "信用额度",
"length": 15,
"dec": 4
}, },
{ {
"name": "last_recharge", "name": "last_recharge",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "最后充值时间" "comment": "最后充值时间"
}, },
{ {
"name": "last_consumption", "name": "last_consumption",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "最后消费时间" "comment": "最后消费时间"
}, },
{ {
"name": "status", "name": "status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "active", "default": "active",
"comment": "状态: active/suspended/arrears" "comment": "状态: active/suspended/arrears",
"length": 16
}, },
{ {
"name": "sync_version", "name": "sync_version",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "同步版本号" "comment": "同步版本号",
"length": 32
}, },
{ {
"name": "cached_at", "name": "cached_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "缓存更新时间" "comment": "缓存更新时间"

View File

@ -3,84 +3,94 @@
{ {
"name": "llmage_cache", "name": "llmage_cache",
"title": "模型API映射缓存", "title": "模型API映射缓存",
"primary": "id", "primary": [
"id"
],
"classification": "cache" "classification": "cache"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键" "comment": "主键",
"length": 32
}, },
{ {
"name": "llmid", "name": "llmid",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "关联模型ID" "comment": "关联模型ID",
"length": 32
}, },
{ {
"name": "model_name", "name": "model_name",
"type": "VARCHAR(128)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "模型名称" "comment": "模型名称",
"length": 128
}, },
{ {
"name": "upappid", "name": "upappid",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "上游应用ID" "comment": "上游应用ID",
"length": 32
}, },
{ {
"name": "apiname", "name": "apiname",
"type": "VARCHAR(128)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "API名称" "comment": "API名称",
"length": 128
}, },
{ {
"name": "api_url", "name": "api_url",
"type": "VARCHAR(512)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "API端点URL" "comment": "API端点URL",
"length": 512
}, },
{ {
"name": "api_params", "name": "api_params",
"type": "TEXT", "type": "text",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "API参数配置JSON" "comment": "API参数配置JSON"
}, },
{ {
"name": "model_params", "name": "model_params",
"type": "TEXT", "type": "text",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "模型参数配置JSONmax_tokens, temperature等" "comment": "模型参数配置JSONmax_tokens, temperature等"
}, },
{ {
"name": "status", "name": "status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "active", "default": "active",
"comment": "状态: active/inactive" "comment": "状态: active/inactive",
"length": 16
}, },
{ {
"name": "sync_version", "name": "sync_version",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "同步版本号" "comment": "同步版本号",
"length": 32
}, },
{ {
"name": "cached_at", "name": "cached_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "缓存写入时间" "comment": "缓存写入时间"

View File

@ -3,98 +3,113 @@
{ {
"name": "pricing_cache", "name": "pricing_cache",
"title": "定价数据缓存", "title": "定价数据缓存",
"primary": "id", "primary": [
"id"
],
"classification": "cache" "classification": "cache"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键,对应 pricing_program id (ppid)" "comment": "主键,对应 pricing_program id (ppid)",
"length": 32
}, },
{ {
"name": "llmid", "name": "llmid",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "关联模型ID" "comment": "关联模型ID",
"length": 32
}, },
{ {
"name": "model_name", "name": "model_name",
"type": "VARCHAR(128)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "模型名称" "comment": "模型名称",
"length": 128
}, },
{ {
"name": "pricing_type", "name": "pricing_type",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "计费类型: token/image/video/audio" "comment": "计费类型: token/image/video/audio",
"length": 32
}, },
{ {
"name": "input_price", "name": "input_price",
"type": "DECIMAL(10,6)", "type": "double",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "输入单价每千token" "comment": "输入单价每千token",
"length": 10,
"dec": 6
}, },
{ {
"name": "output_price", "name": "output_price",
"type": "DECIMAL(10,6)", "type": "double",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "输出单价每千token" "comment": "输出单价每千token",
"length": 10,
"dec": 6
}, },
{ {
"name": "unit_price", "name": "unit_price",
"type": "DECIMAL(10,6)", "type": "double",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "统一单价(按次/按图/按分钟等)" "comment": "统一单价(按次/按图/按分钟等)",
"length": 10,
"dec": 6
}, },
{ {
"name": "currency", "name": "currency",
"type": "VARCHAR(8)", "type": "str",
"nullable": false, "nullable": false,
"default": "CNY", "default": "CNY",
"comment": "货币单位" "comment": "货币单位",
"length": 8
}, },
{ {
"name": "status", "name": "status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "active", "default": "active",
"comment": "状态: active/inactive/deprecated" "comment": "状态: active/inactive/deprecated",
"length": 16
}, },
{ {
"name": "effective_from", "name": "effective_from",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "生效时间" "comment": "生效时间"
}, },
{ {
"name": "effective_to", "name": "effective_to",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "失效时间" "comment": "失效时间"
}, },
{ {
"name": "sync_version", "name": "sync_version",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "同步版本号" "comment": "同步版本号",
"length": 32
}, },
{ {
"name": "cached_at", "name": "cached_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "缓存写入时间" "comment": "缓存写入时间"

View File

@ -3,77 +3,84 @@
{ {
"name": "sync_state", "name": "sync_state",
"title": "同步状态跟踪", "title": "同步状态跟踪",
"primary": "id", "primary": [
"id"
],
"classification": "system" "classification": "system"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键" "comment": "主键",
"length": 32
}, },
{ {
"name": "entity_type", "name": "entity_type",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "实体类型: users/pricing/llmage/uapi" "comment": "实体类型: users/pricing/llmage/uapi",
"length": 32
}, },
{ {
"name": "entity_id", "name": "entity_id",
"type": "VARCHAR(64)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "实体标识(全量同步时为空)" "comment": "实体标识(全量同步时为空)",
"length": 64
}, },
{ {
"name": "last_sync_time", "name": "last_sync_time",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "最后同步时间" "comment": "最后同步时间"
}, },
{ {
"name": "sync_version", "name": "sync_version",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "Sage返回的版本标识" "comment": "Sage返回的版本标识",
"length": 32
}, },
{ {
"name": "sync_status", "name": "sync_status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "success", "default": "success",
"comment": "同步状态: success/pending/failed" "comment": "同步状态: success/pending/failed",
"length": 16
}, },
{ {
"name": "error_msg", "name": "error_msg",
"type": "TEXT", "type": "text",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "失败原因" "comment": "失败原因"
}, },
{ {
"name": "retry_count", "name": "retry_count",
"type": "INT", "type": "int",
"nullable": false, "nullable": false,
"default": 0, "default": 0,
"comment": "重试次数" "comment": "重试次数"
}, },
{ {
"name": "created_at", "name": "created_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "创建时间" "comment": "创建时间"
}, },
{ {
"name": "updated_at", "name": "updated_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "更新时间" "comment": "更新时间"

View File

@ -3,84 +3,94 @@
{ {
"name": "uapi_cache", "name": "uapi_cache",
"title": "uapi定义缓存", "title": "uapi定义缓存",
"primary": "id", "primary": [
"id"
],
"classification": "cache" "classification": "cache"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键" "comment": "主键",
"length": 32
}, },
{ {
"name": "upappid", "name": "upappid",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "上游应用ID" "comment": "上游应用ID",
"length": 32
}, },
{ {
"name": "apiname", "name": "apiname",
"type": "VARCHAR(128)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "API名称" "comment": "API名称",
"length": 128
}, },
{ {
"name": "method", "name": "method",
"type": "VARCHAR(16)", "type": "str",
"nullable": true, "nullable": true,
"default": "POST", "default": "POST",
"comment": "HTTP方法" "comment": "HTTP方法",
"length": 16
}, },
{ {
"name": "endpoint", "name": "endpoint",
"type": "VARCHAR(512)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "API端点" "comment": "API端点",
"length": 512
}, },
{ {
"name": "auth_type", "name": "auth_type",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "bearer", "default": "bearer",
"comment": "认证类型" "comment": "认证类型",
"length": 32
}, },
{ {
"name": "rate_limit", "name": "rate_limit",
"type": "INT", "type": "int",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "速率限制(次/分钟)" "comment": "速率限制(次/分钟)"
}, },
{ {
"name": "description", "name": "description",
"type": "TEXT", "type": "text",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "API描述" "comment": "API描述"
}, },
{ {
"name": "status", "name": "status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "active", "default": "active",
"comment": "状态" "comment": "状态",
"length": 16
}, },
{ {
"name": "sync_version", "name": "sync_version",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "同步版本号" "comment": "同步版本号",
"length": 32
}, },
{ {
"name": "cached_at", "name": "cached_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "缓存写入时间" "comment": "缓存写入时间"

View File

@ -3,84 +3,94 @@
{ {
"name": "users_cache", "name": "users_cache",
"title": "用户数据缓存", "title": "用户数据缓存",
"primary": "id", "primary": [
"id"
],
"classification": "cache" "classification": "cache"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "VARCHAR(32)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "主键,对应 users 表 id" "comment": "主键,对应 users 表 id",
"length": 32
}, },
{ {
"name": "username", "name": "username",
"type": "VARCHAR(128)", "type": "str",
"nullable": false, "nullable": false,
"default": "", "default": "",
"comment": "用户名" "comment": "用户名",
"length": 128
}, },
{ {
"name": "orgid", "name": "orgid",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "组织ID" "comment": "组织ID",
"length": 32
}, },
{ {
"name": "orgname", "name": "orgname",
"type": "VARCHAR(255)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "组织名称" "comment": "组织名称",
"length": 255
}, },
{ {
"name": "email", "name": "email",
"type": "VARCHAR(128)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "邮箱" "comment": "邮箱",
"length": 128
}, },
{ {
"name": "phone", "name": "phone",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "手机号" "comment": "手机号",
"length": 32
}, },
{ {
"name": "status", "name": "status",
"type": "VARCHAR(16)", "type": "str",
"nullable": false, "nullable": false,
"default": "active", "default": "active",
"comment": "状态: active/inactive/suspended" "comment": "状态: active/inactive/suspended",
"length": 16
}, },
{ {
"name": "created_at", "name": "created_at",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "创建时间" "comment": "创建时间"
}, },
{ {
"name": "updated_at", "name": "updated_at",
"type": "DATETIME", "type": "datetime",
"nullable": true, "nullable": true,
"default": null, "default": null,
"comment": "更新时间" "comment": "更新时间"
}, },
{ {
"name": "sync_version", "name": "sync_version",
"type": "VARCHAR(32)", "type": "str",
"nullable": true, "nullable": true,
"default": "", "default": "",
"comment": "同步版本号" "comment": "同步版本号",
"length": 32
}, },
{ {
"name": "cached_at", "name": "cached_at",
"type": "DATETIME", "type": "datetime",
"nullable": false, "nullable": false,
"default": "CURRENT_TIMESTAMP", "default": "CURRENT_TIMESTAMP",
"comment": "缓存写入时间" "comment": "缓存写入时间"

View File

@ -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:

View File

@ -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',

View File

@ -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:

View File

@ -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"

View File

@ -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
View 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)