This commit is contained in:
yumoqing 2026-06-26 13:18:37 +08:00
commit 59e0870f89
23 changed files with 1059 additions and 61 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# CRUD definition directories (auto-generated by Sage platform)
wwwroot/uapi/
wwwroot/uapiio/
wwwroot/upapp/
wwwroot/upappkey/
__pycache__/

View File

@ -397,6 +397,37 @@ uapi
---
## 在 Sage 系统中的角色
uapi 是 Sage 平台的**配置化 API 网关层**llmage大模型管理模块是其主要消费者。两者的协同关系
```
llmage (模型管理) uapi (API 网关)
│ │
│ llm 表 │
│ upappid ──────────────→│ upapp 表 (baseurl, myappid, ownerid)
│ apiname ──────────────→│ uapi 表 (httpmethod, path, headers, ...)
│ │ uapiset 表 (auth_apiname)
│ │ upappkey 表 (apikey, secretkey)
│ │
│ UpAppApi(request) │
│ .stream_linify() ─────→│ StreamHttpClient → 外部 LLM API
│ .call() ──────────────→│ 同步/流式 HTTP 调用
│ │
```
**新增一个 LLM 的完整流程**
1. 在 uapi 模块的 `uapiset` 中创建 API 集合(配置认证方式)
2. 在 `upapp` 中注册上位系统baseurl、appkey 等)
3. 在 `uapi` 中定义具体的 API 端点path、method、headers 模板、response 模板)
4. 在 `upappkey` 中分配 API 密钥给调用方
5. 在 llmage 模块的 `llm` 表中注册模型,关联 `upappid` + `apiname`
6. 用户在 llmage 前端页面点击模型卡片 → 推理 → 通过 uapi 网关调用外部 API
**优势**:新增模型无需修改 Python 代码,只需在数据库/CRUD 页面中配置 API 定义。
---
## 开发注意事项
1. **dbname 获取**:必须通过 `get_serverenv('get_module_dbname')('uapi')` 动态获取,禁止硬编码

74
i18n/en/msg.txt Normal file
View File

@ -0,0 +1,74 @@
API: API
APIKEY: APIKEY
API接口: API Interface
API标题: API Title
API输入输出: API Input/Output
API集: API Collection
API集名称: API Collection Name
Add Error: Add Error
Add Success: Add Success
Authorization Error: Authorization Error
Cancel: Cancel
Conform: Confirm
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Please login: Please login
Record no exist or with wrong ownership: Record no exist or with wrong ownership
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
api名称: API Name
api密码: API Password
api密钥: API Secret
api测试: API Test
api用户: API User
api集id: API Collection ID
failed: failed
headers模版: Headers Template
http方法: HTTP Method
id: id
ok: ok
path: path
上位应用名: Parent Application Name
上位系统: Parent System
上位系统ID: Parent System ID
上位系统密码: Parent System Password
上位系统密码表: Parent System Password Table
上游任务: Upstream Task
加密密钥: Encryption Key
动态headers函数: Dynamic Headers Function
参数模版: Parameter Template
响应模版: Response Template
响应转换函数名: Response Transform Function Name
回调url: Callback URL
外部系统: External System
属主id: Owner ID
属主机构id: Owner Organization ID
应用id: Application ID
开始时间: Start Time
我的appid: My App ID
所属机构: Organization
执行方任务id: Executor Task ID
授权api名: Authorized API Name
接口描述: Interface Description
描述: Description
数据模版: Data Template
是否第一用户: Is First User
本地业务id: Local Business ID
流式匹配串: Stream Match String
流式输出: Stream Output
状态: Status
用户id: User ID
相应数据: Response Data
类型名: Type Name
类型说明: Type Description
系统url: System URL
结束时间: End Time
认证API名: Auth API Name
跳转到: Jump to
输入字段: Input Field
输入输出: Input/Output
输入输出id: Input/Output ID
需要鉴权: Requires Auth

74
i18n/jp/msg.txt Normal file
View File

