11import { BailianError } from "../errors/base.ts" ;
22import { ExitCode } from "../errors/codes.ts" ;
3+ import { withRetry } from "../utils/retry.ts" ;
34import type { SkillIndexEntry , SkillsIndex } from "./types.ts" ;
45
56/**
@@ -9,8 +10,10 @@ import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
910 */
1011const DEFAULT_REGISTRY_BASE_URL = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/skills" ;
1112
12- const INDEX_TIMEOUT_MS = 10_000 ;
13+ const INDEX_TIMEOUT_MS = 30_000 ;
1314const ASSET_TIMEOUT_MS = 120_000 ;
15+ /** Interactive channels retry transient failures; silent background channels pass 1 to fail fast */
16+ const DEFAULT_ATTEMPTS = 3 ;
1417
1518export function getSkillRegistryBaseUrl ( ) : string {
1619 const override = process . env . BAILIAN_SKILL_REGISTRY_URL ?. trim ( ) ;
@@ -20,55 +23,64 @@ export function getSkillRegistryBaseUrl(): string {
2023/**
2124 * Fetch the remote skill index. No local caching — the diff comparison is always
2225 * "live remote index vs local skill-lock.json".
23- * Silent background channels (advisor sync) may pass a tighter timeout than the interactive default.
26+ * Silent background channels (advisor sync) may pass a tighter timeout and attempts=1
27+ * than the interactive defaults.
2428 */
25- export async function fetchSkillsIndex ( timeoutMs : number = INDEX_TIMEOUT_MS ) : Promise < SkillsIndex > {
26- const url = `${ getSkillRegistryBaseUrl ( ) } /index.json` ;
27- let res : Response ;
28- try {
29- res = await fetch ( url , { signal : AbortSignal . timeout ( timeoutMs ) } ) ;
30- } catch ( err ) {
31- throw new BailianError (
32- `Cannot access skill registry: ${ url } ` ,
33- ExitCode . NETWORK ,
34- "Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration" ,
35- { cause : err } ,
36- ) ;
37- }
38- if ( ! res . ok ) {
39- throw new BailianError (
40- `Skill registry returned HTTP ${ res . status } : ${ url } ` ,
41- ExitCode . NETWORK ,
42- res . status === 404
43- ? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
44- : "Remote error, retry later" ,
45- ) ;
46- }
47- let parsed : unknown ;
48- try {
49- parsed = await res . json ( ) ;
50- } catch ( err ) {
51- throw new BailianError (
52- "Skill index index.json is not valid JSON" ,
53- ExitCode . GENERAL ,
54- "Remote may be in the middle of publishing, retry later" ,
55- { cause : err } ,
56- ) ;
57- }
58- const index = parsed as SkillsIndex ;
59- if (
60- typeof index !== "object" ||
61- index === null ||
62- typeof index . skills !== "object" ||
63- index . skills === null
64- ) {
65- throw new BailianError (
66- "Skill index index.json has invalid structure" ,
67- ExitCode . GENERAL ,
68- "Retry later or contact the publisher" ,
69- ) ;
70- }
71- return index ;
29+ export async function fetchSkillsIndex (
30+ timeoutMs : number = INDEX_TIMEOUT_MS ,
31+ attempts : number = DEFAULT_ATTEMPTS ,
32+ ) : Promise < SkillsIndex > {
33+ return withRetry (
34+ async ( ) => {
35+ const url = `${ getSkillRegistryBaseUrl ( ) } /index.json` ;
36+ let res : Response ;
37+ try {
38+ res = await fetch ( url , { signal : AbortSignal . timeout ( timeoutMs ) } ) ;
39+ } catch ( err ) {
40+ throw new BailianError (
41+ `Cannot access skill registry: ${ url } ` ,
42+ ExitCode . NETWORK ,
43+ "Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration" ,
44+ { cause : err } ,
45+ ) ;
46+ }
47+ if ( ! res . ok ) {
48+ throw new BailianError (
49+ `Skill registry returned HTTP ${ res . status } : ${ url } ` ,
50+ ExitCode . NETWORK ,
51+ res . status === 404
52+ ? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
53+ : "Remote error, retry later" ,
54+ ) ;
55+ }
56+ let parsed : unknown ;
57+ try {
58+ parsed = await res . json ( ) ;
59+ } catch ( err ) {
60+ throw new BailianError (
61+ "Skill index index.json is not valid JSON" ,
62+ ExitCode . GENERAL ,
63+ "Remote may be in the middle of publishing, retry later" ,
64+ { cause : err } ,
65+ ) ;
66+ }
67+ const index = parsed as SkillsIndex ;
68+ if (
69+ typeof index !== "object" ||
70+ index === null ||
71+ typeof index . skills !== "object" ||
72+ index . skills === null
73+ ) {
74+ throw new BailianError (
75+ "Skill index index.json has invalid structure" ,
76+ ExitCode . GENERAL ,
77+ "Retry later or contact the publisher" ,
78+ ) ;
79+ }
80+ return index ;
81+ } ,
82+ { attempts } ,
83+ ) ;
7284}
7385
7486/**
@@ -84,29 +96,38 @@ export function resolveAssetFileName(entry?: SkillIndexEntry): string {
8496}
8597
8698/** Download the tar.br archive for a single skill (one skill = one GET) */
87- export async function downloadSkillAsset ( name : string , entry ?: SkillIndexEntry ) : Promise < Buffer > {
88- const url = `${ getSkillRegistryBaseUrl ( ) } /${ name } /${ resolveAssetFileName ( entry ) } ` ;
89- let res : Response ;
90- try {
91- res = await fetch ( url , { signal : AbortSignal . timeout ( ASSET_TIMEOUT_MS ) } ) ;
92- } catch ( err ) {
93- throw new BailianError (
94- `Failed to download skill ${ name } : ${ url } ` ,
95- ExitCode . NETWORK ,
96- "Network error, retryable" ,
97- {
98- cause : err ,
99- } ,
100- ) ;
101- }
102- if ( ! res . ok ) {
103- throw new BailianError (
104- `Failed to download skill ${ name } : HTTP ${ res . status } ` ,
105- ExitCode . NETWORK ,
106- res . status === 404
107- ? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
108- : "Remote error, retry later" ,
109- ) ;
110- }
111- return Buffer . from ( await res . arrayBuffer ( ) ) ;
99+ export async function downloadSkillAsset (
100+ name : string ,
101+ entry ?: SkillIndexEntry ,
102+ attempts : number = DEFAULT_ATTEMPTS ,
103+ ) : Promise < Buffer > {
104+ return withRetry (
105+ async ( ) => {
106+ const url = `${ getSkillRegistryBaseUrl ( ) } /${ name } /${ resolveAssetFileName ( entry ) } ` ;
107+ let res : Response ;
108+ try {
109+ res = await fetch ( url , { signal : AbortSignal . timeout ( ASSET_TIMEOUT_MS ) } ) ;
110+ } catch ( err ) {
111+ throw new BailianError (
112+ `Failed to download skill ${ name } : ${ url } ` ,
113+ ExitCode . NETWORK ,
114+ "Network error, retryable" ,
115+ {
116+ cause : err ,
117+ } ,
118+ ) ;
119+ }
120+ if ( ! res . ok ) {
121+ throw new BailianError (
122+ `Failed to download skill ${ name } : HTTP ${ res . status } ` ,
123+ ExitCode . NETWORK ,
124+ res . status === 404
125+ ? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
126+ : "Remote error, retry later" ,
127+ ) ;
128+ }
129+ return Buffer . from ( await res . arrayBuffer ( ) ) ;
130+ } ,
131+ { attempts } ,
132+ ) ;
112133}
0 commit comments