1- import { defineCommand , unwrapResponse } from "bailian-cli-core" ;
1+ import { BailianError , ExitCode , defineCommand , unwrapResponse } from "bailian-cli-core" ;
22import {
33 FREE_TIER_ONLY_STATUS_API ,
44 extractFreeTierOnlyStatuses ,
@@ -14,13 +14,41 @@ interface BatchResultFailure {
1414 errorCode : string ;
1515}
1616
17+ /** Require a positive success signal; empty successModels must not count as success. */
18+ function isModelOperationConfirmed (
19+ resultData : Record < string , unknown > ,
20+ modelName : string ,
21+ ) : boolean {
22+ const failureModels = ( resultData . failureModels as BatchResultFailure [ ] | undefined ) ?? [ ] ;
23+ if ( failureModels . length > 0 ) return false ;
24+ const successModels = resultData . successModels ;
25+ if ( Array . isArray ( successModels ) ) {
26+ return successModels . includes ( modelName ) ;
27+ }
28+ if ( typeof resultData . successCount === "number" ) {
29+ return resultData . successCount > 0 ;
30+ }
31+ // Legacy responses without a success list still use the boolean flag.
32+ return resultData . success === true ;
33+ }
34+
1735export default defineCommand ( {
1836 description : {
1937 "en-US" :
2038 "Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable" ,
2139 "zh-CN" : "启用或停用免费额度模型自动停止。默认启用,使用 --off 停用" ,
2240 } ,
2341 auth : "console" ,
42+ risk : {
43+ level : "high" ,
44+ reason : "billing" ,
45+ message : {
46+ "en-US" :
47+ "This changes free-tier auto-stop. Enabling it makes calls fail once free quota is exhausted; disabling it allows pay-as-you-go charges after free quota is exhausted." ,
48+ "zh-CN" :
49+ "此操作会变更免费额度自动停止。启用后,免费额度用尽时调用将失败;停用后,免费额度用尽将继续按量计费调用。" ,
50+ } ,
51+ } ,
2452 usageArgs : "<--model <model>[,model2,...] | --all> [--off] [flags]" ,
2553 flags : {
2654 model : {
@@ -48,12 +76,19 @@ export default defineCommand({
4876 } ,
4977 } ,
5078 exampleArgs : [
51- "--model qwen3-max" ,
52- "--model qwen3-max,qwen-turbo" ,
53- "--all" ,
54- "--on --model qwen3-max" ,
55- "--off --model qwen3-max" ,
56- "--off --all" ,
79+ "--model qwen3-max --yes" ,
80+ "--model qwen3-max,qwen-turbo --yes" ,
81+ "--all --yes" ,
82+ "--on --model qwen3-max --yes" ,
83+ "--off --model qwen3-max --yes" ,
84+ "--off --all --yes" ,
85+ ] ,
86+ notes : [
87+ {
88+ "en-US" :
89+ "Requires `--yes` (exit code 7 / requires_confirmation without it). Applies to both enabling and disabling auto-stop." ,
90+ "zh-CN" : "需要 `--yes`(缺少时退出码 7 / requires_confirmation)。启用与停用自动停止均适用。" ,
91+ } ,
5792 ] ,
5893 validate : ( flags ) =>
5994 ! flags . model && ! flags . all ? "Provide --model <model>[,model2,...] or --all." : undefined ,
@@ -62,6 +97,8 @@ export default defineCommand({
6297 const modelFlag = flags . model || undefined ;
6398 const off = Boolean ( flags . off ) ;
6499 const format = settings . output ;
100+ const action = off ? "disable" : "enable" ;
101+ const actionZh = off ? "停用" : "启用" ;
65102
66103 let models : string [ ] ;
67104 if ( modelFlag ) {
@@ -96,6 +133,9 @@ export default defineCommand({
96133
97134 const skipped : string [ ] = [ ] ;
98135 const processed : { model : string ; result : unknown } [ ] = [ ] ;
136+ const confirmed : string [ ] = [ ] ;
137+ const failed : string [ ] = [ ] ;
138+ const timedOut : string [ ] = [ ] ;
99139 const textLines : string [ ] = [ ] ;
100140 let stopMap = new Map < string , boolean > ( ) ;
101141 if ( off ) {
@@ -106,6 +146,38 @@ export default defineCommand({
106146 stopMap = new Map ( stopStatuses . map ( ( status ) => [ status . model , status . freeTierOnly ] ) ) ;
107147 }
108148
149+ let resultWritten = false ;
150+ /** Empty JSON is a valid success; only skip empty output when catching with no progress. */
151+ const writeBusinessResult = ( options ?: { requireProgress ?: boolean } ) => {
152+ if ( resultWritten ) return ;
153+ if ( format === "text" && textLines . length === 0 ) return ;
154+ if (
155+ options ?. requireProgress &&
156+ processed . length === 0 &&
157+ skipped . length === 0 &&
158+ textLines . length === 0
159+ ) {
160+ return ;
161+ }
162+ ctx . output . result (
163+ off ? { processed, skipped } : processed . map ( ( item ) => item . result ) ,
164+ format === "text" ? { text : textLines . join ( "\n" ) } : undefined ,
165+ ) ;
166+ resultWritten = true ;
167+ } ;
168+
169+ const progressMessage = ( failedModel : string ) =>
170+ ctx . localize ( {
171+ "en-US" : [
172+ `Confirmed ${ action } auto-stop for ${ confirmed . length } model(s)${ confirmed . length > 0 ? `: ${ confirmed . join ( ", " ) } ` : "" } .` ,
173+ `Model "${ failedModel } " failed; its outcome is unconfirmed. Later models were not executed.` ,
174+ ] . join ( "\n" ) ,
175+ "zh-CN" : [
176+ `已确认${ actionZh } 自动停止 ${ confirmed . length } 个模型${ confirmed . length > 0 ? `:${ confirmed . join ( "、" ) } ` : "" } 。` ,
177+ `模型 "${ failedModel } " 失败,结果未确认。后续模型未执行。` ,
178+ ] . join ( "\n" ) ,
179+ } ) ;
180+
109181 for ( const name of models ) {
110182 if ( off && stopMap . get ( name ) === false ) {
111183 skipped . push ( name ) ;
@@ -115,11 +187,35 @@ export default defineCommand({
115187 } ) ;
116188 continue ;
117189 }
118- const result = await pollFreeTierBatch ( ctx . client , api , requestKey , [ name ] ) ;
190+
191+ let result : unknown ;
192+ try {
193+ result = await pollFreeTierBatch ( ctx . client , api , requestKey , [ name ] ) ;
194+ } catch ( error ) {
195+ // Keep stdout progress, but do not wrap the original error (preserves cause.code /
196+ // ERROR_STACK). Surface confirmed work via warning instead.
197+ writeBusinessResult ( { requireProgress : true } ) ;
198+ if ( confirmed . length > 0 || processed . length > 0 || skipped . length > 0 ) {
199+ ctx . output . warning ( {
200+ code : "FREETIER_PARTIAL_PROGRESS" ,
201+ message : progressMessage ( name ) ,
202+ details : {
203+ confirmed,
204+ failed_model : name ,
205+ processed_count : processed . length ,
206+ skipped_count : skipped . length ,
207+ } ,
208+ } ) ;
209+ }
210+ throw error ;
211+ }
212+
119213 processed . push ( { model : name , result } ) ;
120214 const resultData = result ? unwrapResponse ( result as Record < string , unknown > ) : undefined ;
121215 const failureModels = ( resultData ?. failureModels as BatchResultFailure [ ] | undefined ) ?? [ ] ;
216+
122217 if ( ! result ) {
218+ timedOut . push ( name ) ;
123219 ctx . output . warning ( {
124220 code : "FREETIER_OPERATION_TIMEOUT" ,
125221 message : {
@@ -128,7 +224,11 @@ export default defineCommand({
128224 } ,
129225 details : { model : name } ,
130226 } ) ;
131- } else if ( failureModels . length > 0 ) {
227+ break ;
228+ }
229+
230+ if ( failureModels . length > 0 ) {
231+ failed . push ( name ) ;
132232 const errorCode = failureModels [ 0 ] . errorCode ;
133233 ctx . output . warning ( {
134234 code : "FREETIER_OPERATION_FAILED" ,
@@ -143,29 +243,64 @@ export default defineCommand({
143243 } ,
144244 details : { model : name , error_code : errorCode } ,
145245 } ) ;
246+ break ;
146247 }
147- // Keep the existing disable response policy; the JSON report retains the raw response.
148- if ( off || ( result && failureModels . length === 0 ) ) {
149- textLines . push (
150- ctx . localize (
151- off
152- ? {
153- "en-US" : `Disabled auto-stop for "${ name } ".` ,
154- "zh-CN" : `已为模型 "${ name } " 停用自动停止。` ,
155- }
156- : {
157- "en-US" : `Enabled auto-stop for "${ name } ".` ,
158- "zh-CN" : `已为模型 "${ name } " 启用自动停止。` ,
159- } ,
160- ) ,
161- ) ;
248+
249+ if ( ! resultData || ! isModelOperationConfirmed ( resultData , name ) ) {
250+ failed . push ( name ) ;
251+ ctx . output . warning ( {
252+ code : "FREETIER_OPERATION_FAILED" ,
253+ message : {
254+ "en-US" : `Auto-stop ${ action } for "${ name } " was not confirmed by the server.` ,
255+ "zh-CN" : `服务端未确认模型 "${ name } " 的自动停止${ actionZh } 。` ,
256+ } ,
257+ details : { model : name } ,
258+ } ) ;
259+ break ;
162260 }
261+
262+ confirmed . push ( name ) ;
263+ textLines . push (
264+ ctx . localize (
265+ off
266+ ? {
267+ "en-US" : `Disabled auto-stop for "${ name } ".` ,
268+ "zh-CN" : `已为模型 "${ name } " 停用自动停止。` ,
269+ }
270+ : {
271+ "en-US" : `Enabled auto-stop for "${ name } ".` ,
272+ "zh-CN" : `已为模型 "${ name } " 启用自动停止。` ,
273+ } ,
274+ ) ,
275+ ) ;
163276 }
164- if ( format === "json" || textLines . length > 0 ) {
165- ctx . output . result (
166- off ? { processed, skipped } : processed . map ( ( item ) => item . result ) ,
167- format === "text" ? { text : textLines . join ( "\n" ) } : undefined ,
277+
278+ writeBusinessResult ( ) ;
279+ if ( failed . length === 0 && timedOut . length === 0 ) return ;
280+
281+ // Client poll timeout is internal TIMEOUT; server rejection stays GENERAL.
282+ const summaryPartsEn : string [ ] = [ ] ;
283+ const summaryPartsZh : string [ ] = [ ] ;
284+ if ( failed . length > 0 ) {
285+ summaryPartsEn . push (
286+ `Failed to ${ action } auto-stop for ${ failed . length } model(s): ${ failed . join ( ", " ) } .` ,
287+ ) ;
288+ summaryPartsZh . push ( `${ failed . length } 个模型${ actionZh } 自动停止失败:${ failed . join ( "、" ) } 。` ) ;
289+ }
290+ if ( timedOut . length > 0 ) {
291+ summaryPartsEn . push (
292+ `Timed out waiting to confirm ${ action } auto-stop for ${ timedOut . length } model(s): ${ timedOut . join ( ", " ) } .` ,
293+ ) ;
294+ summaryPartsZh . push (
295+ `${ timedOut . length } 个模型${ actionZh } 自动停止超时未确认:${ timedOut . join ( "、" ) } 。` ,
168296 ) ;
169297 }
298+ throw new BailianError (
299+ ctx . localize ( {
300+ "en-US" : summaryPartsEn . join ( " " ) ,
301+ "zh-CN" : summaryPartsZh . join ( "" ) ,
302+ } ) ,
303+ failed . length > 0 ? ExitCode . GENERAL : ExitCode . TIMEOUT ,
304+ ) ;
170305 } ,
171306} ) ;
0 commit comments