@ -0,0 +1,74 @@
API: API
APIKEY: APIKEY
API接口: APIインターフェース
API标题: APIタイトル
API输入输出: API入出力
API集: APIコレクション
API集名称: APIコレクション名
Add Error: 追加エラー
Add Success: 追加成功
Authorization Error: 認証エラー
Cancel: キャンセル
Conform: 確認
Delete Error: 削除エラー
Delete Success: 削除成功
Discard: 破棄
Please login: ログインしてください
Record no exist or with wrong ownership: レコードが存在しないか、所有権が不正です
Reset: リセット
Submit: 送信
Update Error: 更新エラー
Update Success: 更新成功
api名称: API名称
api密码: APIパスワード
api密钥: APIシークレット
api测试: APIテスト
api用户: APIユーザー
api集id: APIコレクションID
failed: 失敗
headers模版: ヘッダーテンプレート
http方法: HTTPメソッド
id: id
ok: ok
path: path
上位应用名: 上位アプリケーション名
上位系统: 上位システム
上位系统ID: 上位システムID
上位系统密码: 上位システムパスワード
上位系统密码表: 上位システムパスワード表
上游任务: 上流タスク
加密密钥: 暗号化キー
动态headers函数: 動的ヘッダー関数
参数模版: パラメーターテンプレート
响应模版: レスポンステンプレート
响应转换函数名: レスポンス変換関数名
回调url: コールバックURL
外部系统: 外部システム
属主id: 所有者ID
属主机构id: 所有者組織ID
应用id: アプリケーションID
开始时间: 開始時間
我的appid: マイAppID
所属机构: 所属組織
执行方任务id: 実行タスクID
授权api名: 認可API名
接口描述: インターフェース説明
描述: 説明
数据模版: データテンプレート
是否第一用户: 最初のユーザーか
本地业务id: ローカル業務ID
流式匹配串: ストリームマッチ文字列
流式输出: ストリーム出力
状态: ステータス
用户id: ユーザーID
相应数据: 対応データ
类型名: 型名
类型说明: 型説明
系统url: システムURL
结束时间: 終了時間
认证API名: 認証API名
跳转到: ジャンプ先
输入字段: 入力フィールド
输入输出: 入出力
输入输出id: 入出力ID
需要鉴权: 認証が必要

74
i18n/ko/msg.txt Normal file
View File

@ -0,0 +1,74 @@
API: API
APIKEY: APIKEY
API接口: API 인터페이스
API标题: API 제목
API输入输出: API 입출력
API集: API 컬렉션
API集名称: API 컬렉션 이름
Add Error: 추가 오류
Add Success: 추가 성공
Authorization Error: 인증 오류
Cancel: 취소
Conform: 확인
Delete Error: 삭제 오류
Delete Success: 삭제 성공
Discard: 폐기
Please login: 로그인해주세요
Record no exist or with wrong ownership: 레코드가 존재하지 않거나 소유권이 잘못되었습니다
Reset: 초기화
Submit: 제출
Update Error: 업데이트 오류
Update Success: 업데이트 성공
api名称: API 이름
api密码: API 비밀번호
api密钥: API 비밀키
api测试: API 테스트
api用户: API 사용자
api集id: API 컬렉션 ID
failed: 실패
headers模版: 헤더 템플릿
http方法: HTTP 메서드
id: id
ok: ok
path: path
上位应用名: 상위 애플리케이션 이름
上位系统: 상위 시스템
上位系统ID: 상위 시스템 ID
上位系统密码: 상위 시스템 비밀번호
上位系统密码表: 상위 시스템 비밀번호 테이블
上游任务: 업스트림 작업
加密密钥: 암호화 키
动态headers函数: 동적 헤더 함수
参数模版: 파라미터 템플릿
响应模版: 응답 템플릿
响应转换函数名: 응답 변환 함수 이름
回调url: 콜백 URL
外部系统: 외부 시스템
属主id: 소유자 ID
属主机构id: 소유자 조직 ID
应用id: 애플리케이션 ID
开始时间: 시작 시간
我的appid: 내 App ID
所属机构: 소속 조직
执行方任务id: 실행자 작업 ID
授权api名: 인증된 API 이름
接口描述: 인터페이스 설명
描述: 설명
数据模版: 데이터 템플릿
是否第一用户: 첫 번째 사용자 여부
本地业务id: 로컬 비즈니스 ID
流式匹配串: 스트림 매칭 문자열
流式输出: 스트림 출력
状态: 상태
用户id: 사용자 ID
相应数据: 응답 데이터
类型名: 타입 이름
类型说明: 타입 설명
系统url: 시스템 URL
结束时间: 종료 시간
认证API名: 인증 API 이름
跳转到: 이동
输入字段: 입력 필드
输入输出: 입출력
输入输出id: 입출력 ID
需要鉴权: 인증 필요

