From 127d08dfa44f083975071169086b893d6715a32b Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 24 May 2026 21:57:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(llmage):=20=E5=A4=87=E4=BB=BD=E6=94=B9?= =?UTF-8?q?=E7=94=A8INSERT=20SELECT+DELETE=E5=8D=95SQL=E8=AF=AD=E5=8F=A5?= =?UTF-8?q?=20+=20=E6=96=B0=E5=A2=9E=E5=A4=B1=E8=B4=A5=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dat/qwen3.7-max.txt | 53 ++++++++++++++++++ llmage/__pycache__/accounting.cpython-310.pyc | Bin 0 -> 9693 bytes llmage/accounting.py | 37 ++++++------ models/.llmusage_history.json.swp | Bin 0 -> 12288 bytes wwwroot/api/retry_accounting.dspy | 36 ++++++++++++ wwwroot/failed_accounting.ui | 24 ++++++++ 6 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 dat/qwen3.7-max.txt create mode 100644 llmage/__pycache__/accounting.cpython-310.pyc create mode 100644 models/.llmusage_history.json.swp create mode 100644 wwwroot/api/retry_accounting.dspy diff --git a/dat/qwen3.7-max.txt b/dat/qwen3.7-max.txt new file mode 100644 index 0000000..f07f135 --- /dev/null +++ b/dat/qwen3.7-max.txt @@ -0,0 +1,53 @@ + 已生成完整的 qwen3.7-max 配置SQL。以下是配置方案: + + 模型摘要 + - 模型名称: 千问3.7-Max + - API model: qwen3.7-max (严格按API文档填写) + - 分类: text2text (文生文) + - 供应商: 阿里百炼 (ali-qwen) + - 接口: OpenAI兼容 /chat/completions,同步流式 + + 复用vs新建 + - upapp: 复用 ali-qwen (阿里百炼) + - upappkey: 复用 gCMl-BnrTrfoqWAEPPoH8 (API Key已配置) + - uapi: 复用 t2t (BY2cA4RD_axNxTG4m9Coa) + - uapiio: 复用 Is8l4TGkcZcqFSjbbeIK2 (文本会话) + - llm: 新建 + - llm_api_map: 新建 + - pricing_program: 新建 + - pricing_program_timing: 新建 + + 生成的SQL (4条) + + sql + -- 1. llm表 + INSERT INTO llm VALUES ('Jt26VKlUFsJxABuVLauBc','千问3.7-Max','qwen3.7-max','通义千问3.7系列旗舰模型,相较3.6版本在推理能力、代码能力、多模态理解等方面全面升级。适用于复杂任务处理、智能体开发、代码生成等高级应用场景。','text2text','qwen','ali-qwen','t2t','6fadgewjraOyvxC_EkHou','0','2026-05-23','9999-12-31','',NULL,10.00); + + -- 2. llm_api_map表 + INSERT INTO llm_api_map VALUES ('rFS0Ag-epBUxE5LCF6glW','Jt26VKlUFsJxABuVLauBc','text2text','t2t',NULL,NULL,'0B-JNSUEelsY5TQ_Sawa7','1'); + + -- 3. pricing_program表 + INSERT INTO pricing_program VALUES ('0B-JNSUEelsY5TQ_Sawa7','qwen3.7-max定价','fields:\r\n formula:\r\n label: 计算公式\r\n type: str\r\n model:\r\n label: 模型\r\n type: str\r\n prompt_tokens:\r\n label: 输入词元数\r\n type: int\r\n value_mode: between\r\nformula: null\r\npricings:\r\n- formula: 2.5 prompt_tokens / 1000000 + completion_tokens 10 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: -0.1 ~= 32000\r\n- formula: 4 prompt_tokens / 1000000 + completion_tokens 16 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: 32000 ~= 128000\r\n- formula: 7 prompt_tokens / 1000000 + completion_tokens 28 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: 128000 ~= 256000\r\n- formula: 10 prompt_tokens / 1000000 + completion_tokens 40 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: 256000 ~= 1000000','2026-05-23','9999-12-31'); + + -- 4. pricing_program_timing表 + INSERT INTO pricing_program_timing VALUES ('C-IIEG_9_O9beN8S2vdUI','0B-JNSUEelsY5TQ_Sawa7','qwen3.7-max计费','fields:\r\n formula:\r\n label: 计算公式\r\n type: str\r\n model:\r\n label: 模型\r\n type: str\r\n prompt_tokens:\r\n label: 输入词元数\r\n type: int\r\n value_mode: between\r\nformula: null\r\npricings:\r\n- formula: 2.5 prompt_tokens / 1000000 + completion_tokens 10 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: -0.1 ~= 32000\r\n- formula: 4 prompt_tokens / 1000000 + completion_tokens 16 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: 32000 ~= 128000\r\n- formula: 7 prompt_tokens / 1000000 + completion_tokens 28 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: 128000 ~= 256000\r\n- formula: 10 prompt_tokens / 1000000 + completion_tokens 40 /1000000\r\n model: qwen3.7-max\r\n prompt_tokens: 256000 ~= 1000000','2026-05-23','9999-12-31'); + + + 验证查询 + sql + SELECT id, name, model, upappid, apiname FROM llm WHERE model = 'qwen3.7-max'; + SELECT id, llmid, llmcatelogid, apiname, ppid FROM llm_api_map WHERE llmid = 'Jt26VKlUFsJxABuVLauBc'; + SELECT id, name FROM pricing_program WHERE id = '0B-JNSUEelsY5TQ_Sawa7'; + + + ppid三处一致 + - pricing_program.id = 0B-JNSUEelsY5TQ_Sawa7 + - pricing_program_timing.ppid = 0B-JNSUEelsY5TQ_Sawa7 + - llm_api_map.ppid = 0B-JNSUEelsY5TQ_Sawa7 + + 注意事项 + 1. 定价参考qwen3.6-plus的阶梯式计费模式,具体价格需根据百炼官方定价调整 + 2. qwen3.7-max使用已有的t2t uapi,OpenAI兼容接口 + 3. 如果qwen3.7-max有特殊的推理模式(如思考/非思考切换),可能需要额外的配置 + 4. 之前提到的 startReasoning is not defined 前端报错问题需要单独排查,可能与harnessed_reasoning的bricks前端代码有关 + diff --git a/llmage/__pycache__/accounting.cpython-310.pyc b/llmage/__pycache__/accounting.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ccd2fe1a60a0c266542b8f595917965d193b445 GIT binary patch literal 9693 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 literal 0 HcmV?d00001 diff --git a/llmage/accounting.py b/llmage/accounting.py index b841ce0..53b11c9 100644 --- a/llmage/accounting.py +++ b/llmage/accounting.py @@ -240,27 +240,28 @@ async def llm_accoung_failed(luid, reason=None): async def backup_accounted_llmusage(cutoff_date): - """Backup accounted records with use_date < cutoff_date to history table.""" + """Backup accounted records with use_date < cutoff_date to history table using single SQL statements.""" env = ServerEnv() ts = env.timestampstr() - batched = 0 - recs = [] async with get_sor_context(env, 'llmage') as sor: - sql = """select * from llmusage -where accounting_status='accounted' - and use_date < ${cutoff_date}$""" - recs = await sor.sqlExe(sql, {'cutoff_date': cutoff_date}) - if not recs: - debug(f'backup_accounted_llmusage: no records to backup for use_date < {cutoff_date}') - return 0 - debug(f'backup_accounted_llmusage: {cutoff_date} {len(recs)} records to backup') - for r in recs: - async with get_sor_context(env, 'llmage') as sor: - await sor.C('llmusage_history', r.copy()) - await sor.D('llmusage', {'id': r.id}) - batched += 1 - debug(f'backup_accounted_llmusage: backed up {batched} records for use_date < {cutoff_date}') - return batched + # Step 1: INSERT INTO history SELECT from main table + insert_sql = """INSERT INTO llmusage_history +(id, llmid, use_date, use_time, userid, usages, ioinfo, transno, responsed_seconds, finish_seconds, status, taskid, amount, cost, userorgid, ownerid, accounting_status, backup_time) +SELECT id, llmid, use_date, use_time, userid, usages, ioinfo, transno, responsed_seconds, finish_seconds, status, taskid, amount, cost, userorgid, ownerid, accounting_status, ${ts}$ +FROM llmusage +WHERE accounting_status='accounted' AND use_date < ${cutoff_date}$""" + result = await sor.execute(insert_sql, {'cutoff_date': cutoff_date, 'ts': ts}) + inserted = result if isinstance(result, int) else 0 + debug(f'backup_accounted_llmusage: {inserted} records inserted to history') + + if inserted > 0: + # Step 2: DELETE from main table + delete_sql = """DELETE FROM llmusage +WHERE accounting_status='accounted' AND use_date < ${cutoff_date}$""" + await sor.execute(delete_sql, {'cutoff_date': cutoff_date}) + debug(f'backup_accounted_llmusage: {inserted} records deleted from main table') + + return inserted async def get_failed_accounting_records(filters=None, page=1, page_size=50): diff --git a/models/.llmusage_history.json.swp b/models/.llmusage_history.json.swp new file mode 100644 index 0000000000000000000000000000000000000000..cd1501d8e72c9549a9627ad34befa2bae27095c5 GIT binary patch literal 12288 zcmeI2PiP!f9LHa*rrMg?w8sjrqh2I`cbhb+P$_~CRK$b5gm#_m%tu>LVqzx%)Nui`Al!jy*d+^ZS)GB(>Lvd%)sPR|?LHxd%ot@n&v)yEkP1sOE@m8R%AxKbm67v1&UEPDVFOv3&=SW;slcL5;w5n8*WKHMg zh*;mPw1;>;n5?%(d!Wk)&VY@om{PRHL?X$@#Kxo|hzaW|t>HQxmo!~b`x?46MSfrm zc7rLv6xe_QX+2usRzpI;<|f+v1@8I4?p^P0U?A3oDZmt93NQtj0!#s>08@Y|z!Z2? z70^>vWB|rLmRrDB?p)08@Y|z!YE#Fa?+bOo2yG0bU^F zXV_i81qb~9-}(Fh?`H^E1HXU_7zI7x4R8?b2QPyFs0P37Amkgc04{^`;5ZOK2WSE> zfj!`_8bW>m8E_R`1jB#_2SF>S13SRl(}dgsi(n4SfMLLc*TE~G8SDmsY$xP9a2tFH zra(XF0v+H0*bBA;a~s-$Iq*3+35LKC&<^&1D)9SLgxm!`f<^EJ7ywbw3ElxAPz&xq zNyty&TW||p0z)7Q-URzV9pFGUxVx2*ufQcR0X_r+;0V|Uo&{ULpVe3!_!`WDF`xqp zgh4B)1^1tTFTovf8(aW`;5~2%ybTV3y`ToHJq|m;4KNQzK{t2}w1Eb&4g8AOxCL&2 zc`yRv;8oB98Uc-;I`F?P{Um^L*gDc*_BH)Q3nvM^QAtb)8n-{t$#wCOo^&d#OG%M) zyEqN?zLbb&oFofkFIPwCeW!3Tk3O_g=nW_0;lh#JmPd)xzTdq`Z+Yy%S2mJRp#}CB z?lk4joz_HEQDYU)?~bz}8^QvwSLy_qU=QQ9z;>Z%Io)!}VYh`RKpdyjn7}7f$rNXG zpiZNO(y3(pD97|T!YULb)^xc8=UAobs&}ruLHFja z-n({jW%Rl^c+A_#iB>)mQPQ$5$+0jNr>C`ohW-Ilky+;Kv~lhub8^W%H-sQ_g{N!F zZzJV>SyYRvH_4opiCJ@G;bG>iFPH@}0-v>zoVTnKmlv(M&9i35nSzq;LM(Y}odC-& z&6uB!nO)seP*K9LQTs?7m_59faGpGQ0&+08wzLvU z#oVE+_|IlC#^|(hYN22*+COiJ&TBpYNr;u)g6r6Kw$nl+r-F5=Jg!zUBOlYqC<7 za?bNW7khQTw+$xDE`4GQA2;R}3kIbjk&kalk>u#VIOXz9JS?*}r_Ezm{HCut-o~W(Tu~&9?RJw&dgS(L#X{uakC0%(hKI~ zl)vacX$~FEn`l-fyZ!Di5|+-9kGS%4&Wg5WP8pyIc%JoM=EA4f9mHys8;k9eL=h260~&E JnVB3V{{YX(lMMg> literal 0 HcmV?d00001 diff --git a/wwwroot/api/retry_accounting.dspy b/wwwroot/api/retry_accounting.dspy new file mode 100644 index 0000000..3ec9eb5 --- /dev/null +++ b/wwwroot/api/retry_accounting.dspy @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import json +from datetime import datetime + +result = {'success': False, 'message': ''} + +try: + dbname = get_module_dbname('llmage') + luid = params_kw.get('id') or params_kw.get('llmusageid') + if not luid: + result['message'] = '缺少llmusageid参数' + else: + async with DBPools().sqlorContext(dbname) as sor: + # 1. 重置 llmusage 记账状态为 created,让后台循环重新处理 + await sor.U('llmusage', { + 'id': luid, + 'accounting_status': 'created' + }) + + # 2. 更新失败记录:标记已处理,增加重试次数 + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + await sor.execute(""" + UPDATE llmusage_accounting_failed + SET handled = '1', + retry_count = IFNULL(retry_count, 0) + 1, + handled_time = ${now}$, + handled_note = CONCAT(IFNULL(handled_note, ''), '[', ${now}$, '] 触发重试; ') + WHERE llmusageid = ${luid}$ + """, {'luid': luid, 'now': now}) + + result['success'] = True + result['message'] = '已重置为待记账状态,后台循环将重新处理' +except Exception as e: + result['message'] = str(e) + +return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/failed_accounting.ui b/wwwroot/failed_accounting.ui index 2faa3ef..1ca120e 100644 --- a/wwwroot/failed_accounting.ui +++ b/wwwroot/failed_accounting.ui @@ -80,6 +80,30 @@ }] } ] + }, + { + "widgettype": "VBox", + "options": {"spacing": 4}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "", "fontSize": "12px"}}, + { + "widgettype": "Button", + "id": "retry_btn", + "options": { + "label": "重试", + "bgcolor": "#4caf50", + "color": "#ffffff", + "width": "80px" + }, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "script", + "target": "self", + "script": "var dv = this.root.getElementById('failed_table'); var row = dv.selected_row || (dv.selected_rows && dv.selected_rows[0]); if(!row || !row.llmusageid) { alert('请先选中一条记录'); return; } var url = bricks.build_url ? bricks.build_url('/llmage/api/retry_accounting.dspy') : '/llmage/api/retry_accounting.dspy'; fetch(url + '?id=' + row.llmusageid).then(function(r){return r.json();}).then(function(d){ if(d.success) { alert(d.message); dv.load({}); } else { alert('失败: ' + d.message); } }).catch(function(e){ alert('请求异常: ' + e); });" + }] + } + ] } ] },