Skip to content

Commit 06f932c

Browse files
committed
Merge branch 'main' into feat/model-usage-and-detail
2 parents eca5403 + 1c5d1a8 commit 06f932c

39 files changed

Lines changed: 1697 additions & 59 deletions

AGENTS.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
monorepo 现在按"纯逻辑 → 运行时框架 → 命令库 → 产品入口"分层:
88

99
- `packages/core``bailian-cli-core`,纯逻辑层:鉴权、配置、HTTP client、错误、类型、文件工具
10-
- `packages/runtime``bailian-cli-runtime`,通用 CLI 运行时:`createCli`、参数解析、registry/help、middleware、error handler、输出、pipeline
10+
- `packages/runtime``bailian-cli-runtime`,通用 CLI 运行时:`createCli`、参数解析、registry/help、middleware、error handler、输出、pipeline、Command Pack host
1111
- `packages/commands``bailian-cli-commands`,可复用命令实现库,只导出 command,不决定产品路径
1212
- `packages/cli``bailian-cli`,完整 `bl` 产品入口;`src/commands.ts` 组装 `bl` 暴露的命令路径
1313
- `packages/kscli``knowledge-studio-cli`,Knowledge Studio 专用入口;`src/main.ts` 复用 commands 并重映射为 `kscli` 路径
@@ -17,14 +17,16 @@ monorepo 现在按"纯逻辑 → 运行时框架 → 命令库 → 产品入口"
1717
```
1818
packages/cli/src/main.ts # bl 入口,注入 binName/version/clientName/npmPackage
1919
packages/cli/src/commands.ts # bl 产品命令 map,tools/generate-reference.ts 也读它
20+
packages/cli/src/command-pack-policy.ts # bl 的 Command Pack policy
2021
packages/kscli/src/main.ts # kscli 入口和命令 map
2122
2223
packages/commands/src/index.ts # re-export 单个命令实现
2324
packages/commands/src/commands/ # defineCommand({ auth, flags, usageArgs, exampleArgs, run })
2425
25-
packages/runtime/src/create-cli.ts # createCli(commands, identity)
26+
packages/runtime/src/create-cli.ts # createCli(commands, options)
2627
packages/runtime/src/registry.ts # 命令树解析 + 动态 help
2728
packages/runtime/src/middleware.ts # auth / telemetry / update / run command
29+
packages/runtime/src/command-packs/ # 通用 Command Pack 加载、校验、隔离安装目录和管理命令
2830
packages/runtime/src/urls.ts # 用户面控制台 URL
2931
3032
packages/core/src/types/command.ts # Command / flags / auth 类型
@@ -68,6 +70,7 @@ Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/
6870
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
6971
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
7072
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
73+
| Command Pack | 扩展包 / 白名单 / plugin 管理命令 | [docs/agents/command-pack.md](docs/agents/command-pack.md) |
7174

7275
如果当前任务无法对应任何场景,先按经验完成,然后**回来评估这是不是一类新场景** —— 是就新增 `docs/agents/<scenario>.md`,把清单沉淀下来。
7376

docs/agents/auth-change.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
4848
- `resolveOpenApi()``auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
4949
- `describeAuthState()``auth status` / banner / telemetry 使用的只读快照
5050

51-
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore()` / `ctx.configStore()` 的窄接口操作落盘。
51+
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore` / `ctx.configStore` 的窄接口操作落盘。
5252

5353
## 必查清单
5454

@@ -87,7 +87,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
8787

8888
- [ ] `packages/commands/src/commands/auth/login.ts`:
8989
- 新增/调整登录 flag 与流程
90-
- 持久化只走 `ctx.authStore().login(...)`
90+
- 持久化只走 `ctx.authStore.login(...)`
9191
- [ ] `packages/commands/src/commands/auth/status.ts`:
9292
- 分别显示 model / console / openapi 鉴权状态,并 mask token
9393
- [ ] `packages/commands/src/commands/auth/logout.ts`:

docs/agents/command-add-remove.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ packages/commands/src/index.ts
7272
- `exampleArgs`(不含 bin/path 前缀)
7373
- `validate`(跨 flag 校验)
7474
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
75-
- `commands/auth/**` 可用 `ctx.authStore()`,`commands/config/**` 可用 `ctx.configStore()`;不要把这些 store accessor 扩散到普通业务命令
75+
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
76+
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
7677
- [ ] `packages/commands/src/index.ts`:新增或移除对应 export
7778
- [ ] 如果命令调用 Console Gateway,设置 `auth: "console"`;不要重复声明 console 凭证域 flags
7879
- [ ] 如果命令不需要网络或自己管理配置/登录,设置 `auth: "none"`;不要绕过 runtime auth stage