74
i18n/zh/msg.txt Normal file
View File

@ -0,0 +1,74 @@
API: API
APIKEY: APIKEY
API接口: API接口
API标题: API标题
API输入输出: API输入输出
API集: API集
API集名称: API集名称
Add Error: Add Error
Add Success: Add Success
Authorization Error: Authorization Error
Cancel: Cancel
Conform: Conform
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Please login: Please login
Record no exist or with wrong ownership: Record no exist or with wrong ownership
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
api名称: api名称
api密码: api密码
api密钥: api密钥
api测试: api测试
api用户: api用户
api集id: api集id
failed: failed
headers模版: headers模版
http方法: http方法
id: id
ok: ok
path: path
上位应用名: 上位应用名
上位系统: 上位系统
上位系统ID: 上位系统ID
上位系统密码: 上位系统密码
上位系统密码表: 上位系统密码表
上游任务: 上游任务
加密密钥: 加密密钥
动态headers函数: 动态headers函数
参数模版: 参数模版
响应模版: 响应模版
响应转换函数名: 响应转换函数名
回调url: 回调url
外部系统: 外部系统
属主id: 属主id
属主机构id: 属主机构id
应用id: 应用id
开始时间: 开始时间
我的appid: 我的appid
所属机构: 所属机构
执行方任务id: 执行方任务id
授权api名: 授权api名
接口描述: 接口描述
描述: 描述
数据模版: 数据模版
是否第一用户: 是否第一用户
本地业务id: 本地业务id
流式匹配串: 流式匹配串
流式输出: 流式输出
状态: 状态
用户id: 用户id
相应数据: 相应数据
类型名: 类型名
类型说明: 类型说明
系统url: 系统url
结束时间: 结束时间
认证API名: 认证API名
跳转到: 跳转到
输入字段: 输入字段
输入输出: 输入输出
输入输出id: 输入输出id
需要鉴权: 需要鉴权

View File

@ -27,11 +27,11 @@
"title":"API",
"description":"API定义",
"sortby":["apisetid", "name"],
"sortby":"name",
"browserfields":{
"exclouded":["id", "apisetid"],
"exclouded":["id"],
"alters":{}
},
"editexclouded":["id", "apisetid"]
"editexclouded":["id","upappid"]
}
}

View File

@ -4,6 +4,12 @@
"title":"API输入输出",
"description":"API的输入输出定义",
"sortby": "name",
"data_filter":{
"fields":[
{"field":"name","title":"类型名","uitype":"str"},
{"field":"description","title":"类型说明","uitype":"str"}
]
},
"browserfields":{
"exclouded":["id"],
"alters":{}

View File

@ -1,19 +0,0 @@
{
"tblname":"uapiset",
"params":{
"title":"API集",
"sortby":"name",
"browserfields":{
"exclouded":["id" ],
"alters":{}
},
"editexclouded":["id"],
"subtables":[
{
"field":"apisetid",
"subtable":"uapi",
"title":"定义api"
}
]
}
}

View File

@ -16,27 +16,12 @@
"field":"upappid",
"subtable": "upappkey",
"title": "APIKEY"
},
{
"field":"upappid",
"subtable": "uapi",
"title": "API"
}
],
"toolbar":{
"tools":[
{
"name":"jumpin",
"label":"跳转到",
"selected_data": true
}
]
},
"binds":[
{
"wid":"self",
"event":"jumpin",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('/uapi/jump_in.dspy')}}"
}
}
]
]
}
}

153
models/uapi.json Normal file
View File

