Skip to content

Commit ff469ce

Browse files
committed
feat(install-docs): enhance installation documentation and validation processes
1 parent 4751145 commit ff469ce

10 files changed

Lines changed: 460 additions & 89 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/
6868
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
6969
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
7070
| Profile / 激活 | 改命名 Profile、预设或 `active_config` | [docs/agents/config-profile-change.md](docs/agents/config-profile-change.md) |
71+
| 安装文档 | 改安装、鉴权、验证流程或线上 install 页面 | [docs/agents/install-doc-change.md](docs/agents/install-doc-change.md) |
7172
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
7273
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
7374
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |

INSTALL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ npx skills add modelstudioai/cli --all -g
9898
### Agent 安全约束
9999

100100
- **禁止**把真实 API Key 写入仓库、日志、Skill、聊天记录的可公开部分。
101-
- CI / 非交互环境:使用 `bl ... --non-interactive`通过密钥管理或环境变量注入,勿在脚本中硬编码 Key。
101+
- CI / 非交互环境:显式传入必填参数并使用 `--output json` 获取机器可读结果;如需纯文本输出,设置 `NO_COLOR=1`通过密钥管理或环境变量注入,勿在脚本中硬编码 Key。
102102

103103
---
104104

docs/agents/install-doc-change.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# 安装文档变更
2+
3+
## 触发条件
4+
5+
- 修改根目录 `INSTALL.md` 的安装、鉴权或验证流程
6+
- 修改发布包 Node.js 要求、全局 flag 或安装文档引用的命令
7+
- 同步或发布 `https://bailian.aliyun.com/cli/install.md`
8+
9+
## 必查清单
10+
11+
### A. CLI 契约
12+
13+
- [ ] `INSTALL.md` 中的 `bl` 命令路径存在于 `packages/cli/src/commands.ts`
14+
- [ ] 示例 flag 属于 `GLOBAL_FLAGS`、命令鉴权域 flag 或命令自身 `flags`
15+
- [ ] Node.js 用户安装要求与 `packages/cli/package.json``engines.node` 一致,不使用根 `package.json` 的开发环境要求
16+
- [ ] 鉴权流程与 `packages/commands/src/commands/auth/` 的实际校验、保存和 Profile 激活行为一致
17+
18+
### B. 静态副本
19+
20+
- [ ]`INSTALL.md` 同步到 `bailian-cli-static-resources/public/install.txt`
21+
- [ ] 使用 `cmp -s` 确认两份文档逐字节一致
22+
- [ ] 静态资源仓库单独创建分支、提交和发布,不把跨仓库改动遗漏在 CLI PR 之外
23+
24+
### C. 线上验证
25+
26+
- [ ] 发布后读取 `https://bailian.aliyun.com/cli/install.md`,确认内容来自最新静态副本
27+
- [ ] 带随机 query 参数复查,区分 CDN 缓存与源站未更新
28+
- [ ] 验证线上文档中的安装命令、Node.js 要求和配置验证段落,不只检查页面可访问
29+
30+
## 完成后自查
31+
32+
```sh
33+
pnpm -F bailian-cli test -- tests/install-doc.test.ts
34+
cmp -s INSTALL.md ../bailian-cli-static-resources/public/install.txt
35+
curl -L -s "https://bailian.aliyun.com/cli/install.md?verify=$(date +%s)"
36+
```
37+
38+
## 常见漏点
39+
40+
- `--non-interactive` 已从 CLI 移除,但旧安装文档和静态副本仍把它当作全局 flag
41+
-`package.json` 是开发工具链 Node.js 要求;用户安装要求以 `packages/cli/package.json` 为准
42+
- 静态仓库文件名是 `public/install.txt`,线上稳定地址是 `/cli/install.md`;只更新其中一侧不会自动证明发布成功
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { credentialFlagDefs, GLOBAL_FLAGS, type AnyCommand } from "bailian-cli-core";
4+
import { monorepoRoot } from "e2e/monorepo-root";
5+
import { describe, expect, test } from "vite-plus/test";
6+
import { commands } from "../src/commands.ts";
7+
8+
const repositoryRoot = monorepoRoot();
9+
const installGuide = readFileSync(join(repositoryRoot, "INSTALL.md"), "utf8");
10+
const cliPackage = JSON.parse(
11+
readFileSync(join(repositoryRoot, "packages/cli/package.json"), "utf8"),
12+
) as {
13+
engines?: { node?: string };
14+
};
15+
16+
function toFlagName(key: string): string {
17+
return `--${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
18+
}
19+
20+
function findDocumentedCommand(snippet: string): {
21+
commandPath?: string;
22+
command?: AnyCommand;
23+
} {
24+
const argumentText = snippet.slice("bl ".length).trim();
25+
const commandPath = Object.keys(commands)
26+
.sort((leftPath, rightPath) => rightPath.length - leftPath.length)
27+
.find((candidatePath) => {
28+
return argumentText === candidatePath || argumentText.startsWith(`${candidatePath} `);
29+
});
30+
31+
return commandPath ? { commandPath, command: commands[commandPath] } : {};
32+
}
33+
34+
function documentedCommandSnippets(): string[] {
35+
const fencedCommands = installGuide
36+
.split("\n")
37+
.map((line) => line.trim())
38+
.filter((line) => line.startsWith("bl "));
39+
const inlineCommands = Array.from(installGuide.matchAll(/`(bl [^`\n]+)`/g), (match) => match[1]);
40+
return [...new Set([...fencedCommands, ...inlineCommands])];
41+
}
42+
43+
describe("INSTALL.md", () => {
44+
test("发布包 Node.js 要求与安装文档一致", () => {
45+
const nodeEngine = cliPackage.engines?.node;
46+
expect(nodeEngine).toMatch(/^>=\d+\.\d+\.\d+$/);
47+
expect(installGuide).toContain(`要求 **≥ ${nodeEngine?.slice(2)}**`);
48+
});
49+
50+
test("示例只使用当前命令支持的 flags", () => {
51+
for (const snippet of documentedCommandSnippets()) {
52+
const { commandPath, command } = findDocumentedCommand(snippet);
53+
const argumentText = snippet.slice("bl ".length).trim();
54+
55+
if (!commandPath || !command) {
56+
expect(argumentText, `INSTALL.md 中存在未知命令:${snippet}`).toMatch(/^--/);
57+
}
58+
59+
const supportedFlags = {
60+
...GLOBAL_FLAGS,
61+
...(command ? credentialFlagDefs(command) : {}),
62+
...command?.flags,
63+
};
64+
const supportedFlagNames = new Set(Object.keys(supportedFlags).map(toFlagName));
65+
const usedFlagNames = Array.from(snippet.matchAll(/--[a-z0-9-]+/g), (match) => match[0]);
66+
const unsupportedFlagNames = usedFlagNames.filter(
67+
(flagName) => !supportedFlagNames.has(flagName),
68+
);
69+
70+
expect(unsupportedFlagNames, `INSTALL.md 命令使用了未声明的 flag:${snippet}`).toEqual([]);
71+
}
72+
});
73+
});