docs/agents/command-pack.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Command Pack 维护
2+
3+
## 触发条件
4+
5+
- 新增或移除 Command Pack 包
6+
- 调整包白名单、允许的命令前缀或协议字段
7+
- 修改 `plugin install/link/list/remove`
8+
- 修改 Command Pack 加载、隔离、兼容性或独立安装目录
9+
10+
## 分层边界
11+
12+
- `packages/core/src/types/command-pack.ts`:稳定的协议元数据和导出类型,不知道具体产品或白名单。
13+
- `packages/runtime/src/command-packs/`:所有 CLI 共用的加载、校验、API 适配、产品隔离安装目录和 manager 实现。
14+
- `packages/runtime/src/create-cli.ts`:始终接收静态 command map,按 `CliOptions.commandPacks` 统一合并 pack,并把已绑定产品 identity/policy 的 manager 注入 `ctx.commandPacks`
15+
- `packages/commands/src/commands/plugin/`:普通共享管理命令,只依赖 `ctx.commandPacks`,不 import 任何产品 policy。
16+
- `packages/cli/src/command-pack-policy.ts``bl` 支持的包、命令前缀和凭据授权。
17+
- `kscli` 当前不传 `commandPacks`,使用 runtime 的默认空 policy。
18+
- 当前只有 `bl``bailian-cli-commands` 导入并登记 `plugin *`;使用默认空 policy 的产品不提前暴露管理命令。
19+
20+
不要把产品白名单写进 core/runtime,也不要通过扫描全局 `node_modules` 自动发现包。通用机制放 runtime,产品差异只由 policy 表达。
21+
22+
## 安全与兼容性清单
23+
24+
- [ ] 包名必须精确命中当前产品 policy 的 `supported`,命令路径必须位于该包允许的前缀。
25+
- [ ] 正式安装只接受包名加 version/tag;本地目录只走 `plugin link`
26+
- [ ] npm 使用独立安装目录和 `--ignore-scripts`,不污染 CLI 自身依赖树。
27+
- [ ] npm 子进程只继承明确允许的 registry/config/cache/proxy/TLS 配置,不通配透传 pnpm 注入的 `npm_config_*`
28+
- [ ] 安装目录按 `identity.npmPackage` 隔离,不能让一个产品安装/删除另一个产品的 pack。
29+
- [ ] 安装目录只隔离依赖位置,不隔离执行权限;Command Pack 必须视为 CLI 进程内的完全可信代码。
30+
- [ ] 入口 realpath 不能逃逸包根目录。
31+
- [ ] 加载前检查 `type``apiVersion``minCliVersion`;报告状态只使用 `loaded/failed`,具体原因写入 `error`
32+
- [ ] Command Pack 不能覆盖内置命令、其他 pack 命令或重声明保留 flag。
33+
- [ ] 普通网络请求走 `ctx.client`;基础 Context 提供 `identity/settings/flags/client/output/errors`,不提供原始凭据。
34+
- [ ] `ctx.credentials.apiKey()` 仅限 policy 显式声明 `credentialAccess: ["apiKey"]`,且命令自身为 `auth: "apiKey"`
35+
- [ ] 不向 Command Pack 暴露原始 Console Token、OpenAPI AK/SK、`authStore``configStore`
36+
- [ ] 不向 Command Pack 暴露宿主的 `commandPacks` manager,避免 pack 安装或删除其他 pack。
37+
- [ ] 单包失败必须 fail-open:保留内置命令和其他合法 pack。
38+
- [ ] 破坏协议前优先在适配层兼容;确实无法兼容时才提升 `apiVersion`
39+
40+
## 测试与文档
41+
42+
- [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。
43+
- [ ] `packages/cli/tests/e2e/command-packs.e2e.test.ts` 覆盖 help、link、执行、output/errors、凭据授权、list、remove。
44+
- [ ] `packages/kscli/tests/e2e/command-packs.e2e.test.ts` 覆盖统一 host 和 runtime 默认空 policy 下不暴露管理命令。
45+
- [ ] fixture 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。
46+
- [ ] 更新生成的 `skills/bailian-cli/reference/plugin.md`;公开 `README.md` / `README.zh.md` 等正式对外发布时再补。
47+
48+
验证:
49+
50+
```sh
51+
vp test packages/runtime/tests/command-packs.test.ts
52+
vp test packages/cli/tests/e2e/command-packs.e2e.test.ts
53+
vp test packages/kscli/tests/e2e/command-packs.e2e.test.ts
54+
pnpm run sync:skill-assets
55+
vp check
56+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { CommandPackPolicy } from "bailian-cli-runtime";
2+
3+
/** Command Packs accepted by the bl product. */
4+
export const commandPackPolicy = {
5+
supported: {
6+
"@ali/bailian-plugin-agent": {
7+
commandPrefixes: ["agent"],
8+
credentialAccess: ["apiKey"],
9+
},
10+
},
11+
} as const satisfies CommandPackPolicy;

packages/cli/src/commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ import {
7979
tokenPlanCreateKey,
8080
tokenPlanAssignSeats,
8181
tokenPlanAddMember,
82+
pluginInstall,
83+
pluginLink,
84+
pluginList,
85+
pluginRemove,
8286
} from "bailian-cli-commands";
8387

8488
// Full bailian-cli product: every command, exposed under the `bl` binary.
@@ -166,4 +170,8 @@ export const commands: Record<string, AnyCommand> = {
166170
"token-plan create-key": tokenPlanCreateKey,
167171
"token-plan assign-seats": tokenPlanAssignSeats,
168172
"token-plan add-member": tokenPlanAddMember,
173+
"plugin install": pluginInstall,
174+
"plugin link": pluginLink,
175+
"plugin list": pluginList,
176+
"plugin remove": pluginRemove,
169177
};

packages/cli/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createCli } from "bailian-cli-runtime";
22
import { commands } from "./commands.ts";
3+
import { commandPackPolicy } from "./command-pack-policy.ts";
34
import pkg from "../package.json" with { type: "json" };
45

56
const quickStartTasks = [
@@ -15,4 +16,5 @@ void createCli(commands, {
1516
clientName: "bailian-cli",
1617
npmPackage: "bailian-cli",
1718
quickStartTasks,
19+
commandPacks: commandPackPolicy,
1820
}).run();
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { mkdtemp, rm } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test";
6+
import { parseStdoutJson, runCli } from "./helpers.ts";
7+
8+
const fixtureRoot = join(fileURLToPath(import.meta.url), "..", "..", "fixtures", "command-pack");
9+
let configDir: string;
10+
11+
function env(): NodeJS.ProcessEnv {
12+
return { BAILIAN_CONFIG_DIR: configDir, DO_NOT_TRACK: "1" };
13+
}
14+
15+
describe("e2e: Command Pack", () => {
16+
beforeAll(async () => {
17+
configDir = await mkdtemp(join(tmpdir(), "bl-command-pack-e2e-"));
18+
});
19+
20+
afterAll(async () => {
21+
await rm(configDir, { recursive: true, force: true });
22+
});
23+
24+
test("plugin 分组和管理命令 help 正常", async () => {
25+
const group = await runCli(["plugin"], env());
26+
expect(group.exitCode, group.stderr).toBe(0);
27+
expect(group.stderr).toContain("plugin install");
28+
expect(group.stderr).toContain("plugin link");
29+
expect(group.stderr).toContain("plugin list");
30+
expect(group.stderr).toContain("plugin remove");
31+
32+
const install = await runCli(["plugin", "install", "--help"], env());
33+
expect(install.exitCode, install.stderr).toBe(0);
34+
expect(install.stderr).toContain("--package");
35+
});
36+
37+
test("未 link 时插件命令不存在", async () => {
38+
const result = await runCli(["agent", "ping", "--message", "before"], env());
39+
expect(result.exitCode).not.toBe(0);
40+
expect(result.stderr).toMatch(/Unknown command/i);
41+
});
42+
43+
test("link 后命令进入原生 registry/help/执行链路", async () => {
44+
const linked = await runCli(
45+
["plugin", "link", "--path", fixtureRoot, "--output", "json"],
46+
env(),
47+
);
48+
expect(linked.exitCode, linked.stderr).toBe(0);
49+
const linkedJson = parseStdoutJson<{ linked: { name: string; commands: string[] } }>(
50+
linked.stdout,
51+
);
52+
expect(linkedJson.linked.name).toBe("@ali/bailian-plugin-agent");
53+
expect(linkedJson.linked.commands).toEqual([
54+
"agent credential",
55+
"agent credential-denied",
56+
"agent fail",
57+
"agent output",
58+
"agent ping",
59+
]);
60+
61+
const rootHelp = await runCli(["--help"], env());
62+
expect(rootHelp.exitCode, rootHelp.stderr).toBe(0);
63+
expect(rootHelp.stderr).toContain("agent ping");
64+
65+
const commandHelp = await runCli(["agent", "ping", "--help"], env());
66+
expect(commandHelp.exitCode, commandHelp.stderr).toBe(0);
67+
expect(commandHelp.stderr).toContain("Ping the Command Pack fixture");
68+
expect(commandHelp.stderr).toContain("--message");
69+
70+
const executed = await runCli(["agent", "ping", "--message", "hello"], env());
71+
expect(executed.exitCode, executed.stderr).toBe(0);
72+
expect(executed.stdout).toContain("command-pack:hello");
73+
74+
const credential = await runCli(["agent", "credential", "--api-key", "fixture-key"], env());
75+
expect(credential.exitCode, credential.stderr).toBe(0);
76+
expect(credential.stdout).toContain("credential-source:flag");
77+
78+
const denied = await runCli(["agent", "credential-denied"], {
79+
...env(),
80+
DASHSCOPE_API_KEY: "fixture-key",
81+
});
82+
expect(denied.exitCode).toBe(1);
83+
expect(denied.stderr).toContain('must declare auth="apiKey"');
84+
85+
const outputText = await runCli(["agent", "output"], env());
86+
expect(outputText.exitCode, outputText.stderr).toBe(0);
87+
expect(outputText.stdout).toBe("command-pack-output\n");
88+
89+
const outputJson = await runCli(["agent", "output", "--output", "json"], env());
90+
expect(outputJson.exitCode, outputJson.stderr).toBe(0);
91+
expect(parseStdoutJson(outputJson.stdout)).toEqual({ source: "command-pack", ok: true });
92+
93+
const failed = await runCli(["agent", "fail", "--output", "text"], env());
94+
expect(failed.exitCode).toBe(2);
95+
expect(failed.stderr).toContain("Command Pack fixture usage error.");
96+
expect(failed.stderr).toContain("Use agent fail only in tests.");
97+
});
98+
99+
test("plugin list 输出加载状态", async () => {
100+
const result = await runCli(["plugin", "list", "--output", "json"], env());
101+
expect(result.exitCode, result.stderr).toBe(0);
102+
const json = parseStdoutJson<{
103+
command_packs: Array<{ name: string; status: string; commands: string[] }>;
104+
}>(result.stdout);
105+
expect(json.command_packs).toEqual([
106+
expect.objectContaining({
107+
name: "@ali/bailian-plugin-agent",
108+
status: "loaded",
109+
commands: [
110+
"agent credential",
111+
"agent credential-denied",
112+
"agent fail",
113+
"agent output",
114+
"agent ping",
115+
],
116+
}),
117+
]);
118+
});
119+
120+
test("remove 后命令从下一进程消失", async () => {
121+
const removed = await runCli(
122+
["plugin", "remove", "--name", "@ali/bailian-plugin-agent", "--output", "json"],
123+
env(),
124+
);
125+
expect(removed.exitCode, removed.stderr).toBe(0);
126+
expect(parseStdoutJson<{ removed: string }>(removed.stdout).removed).toBe(
127+
"@ali/bailian-plugin-agent",
128+
);
129+
130+
const result = await runCli(["agent", "ping", "--message", "after"], env());
131+
expect(result.exitCode).not.toBe(0);
132+
expect(result.stderr).toMatch(/Unknown command/i);
133+
});
134+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const ping = {
2+
description: "Ping the Command Pack fixture",
3+
auth: "none",
4+
flags: {
5+
message: {
6+
type: "string",
7+
valueHint: "<text>",
8+
required: true,
9+
description: "Message returned by the fixture",
10+
},
11+
},
12+
usageArgs: "--message <text>",
13+
exampleArgs: ['--message "hello"'],
14+
async run(ctx) {
15+
process.stdout.write(`command-pack:${ctx.flags.message}\n`);
16+
},
17+
};
18+
19+
const credential = {
20+
description: "Read an API key through the Command Pack host adapter",
21+
auth: "apiKey",
22+
async run(ctx) {
23+
const apiKey = ctx.credentials.apiKey();
24+
process.stdout.write(`credential-source:${apiKey.source}\n`);
25+
},
26+
};
27+
28+
const credentialDenied = {
29+
description: "Verify credential access also requires command auth",
30+
auth: "none",
31+
async run(ctx) {
32+
ctx.credentials.apiKey();
33+
},
34+
};
35+
36+
const output = {
37+
description: "Exercise the Command Pack output helper",
38+
auth: "none",
39+
async run(ctx) {
40+
ctx.output.result({ source: "command-pack", ok: true }, { text: "command-pack-output" });
41+
},
42+
};
43+
44+
const fail = {
45+
description: "Exercise the Command Pack semantic error helper",
46+
auth: "none",
47+
async run(ctx) {
48+
throw ctx.errors.usage("Command Pack fixture usage error.", "Use agent fail only in tests.");
49+
},
50+
};
51+
52+
export default {
53+
"agent credential": credential,
54+
"agent credential-denied": credentialDenied,
55+
"agent fail": fail,
56+
"agent output": output,
57+
"agent ping": ping,
58+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@ali/bailian-plugin-agent",
3+
"version": "0.0.0-test",
4+
"private": true,
5+
"type": "module",
6+
"bailianCli": {
7+
"type": "command-pack",
8+
"apiVersion": 1,
9+
"entry": "./commands.mjs",
10+
"minCliVersion": "1.7.0"
11+
}
12+
}

0 commit comments

Comments
 (0)