@@ -25,6 +25,28 @@ export class WikiError extends Error {
2525 }
2626}
2727
28+ function looksLikeLockContention ( body : string ) : boolean {
29+ return (
30+ body . includes ( '131009' ) ||
31+ body . toLowerCase ( ) . includes ( 'lock contention' )
32+ ) ;
33+ }
34+
35+ function sleepSync ( ms : number ) : void {
36+ // Synchronous sleep for retry backoff. Avoids requiring caller to be async.
37+ const until = Date . now ( ) + ms ;
38+ // eslint-disable-next-line no-empty
39+ const spawnSync = require ( 'node:child_process' ) . spawnSync ;
40+ // Use shell sleep for accurate ms-level wait without busy-loop
41+ spawnSync ( 'sleep' , [ ( ms / 1000 ) . toFixed ( 3 ) ] , { stdio : 'ignore' , timeout : ms + 5000 } ) ;
42+ // Guard in case sleep is unavailable (Windows): fall back to small busy-wait spike
43+ if ( Date . now ( ) < until ) {
44+ while ( Date . now ( ) < until ) {
45+ /* busy wait short tail */
46+ }
47+ }
48+ }
49+
2850function runLark (
2951 bin : string ,
3052 args : string [ ] ,
@@ -143,38 +165,58 @@ export function createWikiChild(
143165 larkBin = 'lark-cli' ,
144166 env ?: NodeJS . ProcessEnv ,
145167) : WikiChild {
146- const r = runLark (
147- larkBin ,
148- [
149- 'wiki' ,
150- '+node-create' ,
151- '--space-id' ,
152- spaceId ,
153- '--parent-node-token' ,
154- parentNodeToken ,
155- '--title' ,
156- title ,
157- '--obj-type' ,
158- 'docx' ,
159- '--node-type' ,
160- 'origin' ,
161- ] ,
162- env ,
163- 60_000 ,
164- ) ;
165- if ( ! r . ok ) {
166- throw new WikiError ( `wiki +node-create "${ title } " failed: ${ r . stderr || r . stdout } ` ) ;
168+ // Lark wiki has server-side lock contention when creating many children under
169+ // the same parent in quick succession (error 131009). Auto-retry a few times
170+ // with exponential backoff.
171+ const args = [
172+ 'wiki' ,
173+ '+node-create' ,
174+ '--space-id' ,
175+ spaceId ,
176+ '--parent-node-token' ,
177+ parentNodeToken ,
178+ '--title' ,
179+ title ,
180+ '--obj-type' ,
181+ 'docx' ,
182+ '--node-type' ,
183+ 'origin' ,
184+ ] ;
185+ const MAX_RETRIES = 4 ;
186+ let r ;
187+ let lastBody = '' ;
188+ for ( let attempt = 0 ; attempt <= MAX_RETRIES ; attempt ++ ) {
189+ r = runLark ( larkBin , args , env , 60_000 ) ;
190+ lastBody = r . stdout || r . stderr ;
191+ if ( r . ok && ! looksLikeLockContention ( lastBody ) ) break ;
192+ // Inspect JSON body to detect server-side errors that succeeded at CLI level
193+ if ( r . ok ) {
194+ try {
195+ const j = JSON . parse ( r . stdout ) ;
196+ if ( j && j . ok !== false ) break ; // genuine success
197+ } catch {
198+ break ;
199+ }
200+ }
201+ if ( ! looksLikeLockContention ( lastBody ) || attempt === MAX_RETRIES ) break ;
202+ const delayMs = 250 * Math . pow ( 2 , attempt ) ; // 250 / 500 / 1000 / 2000 / 4000
203+ sleepSync ( delayMs ) ;
204+ }
205+ if ( ! r ! . ok || looksLikeLockContention ( lastBody ) ) {
206+ throw new WikiError (
207+ `wiki +node-create "${ title } " failed after retries: ${ lastBody . slice ( 0 , 300 ) } ` ,
208+ ) ;
167209 }
168210 let parsed : any ;
169211 try {
170- parsed = JSON . parse ( r . stdout ) ;
212+ parsed = JSON . parse ( r ! . stdout ) ;
171213 } catch ( err ) {
172214 throw new WikiError ( `wiki +node-create returned non-JSON: ${ ( err as Error ) . message } ` ) ;
173215 }
174216 // Real shape (lark-cli 1.0.32): { data: { node_token, obj_token, ... } } — flat under data
175217 const node = parsed ?. data ?. node ?? parsed ?. data ?? parsed ?. node ?? parsed ;
176218 if ( ! node ?. node_token || ! node ?. obj_token ) {
177- throw new WikiError ( `wiki +node-create response missing tokens: ${ r . stdout . slice ( 0 , 200 ) } ` ) ;
219+ throw new WikiError ( `wiki +node-create response missing tokens: ${ r ! . stdout . slice ( 0 , 200 ) } ` ) ;
178220 }
179221 return {
180222 nodeToken : node . node_token ,
0 commit comments