Skip to content

Commit a402026

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/runtime-risk-confirmation
2 parents da6e131 + a78ed7f commit a402026

19 files changed

Lines changed: 257 additions & 103 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
66

77
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
88

9+
## [1.18.2] - 2026-09-01
10+
11+
### Changed
12+
13+
- **Confirmation before deleting or clearing resources**`bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
14+
15+
### Fixed
16+
17+
- **Skill installation reliability**`bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
18+
919
## [1.18.1] - 2026-08-28
1020

1121
### Removed

CHANGELOG.zh.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66

77
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
88

9+
## [1.18.2] - 2026-09-01
10+
11+
### 变更
12+
13+
- **删除与清除操作增加确认** —— `bl finetune delete``bl deploy delete``bl dataset delete``bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`
14+
15+
### 修复
16+
17+
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
18+
919
## [1.18.1] - 2026-08-28
1020

1121
### 已移除

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli",
3-
"version": "1.18.1",
3+
"version": "1.18.2",
44
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
55
"keywords": [
66
"agent",

packages/cli/postinstall.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,13 @@ function atomicSwap(tmpDir, catalogDir) {
181181
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
182182
throw err;
183183
}
184-
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
184+
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
185+
// succeeded, so a backup deletion failure must not fail the pre-download
186+
try {
187+
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
188+
} catch {
189+
/* keep the backup on disk rather than report a completed swap as failed */
190+
}
185191
}
186192

187193
async function main() {
@@ -214,7 +220,11 @@ async function main() {
214220
}
215221
atomicSwap(tmpDir, catalogDir);
216222
} catch (err) {
217-
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
223+
try {
224+
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
225+
} catch {
226+
/* cleanup must not mask the original error */
227+
}
218228
throw err;
219229
}
220230

packages/commands/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli-commands",
3-
"version": "1.18.1",
3+
"version": "1.18.2",
44
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
55
"homepage": "https://bailian.console.aliyun.com/cli",
66
"bugs": {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli-core",
3-
"version": "1.18.1",
3+
"version": "1.18.2",
44
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
55
"homepage": "https://bailian.console.aliyun.com/cli",
66
"bugs": {

packages/core/src/advisor/sync.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const MODELS_FILE = "models.jsonl";
3737
const THROTTLE_MS = 12 * 60 * 60 * 1000; // 12h
3838
/** Tighter than the interactive default: the silent channel must not stall `bl advisor recommend` */
3939
const INDEX_TIMEOUT_MS = 3000;
40+
/** No retries on the silent channel: a failed sync simply tries again on the next recommend */
41+
const FETCH_ATTEMPTS = 1;
4042

4143
interface SyncState {
4244
lastChecked: number;
@@ -108,7 +110,7 @@ function wikiLockNeedsBackfill(contentHash: string): boolean {
108110
/** Fetch skills/index.json via the shared registry client and extract the wiki skill entry; returns null on any failure */
109111
async function fetchIndexEntry(): Promise<SkillIndexEntry | null> {
110112
try {
111-
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS);
113+
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS, FETCH_ATTEMPTS);
112114
return index.skills[WIKI_SKILL_NAME] ?? null;
113115
} catch {
114116
return null;
@@ -160,6 +162,7 @@ export async function maybeSyncWikiData(): Promise<boolean> {
160162
entry,
161163
detectInstalledAgents(),
162164
previousLinks,
165+
FETCH_ATTEMPTS,
163166
);
164167
recordWikiInLock(record.lockEntry);
165168
} catch {

packages/core/src/skills/extract.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,12 @@ export function atomicSwap(tmpDir: string, destDir: string): void {
100100
if (existsSync(backup) && !existsSync(destDir)) renameSync(backup, destDir);
101101
throw err;
102102
}
103-
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
103+
// Best-effort cleanup: the swap already succeeded, so a backup deletion failure
104+
// (permissions, host safe-delete guards on large dirs) must not fail the install;
105+
// leftover .old-* dirs are inert (skill status scans ignore them)
106+
try {
107+
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
108+
} catch {
109+
/* keep the backup on disk rather than report a completed install as failed */
110+
}
104111
}

packages/core/src/skills/installer.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,30 @@ export async function installSkillFromBuffer(
6161
atomicSwap(tmpDir, dest);
6262
return { name, path: dest, meta };
6363
} finally {
64-
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
64+
// Best-effort cleanup: on failure paths this must not mask the original error,
65+
// and on success the dir is already renamed away (existsSync → false)
66+
try {
67+
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
68+
} catch {
69+
/* leave the temp dir rather than hide the real error */
70+
}
6571
}
6672
}
6773

6874
/** Install a single skill by index entry (download + validate + write to disk) */
69-
export async function installSkill(name: string, entry: SkillIndexEntry): Promise<InstalledSkill> {
75+
export async function installSkill(
76+
name: string,
77+
entry: SkillIndexEntry,
78+
downloadAttempts?: number,
79+
): Promise<InstalledSkill> {
7080
if (entry.compression && entry.compression !== "tar.br") {
7181
throw new BailianError(
7282
`Skill ${name} uses unsupported compression format: ${entry.compression}`,
7383
ExitCode.GENERAL,
7484
"Upgrade bailian-cli to the latest version and retry",
7585
);
7686
}
77-
const buffer = await downloadSkillAsset(name, entry);
87+
const buffer = await downloadSkillAsset(name, entry, downloadAttempts);
7888
return installSkillFromBuffer(name, buffer, entry.contentHash);
7989
}
8090

@@ -116,14 +126,17 @@ export interface SkillInstallRecord {
116126
* entry (batch writeSkillLock for commands, best-effort upsertSkillLockEntry for silent channels).
117127
* recordedLinks = the skill's previously recorded fan-out paths from the lock; lets the
118128
* fan-out replace copy-fallback artifacts and keeps unvisited paths reclaimable.
129+
* downloadAttempts = registry fetch attempts (undefined → interactive default; silent
130+
* background channels pass 1 to fail fast instead of stalling the host command).
119131
*/
120132
export async function installSkillWithFanout(
121133
name: string,
122134
entry: SkillIndexEntry,
123135
agents: AgentTarget[] = detectInstalledAgents(),
124136
recordedLinks: string[] = [],
137+
downloadAttempts?: number,
125138
): Promise<SkillInstallRecord> {
126-
await installSkill(name, entry);
139+
await installSkill(name, entry, downloadAttempts);
127140
const fanout = fanOutSkillToAgents(name, agents, recordedLinks);
128141
return {
129142
lockEntry: buildSkillLockEntry(entry, fanout.links),
Lines changed: 95 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BailianError } from "../errors/base.ts";
22
import { ExitCode } from "../errors/codes.ts";
3+
import { withRetry } from "../utils/retry.ts";
34
import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
45

56
/**
@@ -9,8 +10,10 @@ import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
910
*/
1011
const 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;
1314
const 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

1518
export 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

Comments
 (0)