@ -0,0 +1,153 @@
{
"summary": [
{
"name": "uapi",
"title": "API接口",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "upappid",
"title": "上位系统ID",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "api名称",
"type": "str",
"length": 200
},
{
"name": "title",
"title": "API标题",
"type": "str",
"length": 100
},
{
"name": "description",
"title": "接口描述",
"type": "text"
},
{
"name": "need_auth",
"title": "需要鉴权",
"type": "str",
"length": 1,
"default": "0"
},
{
"name": "stream",
"title": "流式输出",
"type": "str",
"length": 20
},
{
"name": "path",
"title": "path",
"type": "str",
"length": 4000
},
{
"name": "httpmethod",
"title": "http方法",
"type": "str",
"length": 20,
"nullable": "yes",
"default": "GET"
},
{
"name": "chunk_match",
"title": "流式匹配串",
"type": "str",
"length": 100
},
{
"name": "headers",
"title": "headers模版",
"type": "text",
"nullable": "yes"
},
{
"name": "params",
"title": "参数模版",
"type": "text",
"nullable": "yes"
},
{
"name": "data",
"title": "数据模版",
"type": "text",
"nullable": "yes"
},
{
"name": "response",
"title": "响应模版",
"type": "text",
"nullable": "yes"
},
{
"name": "ioid",
"title": "输入输出id",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "callbackurl",
"title": "回调url",
"type": "str",
"length": 1000,
"nullable": "yes"
}
],
"indexes": [
{
"name": "idx1",
"idxtype": "unique",
"idxfields": [
"upappid",
"name"
]
}
],
"codes": [
{
"field": "httpmethod",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='httpmethod'"
},
{
"field": "need_auth",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='yesno'"
},
{
"field": "stream",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='resp_mode'"
},
{
"field": "ioid",
"table": "uapiio",
"valuefield": "id",
"textfield": "name"
}
]
}

36
models/uapiio.json Normal file
View File

@ -0,0 +1,36 @@
{
"summary": [
{
"name": "uapiio",
"title": "输入输出",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "类型名",
"type": "str",
"length": 100
},
{
"name": "description",
"title": "类型说明",
"type": "text"
},
{
"name": "input_fields",
"title": "输入字段",
"type": "text"
}
]
}

39
models/uapiset.json Normal file
View File

@ -0,0 +1,39 @@
{
"summary": [
{
"name": "uapiset",
"title": "API集",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "API集名称",
"type": "str",
"length": 200
},
{
"name": "description",
"title": "描述",
"type": "text",
"default": "0"
},
{
"name": "auth_apiname",
"title": "授权api名",
"type": "str",
"length": 200,
"nullable": "yes"
}
]
}

83
models/upapp.json Normal file
View File

