bugfix
This commit is contained in:
commit
59e0870f89
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# CRUD definition directories (auto-generated by Sage platform)
|
||||
wwwroot/uapi/
|
||||
wwwroot/uapiio/
|
||||
wwwroot/upapp/
|
||||
wwwroot/upappkey/
|
||||
__pycache__/
|
||||
31
README.md
31
README.md
@ -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
74
i18n/en/msg.txt
Normal 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
74
i18n/jp/msg.txt
Normal 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
74
i18n/ko/msg.txt
Normal 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
74
i18n/zh/msg.txt
Normal 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
|
||||
需要鉴权: 需要鉴权
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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":{}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"tblname":"uapiset",
|
||||
"params":{
|
||||
"title":"API集",
|
||||
"sortby":"name",
|
||||
"browserfields":{
|
||||
"exclouded":["id" ],
|
||||
"alters":{}
|
||||
},
|
||||
"editexclouded":["id"],
|
||||
"subtables":[
|
||||
{
|
||||
"field":"apisetid",
|
||||
"subtable":"uapi",
|
||||
"title":"定义api"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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
153
models/uapi.json
Normal 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
36
models/uapiio.json
Normal 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
39
models/uapiset.json
Normal 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
83
models/upapp.json
Normal 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
90
models/upappkey.json
Normal 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
74
models/uptask.json
Normal 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
56
scripts/load_path.py
Normal 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())
|
||||
133
scripts/migrate_uapi_upappid.py
Normal file
133
scripts/migrate_uapi_upappid.py
Normal 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())
|
||||
@ -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}')
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user