diff --git a/json/llm_account_by_vendor.json b/json/llm_account_by_vendor.json index e59ccb1..a2fd78b 100644 --- a/json/llm_account_by_vendor.json +++ b/json/llm_account_by_vendor.json @@ -43,6 +43,32 @@ }, "confidential_fields": [ "api_key" + ], + "toolbar": { + "tools": [ + { + "name": "recharge", + "label": "💰 充值登记", + "selected_row": true + } + ] + }, + "binds": [ + { + "wid": "self", + "event": "recharge", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "title": "充值登记", + "width": "46%", + "height": "52%", + "resizable": true + }, + "options": { + "url": "{{entire_url('../api/llm_account_recharge_popup.dspy')}}" + } + } ] } } diff --git a/wwwroot/api/llm_account_recharge_popup.dspy b/wwwroot/api/llm_account_recharge_popup.dspy new file mode 100644 index 0000000..286e48c --- /dev/null +++ b/wwwroot/api/llm_account_recharge_popup.dspy @@ -0,0 +1,99 @@ +# llm_account_recharge_popup.dspy — 供应商账号列表「充值登记」弹窗内容 +# 入口: json/llm_account_by_vendor.json 工具栏 recharge 按钮(selected_row) → binds +# urlwidget → PopupWindow(外壳由 popup_options 生成,此处只返回内容 widget, +# 返回 PopupWindow 会套娃——同 llm_call_trace_io.dspy 约定)。 +# 入参: 列表页 toolbar selected_row 自动注入整行数据(id=账号主键)。 +# 提交: POST llm_account_recharge.dspy → gateway.recharge_account: +# balance += 充值金额、total_recharge 累加、写充值流水;余额回升后 +# 「钱包充值」待办由状态派生自动消失。 + +uid = await get_user() +if not uid: + return {"widgettype": "Message", "options": {"title": "提示", "message": "请先登录", "timeout": 3}} + +acc_id = str((params_kw or {}).get('id') or '').strip() +if not acc_id: + return {"widgettype": "Message", "options": {"title": "打开失败", "message": "请先在列表中选中一个账号行"}} + +user_org = str(await get_userorgid() or '0') +dbname = get_module_dbname('pipeline-llm') +async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT a.id, a.name, a.balance, a.total_recharge, a.status, " + "COALESCE(NULLIF(a.org_id,''), v.org_id, '0') AS owner_org, v.name AS vendor_name " + "FROM llm_account a LEFT JOIN llm_vendor v ON v.id=a.vendor_id " + "WHERE a.id=${i}$ LIMIT 1", {"i": acc_id}) + await sor.sqlExe("COMMIT", {}) + +if not recs: + return {"widgettype": "Message", "options": {"title": "打开失败", "message": "账号不存在或已删除"}} +row = recs[0] +owner_org = str(row['owner_org'] or '0') +# 机构隔离:平台业主('0')可看全部;否则只看本属主机构的账号(同 llm_wallet_todo_popup) +if user_org != '0' and owner_org not in ('', user_org): + return {"widgettype": "Message", "options": {"title": "无权访问", "message": "该账号属于其他机构"}} + +balance = float(row['balance'] or 0) +acc_name = str(row['name'] or acc_id) +vendor_name = str(row['vendor_name'] or '') + +head_md = ['## 充值登记', '', + '**账号**:' + (('【%s】' % vendor_name) if vendor_name else '') + acc_name, + '**当前余额**:%.4f 元 | **累计充值**:%.4f 元 | **状态**:%s' + % (balance, float(row['total_recharge'] or 0), str(row['status'] or '')), + '', + '> 确认后账号余额 = 当前余额 + 充值金额,并写入充值流水;' + '余额回升到门禁阈值以上后该账号自动恢复模型调用轮询。'] + +recharge_url = entire_url('/pipeline-llm/api/llm_account_recharge.dspy') + + +def _read_input_js(wid): + # 弹窗内控件 getWidgetById 起点必须 bricks.app(DOM 挂 body 下,app.root 查不到) + return ("var cw=bricks.getWidgetById(" + json.dumps(wid) + ",bricks.app);var cv='';" + "if(cw){cv=(typeof cw.resultValue==='function')?cw.resultValue():" + "((cw.dom_element&&cw.dom_element.value)||'');}" + "cv=(cv===null||cv===undefined)?'':String(cv).trim();") + + +# 成功:关弹窗(从内容控件沿 parent 链找 PopupWindow 外壳) + 刷新账号列表 + 提示; +# 失败:弹窗保持打开,允许改金额重试。 +_close_and_refresh_js = ( + "var box=bricks.getWidgetById('llm_acc_recharge_box',bricks.app);" + "var pw=box;while(pw&&!(pw instanceof bricks.PopupWindow)&&!(pw instanceof bricks.Popup)){pw=pw.parent;}" + "if(pw){if(pw.destroy){pw.destroy();}else if(pw.dismiss){pw.dismiss();}}" + "var t=bricks.getWidgetById('llm_account_tbl',bricks.app);" + "if(t&&t.render){t.old_params=null;t.render({});}") + +do_js = (_read_input_js('llm_acc_amt') + + "var amt=parseFloat(cv);" + + "if(isNaN(amt)||amt<=0){new bricks.Message({title:'请填写金额',message:'充值金额必须是大于 0 的数字'}).open();return;}" + + _read_input_js('llm_acc_note') + + "var r=await fetch(" + json.dumps(recharge_url) + ",{method:'POST'," + "headers:{'Content-Type':'application/json'}," + "body:JSON.stringify({account_id:" + json.dumps(acc_id) + ",amount:amt,note:cv})});" + "var d=null;try{d=await r.json();}catch(e){}" + + "if(d&&d.success){" + _close_and_refresh_js + + "new bricks.Message({title:'充值成功',message:d.message||'账号余额已更新'}).open();}" + + "else{new bricks.Message({title:'充值失败',message:(d&&(d.message||d.error))||('请求失败 HTTP '+r.status)}).open();}") + +return { + "widgettype": "VBox", + "id": "llm_acc_recharge_box", + "options": {"width": "100%", "height": "100%", "padding": "16px", "gap": "12px"}, + "subwidgets": [ + {"widgettype": "MdWidget", "options": {"mdtext": '\n'.join(head_md), "width": "100%"}}, + {"widgettype": "HBox", + "options": {"width": "100%", "gap": "8px", "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "充值金额(元):", "cfontsize": 0.9, "color": "#334155"}}, + {"widgettype": "UiText", "id": "llm_acc_amt", + "options": {"name": "llm_acc_amt", "placeholder": "如 100.00", "width": "160px"}}, + {"widgettype": "UiText", "id": "llm_acc_note", + "options": {"name": "llm_acc_note", "placeholder": "备注(可选,如银行流水号)", + "flex": "1 1 auto"}}, + {"widgettype": "Button", + "options": {"label": "💰 确认充值", "css": "primary small"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": do_js}]}]}] +}