packages/commands/src/commands/config/agent/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ export default defineCommand({
9494
emitBare(`${agentDef.label} configured successfully.`);
9595
for (const path of summary.paths) emitBare(` Written: ${path}`);
9696
emitBare(` ${summary.nextStep}`);
97+
for (const warning of summary.warnings ?? []) {
98+
process.stderr.write(`Warning: ${warning}\n`);
99+
}
97100
}
98101
},
99102
});
Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import { homedir } from "os";
22
import { join } from "path";
3-
import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts";
3+
import {
4+
backup,
5+
readJson,
6+
writeJsonAtomic,
7+
resolveClaudeCodeBaseUrl,
8+
type AgentDef,
9+
} from "./utils.ts";
10+
11+
/** Fill a tier/default model env only when the user has not set it yet. */
12+
function setModelEnvIfAbsent(env: Record<string, string>, key: string, model: string): void {
13+
const current = env[key];
14+
if (current === undefined || current.trim() === "") {
15+
env[key] = model;
16+
}
17+
}
418

519
export default {
620
label: "Claude Code",
@@ -10,22 +24,33 @@ export default {
1024
process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
1125
const settingsPath = join(configDir, "settings.json");
1226
const onboardingPath = join(homedir(), ".claude.json");
27+
const warnings: string[] = [];
28+
29+
const resolved = resolveClaudeCodeBaseUrl(baseUrl);
30+
if (resolved.rewrittenFrom) {
31+
warnings.push(
32+
`Rewrote base URL for Claude Code: "${resolved.rewrittenFrom}" → "${resolved.url}" ` +
33+
`(Claude Code needs /apps/anthropic, not OpenAI compatible-mode).`,
34+
);
35+
}
1336

1437
// settings.json — merge env. Base URL + auth token connect Claude Code to
15-
// the endpoint; the model tier vars force every tier onto the chosen model.
38+
// the Anthropic-compatible endpoint; primary model always updates, while
39+
// tier/subagent defaults are filled only when absent so existing setups
40+
// (e.g. Token Plan Haiku/Subagent splits) are not wiped.
1641
backup(settingsPath);
1742
const settings = readJson(settingsPath);
1843
const env = (settings.env ?? {}) as Record<string, string>;
19-
env.ANTHROPIC_BASE_URL = baseUrl;
44+
env.ANTHROPIC_BASE_URL = resolved.url;
2045
env.ANTHROPIC_AUTH_TOKEN = apiKey;
2146
// AUTH_TOKEN and API_KEY are mutually exclusive credential fields — drop a
2247
// stale ANTHROPIC_API_KEY so it cannot shadow the token we just wrote.
2348
delete env.ANTHROPIC_API_KEY;
2449
env.ANTHROPIC_MODEL = model;
25-
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model;
26-
env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
27-
env.ANTHROPIC_DEFAULT_OPUS_MODEL = model;
28-
env.CLAUDE_CODE_SUBAGENT_MODEL = model;
50+
setModelEnvIfAbsent(env, "ANTHROPIC_DEFAULT_HAIKU_MODEL", model);
51+
setModelEnvIfAbsent(env, "ANTHROPIC_DEFAULT_SONNET_MODEL", model);
52+
setModelEnvIfAbsent(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", model);
53+
setModelEnvIfAbsent(env, "CLAUDE_CODE_SUBAGENT_MODEL", model);
2954
settings.env = env;
3055
writeJsonAtomic(settingsPath, settings);
3156

@@ -38,6 +63,7 @@ export default {
3863
return {
3964
paths: [settingsPath, onboardingPath],
4065
nextStep: "Run `claude` to start using Claude Code with DashScope.",
66+
warnings: warnings.length > 0 ? warnings : undefined,
4167
};
4268
},
4369
} satisfies AgentDef;

packages/commands/src/commands/config/agent/writers/openclaw.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,32 @@ import {
1212
// offer ≥256K context; users can raise it per model via the flag.
1313
const DEFAULT_CONTEXT_WINDOW = 256000;
1414

15+
const PROVIDER_ID = "bailian-cli";
16+
17+
function readPrimary(defaults: Record<string, unknown>): string | undefined {
18+
const model = defaults.model;
19+
if (!model || typeof model !== "object") return undefined;
20+
const primary = (model as Record<string, unknown>).primary;
21+
return typeof primary === "string" && primary.trim() !== "" ? primary.trim() : undefined;
22+
}
23+
1524
export default {
1625
label: "OpenClaw",
1726
write({ baseUrl, apiKey, model, contextWindow }) {
1827
const configPath = join(homedir(), ".openclaw", "openclaw.json");
28+
const warnings: string[] = [];
29+
const modelRef = `${PROVIDER_ID}/${model}`;
1930

2031
backup(configPath);
2132
const config = readJson(configPath);
2233

23-
// models.providers["bailian-cli"]
34+
// models.providers["bailian-cli"] — upsert without removing other providers
35+
// (e.g. an existing working bailian-token-plan setup).
2436
const models = (config.models ?? {}) as Record<string, unknown>;
2537
models.mode = "merge";
2638
const providers = (models.providers ?? {}) as Record<string, unknown>;
27-
const api = isAnthropicEndpoint(baseUrl)
28-
? "anthropic-messages"
29-
: "openai-completions";
30-
providers["bailian-cli"] = {
39+
const api = isAnthropicEndpoint(baseUrl) ? "anthropic-messages" : "openai-completions";
40+
providers[PROVIDER_ID] = {
3141
baseUrl,
3242
apiKey,
3343
api,
@@ -43,14 +53,27 @@ export default {
4353
models.providers = providers;
4454
config.models = models;
4555

46-
// agents.defaults — select the model and register it in the allowlist.
56+
// agents.defaults — register the model in the allow-list. Only set primary
57+
// when unset, or when primary already points at bailian-cli (reconfigure).
58+
// Never steal primary away from another provider such as bailian-token-plan.
4759
const agents = (config.agents ?? {}) as Record<string, unknown>;
4860
const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
49-
const primary = `bailian-cli/${model}`;
50-
defaults.model = { primary };
51-
const allowlist = (defaults.models ?? {}) as Record<string, unknown>;
52-
allowlist[primary] = allowlist[primary] ?? {};
53-
defaults.models = allowlist;
61+
const allowedModels = (defaults.models ?? {}) as Record<string, unknown>;
62+
allowedModels[modelRef] = allowedModels[modelRef] ?? {};
63+
defaults.models = allowedModels;
64+
65+
const existingPrimary = readPrimary(defaults);
66+
if (!existingPrimary) {
67+
defaults.model = { primary: modelRef };
68+
} else if (existingPrimary.startsWith(`${PROVIDER_ID}/`)) {
69+
defaults.model = { primary: modelRef };
70+
} else {
71+
warnings.push(
72+
`Left existing primary model unchanged ("${existingPrimary}"). ` +
73+
`Added provider "${PROVIDER_ID}" — switch to "${modelRef}" in OpenClaw if you want to use it.`,
74+
);
75+
}
76+
5477
agents.defaults = defaults;
5578
config.agents = agents;
5679

@@ -60,6 +83,7 @@ export default {
6083
paths: [configPath],
6184
nextStep:
6285
"Run `openclaw gateway restart`, then `openclaw` to start using OpenClaw with DashScope.",
86+
warnings: warnings.length > 0 ? warnings : undefined,
6387
};
6488
},
6589
} satisfies AgentDef;

0 commit comments

Comments
 (0)