@ -0,0 +1,83 @@
{
"summary": [
{
"name": "upapp",
"title": "外部系统",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "上位应用名",
"type": "str",
"length": 200
},
{
"name": "description",
"title": "描述",
"type": "text",
"default": "0"
},
{
"name": "ownerid",
"title": "所属机构",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "apisetid",
"title": "api集id",
"type": "str",
"length": 32
},
{
"name": "secretkey",
"title": "加密密钥",
"type": "str",
"length": 100
},
{
"name": "baseurl",
"title": "系统url",
"type": "str",
"length": 500
},
{
"name": "myappid",
"title": "我的appid",
"type": "str",
"length": 100
},
{
"name": "dynamic_func",
"title": "动态headers函数",
"type": "str",
"length": 255
},
{
"name": "auth_apiname",
"title": "认证API名",
"type": "str",
"length": 200,
"nullable": "yes"
}
],
"codes": [
{
"field": "ownerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

90
models/upappkey.json Normal file
View File

@ -0,0 +1,90 @@
{
"summary": [
{
"name": "upappkey",
"title": "上位系统密码表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "upappid",
"title": "应用id",
"type": "str",
"length": 32
},
{
"name": "ownerid",
"title": "属主id",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "apikey",
"title": "api密钥",
"type": "str",
"length": 4000,
"default": "0"
},
{
"name": "apiuser",
"title": "api用户",
"type": "str",
"length": 100
},
{
"name": "apipasswd",
"title": "api密码",
"type": "str",
"length": 100
},
{
"name": "orgid",
"title": "属主机构id",
"type": "str",
"length": 32
},
{
"name": "is_first",
"title": "是否第一用户",
"type": "str",
"length": 1
}
],
"codes": [
{
"field": "ownerid",
"table": "users",
"valuefield": "id",
"textfield": "username"
},
{
"field": "upappid",
"table": "upapp",
"valuefield": "id",
"textfield": "name"
},
{
"field": "orgid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "is_first",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='yesno'"
}
]
}

74
models/uptask.json Normal file
View File

@ -0,0 +1,74 @@
{
"summary": [
{
"name": "uptask",
"title": "上游任务",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "userid",
"title": "用户id",
"type": "str",
"length": 32
},
{
"name": "executor_taskid",
"title": "执行方任务id",
"type": "str",
"length": 64
},
{
"name": "convert_func_name",
"title": "响应转换函数名",
"type": "str",
"length": 128
},
{
"name": "local_bizid",
"title": "本地业务id",
"type": "str",
"length": 32
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 24
},
{
"name": "response_data",
"title": "相应数据",
"type": "text"
},
{
"name": "start_timestamp",
"title": "开始时间",
"type": "time"
},
{
"name": "end_timestamp",
"title": "结束时间",
"type": "time"
}
],
"indexes": [
{
"name": "idx1",
"idxtype": "unique",
"idxfields": [
"executor_taskid"
]
}
]
}

56
scripts/load_path.py Normal file
View File

@ -0,0 +1,56 @@
"""Generate RBAC permissions for uapi module paths.
Run from Sage root with Sage venv:
cd ~/repos/sage && ./py3/bin/python ../uapi/scripts/load_path.py
"""
import os
import sys
import asyncio
sage_root = os.environ.get('SAGE_ROOT')
if sage_root and sage_root not in sys.path:
sys.path.insert(0, sage_root)
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.dictObject import DictObject
from appPublic.uniqueID import getID
paths = [
("/uapi", "logined"),
("/uapi/jump_in.dspy", "logined"),
("/uapi/minimax_callback.dspy", "any"),
("/uapi/uptask_callback.dspy", "any"),
("/uapi/viducallback", "any"),
]
async def main():
config = getConfig('.')
DBPools(config.databases)
dbname = 'sage'
async with DBPools().sqlorContext(dbname) as sor:
cnt = 0
for path, role in paths:
r = await sor.sqlExe(
'select * from permission where permcode = ${permcode}$',
{'permcode': path}
)
if len(r) == 0:
await sor.sqlExe(
'''insert into permission (id, permcode, permname, permtype)
values (${id}$, ${permcode}$, ${permname}$, ${permtype}$)''',
{
'id': getID(),
'permcode': path,
'permname': path,
'permtype': role,
}
)
cnt += 1
print(f'{cnt} path(s) inserted for uapi')
if __name__ == '__main__':
asyncio.run(main())

View File

@ -0,0 +1,133 @@
"""
Migration: uapi table apisetid -> upappid
Removes uapiset intermediate layer. Each upapp now owns its own uapi records.
For shared apisetid: copies uapi records so each upapp has its own copy.
Usage (in Sage virtual env):
./py3/bin/python3 ~/repos/uapi/scripts/migrate_uapi_upappid.py --output /tmp/migrate_uapi.sql
Review the output SQL, then execute on your database.
"""
from appPublic.jsonConfig import getConfig
import asyncio, json, sys, argparse
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
config = getConfig('.')
db = DBPools(config.databases)
dbname = list(config.databases.keys())[0]
async def generate_migration_sql():
"""Generate SQL to migrate uapi.apisetid -> uapi.upappid."""
lines = [
"-- Migration: uapi table apisetid -> upappid",
"-- Removes uapiset intermediate layer.",
"",
"-- Step 1: Add upappid column to uapi",
"ALTER TABLE uapi ADD COLUMN upappid VARCHAR(32) DEFAULT NULL COMMENT '上位系统ID' AFTER id;",
""
]
# Load data
try:
async with db.sqlorContext(dbname) as sor:
# Get all upapps
upapps = await sor.sqlExe('select id, name, apisetid from upapp', {})
# Get all uapis (removed non-existent auth_apiname field)
uapis = await sor.sqlExe('select id, apisetid, name, httpmethod, path, headers, ioid, response, params, data, chunk_match from uapi', {})
except Exception as e:
print(f"Error querying database: {e}", file=sys.stderr)
sys.exit(1)
# Build mapping: apisetid -> [upapp1, upapp2, ...]
apiset_to_upapps = {}
for u in upapps:
aid = u.get('apisetid')
if aid:
apiset_to_upapps.setdefault(aid, []).append(u)
# Build mapping: apisetid -> [uapi_records]
apiset_to_uapis = {}
for a in uapis:
aid = a.get('apisetid')
if aid:
apiset_to_uapis.setdefault(aid, []).append(a)
inserts = []
for apisetid, upapp_list in apiset_to_upapps.items():
uapi_records = apiset_to_uapis.get(apisetid, [])
if len(upapp_list) == 1:
# Single upapp owns this apisetid -> just update
upapp = upapp_list[0]
for uapi in uapi_records:
inserts.append(
f"UPDATE uapi SET upappid = '{upapp['id']}' WHERE id = '{uapi['id']}';"
)
else:
# Multiple upapps share this apisetid -> pick first as owner, copy for rest
owner = upapp_list[0]
# Update existing records to point to owner
for uapi in uapi_records:
inserts.append(
f"UPDATE uapi SET upappid = '{owner['id']}' WHERE id = '{uapi['id']}';"
)
# Copy for other upapps
for other_upapp in upapp_list[1:]:
for uapi in uapi_records:
new_id = getID()
old_id = uapi['id']
fields = ['id', 'upappid', 'name', 'httpmethod', 'path', 'headers', 'ioid', 'response', 'params', 'data', 'chunk_match']
vals = [f"'{new_id}'", f"'{other_upapp['id']}'"]
for f in fields[2:]:
v = uapi.get(f)
if v is None:
vals.append('NULL')
elif isinstance(v, str):
escaped = v.replace("'", "\\'")
vals.append(f"'{escaped}'")
else:
vals.append(str(v))
col_str = ', '.join(fields)
val_str = ', '.join(vals)
inserts.append(f"INSERT INTO uapi ({col_str}) VALUES ({val_str});")
if inserts:
lines.append("-- Step 2: Migrate data (updates + copies for shared apisetid)")
lines.extend(inserts)
lines.append("")
lines.extend([
"-- Step 3: Drop apisetid column (verify upappid has no NULLs first)",
"-- UPDATE uapi SET upappid = (SELECT ownerid FROM organization LIMIT 1) WHERE upappid IS NULL;",
"-- ALTER TABLE uapi DROP COLUMN apisetid;",
"",
"-- Step 4: Add index",
"CREATE INDEX idx_uapi_upappid ON uapi (upappid);",
""
])
return '\n'.join(lines)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('--output', '-o', default='-')
args = parser.parse_args()
sql = await generate_migration_sql()
if args.output == '-':
print(sql)
else:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(sql)
print(f"Generated migration SQL -> {args.output}")
if __name__ == '__main__':
asyncio.run(main())

View File

@ -8,10 +8,23 @@ from appPublic.streamhttpclient import StreamHttpClient, liner
from appPublic.dictObject import DictObject
from appPublic.log import debug, exception, error
from appPublic.aes import aes_encode_b64
from appPublic.jsonConfig import getConfig
from ahserver.globalEnv import password_decode
from ahserver.serverenv import get_serverenv, ServerEnv
from random import randint
def _cache_enabled():
"""Check if cache is enabled for uapi module in config.json"""
try:
config = getConfig()
module_cache = config.module_cache
if module_cache is None:
return True
return getattr(module_cache, 'uapi', True)
except Exception:
return True
async def get_deerer(upappid, callerid):
db = DBPools()
dbname = get_dbname()
@ -25,10 +38,9 @@ async def get_deerer(upappid, callerid):
async def sor_get_uapi(sor, upappid, apiname):
sql = """select a.*,
c.auth_apiname
from uapi a, upapp b, uapiset c
where a.apisetid = b.apisetid
and b.apisetid = c.id
b.auth_apiname
from uapi a, upapp b
where a.upappid = b.id
and a.name = ${apiname}$
and b.id = ${upappid}$"""
recs = await sor.sqlExe(sql, {'upappid': upappid, 'apiname': apiname})
@ -52,7 +64,15 @@ class UAPIData:
self.apidata = {}
self.apikeys = {}
self.org_users = {}
def on_hot_reload(self, data=None):
"""Event handler for hot_reload event. Clears all caches."""
from appPublic.log import debug
debug(f'[uapi] on_hot_reload called, clearing caches (data={data})')
self.apidata.clear()
self.apikeys.clear()
self.org_users.clear()
async def get_userapikey(self, appid, callerid):
users = await self.get_apiusers(appid)
for u in users:
@ -69,10 +89,11 @@ class UAPIData:
return None
async def get_apiusers(self, appid, orgid=None):
key = appid
d = self.org_users.get(key)
if d:
return d
key = f'{appid}.{orgid}' if orgid else str(appid)
if _cache_enabled():
d = self.org_users.get(key)
if d:
return d
env = ServerEnv()
async with get_sor_context(env, 'uapi') as sor:
sql = """select
@ -105,7 +126,8 @@ where b.orgid = c.ownerid
e = Exception(f'{appid=} {orgid=} get none user')
exception(f'{e}')
raise e
self.apidata[key] = d
if _cache_enabled():
self.org_users[key] = d
return d
e = Exception(f'{appid=} {orgid=} get none user')
exception(f'{e}')
@ -123,6 +145,10 @@ where b.orgid = c.ownerid
async def get_api_from_db(self, appid, apiname):
key = f'{appid}.{apiname}'
if _cache_enabled():
api = self.apidata.get(key)
if api:
return api
env = ServerEnv()
async with get_sor_context(env, 'uapi') as sor:
d = await sor_get_uapi(sor, appid, apiname)
@ -130,7 +156,8 @@ where b.orgid = c.ownerid
e = Exception(f'{appid=}, {apiname=} get none api')
exception(f'{e}')
raise e
self.apidata[key] = d
if _cache_enabled():
self.apidata[key] = d
return d
e = Exception(f'{appid=}, {apiname=} get none api')
exception(f'{e}')

View File

@ -20,7 +20,7 @@ async def get_callerid(orgid):
async def sor_get_uapi_by_appname_apiname(sor, appname, apiname):
sql = """select a.* from uapi a, upapp b
where a.apisetid = b.apisetid
where a.upappid = b.id
and b.name = ${appname}$
and a.name = ${apiname}$"""
recs = await sor.sqlExe(sql, {'apiname': apiname, 'appname': appname})
@ -90,10 +90,9 @@ async def get_userapikey(sor, upappid, callerid):
async def sor_get_uapi(sor, upappid, apiname):
sql = """select a.*,
c.auth_apiname
from uapi a, upapp b, uapiset c
where a.apisetid = b.apisetid
and b.apisetid = c.id
b.auth_apiname
from uapi a, upapp b
where a.upappid = b.id
and a.name = ${apiname}$
and b.id = ${upappid}$"""
recs = await sor.sqlExe(sql, {'upappid': upappid, 'apiname': apiname})

View File

@ -32,6 +32,9 @@ def load_uapi():
g.get_my_uptasks = get_my_uptasks
g.uptask_feedback = uptask_feedback
g.uptask_started = uptask_started
# Bind hot_reload event — instance method, WeakMethod safe (stored on g)
if hasattr(g, 'event_dispatcher'):
g.event_dispatcher.bind('hot_reload', g.uapi_data.on_hot_reload)
# USAGE in dspy

View File

@ -1,4 +1,4 @@
debug(f'{params_kw=}')
debug_params('params_kw', params_kw)
if parmas_kw.task_id is None:
e = Exception(f'need a task_id')
raise e

View File

@ -1,4 +1,4 @@
debug(f'{params_kw=}
debug_params('params_kw', params_kw)
taskid = params_kw.id
async with get_sor_context(request._run_ns, 'uapi') as sor:
llmusage = await get_llmlage_by_taskid(taskid)