Skip to content

Commit d6bd38a

Browse files
committed
feat(agent): agent相关cli命令的client层功能,对齐cli client的基础能力
1 parent 6329427 commit d6bd38a

29 files changed

Lines changed: 723 additions & 58 deletions

docs/agents/auth-change.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
5050

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

53+
### 例外:agent 命令的 SDK 凭证桥接
54+
55+
`bl agent *` 命令声明 `auth: "none"`,凭证由 `@openagentpack/sdk` 自主从 env 解析(agents.yaml 的 `${DASHSCOPE_API_KEY}` / `${BAILIAN_WORKSPACE_ID}` 插值)。为让 bl 登录态复用,`packages/commands/src/commands/agent/_engine/credentials.ts``bridgeBailianCredentials()`**直接 `readConfigFile()`**,把 config 的 `api_key` / `workspace_id` 作为最低优先级兜底填入对应 env,仅填空值,不覆盖已有。这是唯一允许命令层直接读 config 的场景(SDK 只认 env,不走 `ctx.client`);优先级链:`~/.agents/config.json` > shell env > `.env` > `~/.bailian/config.json`
56+
5357
## 必查清单
5458

5559
### A. core 层(类型 + 解析)

packages/cli/.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ node_modules
22
dist
33
*.log
44
.DS_Store
5-
outputs/
5+
outputs/
6+
# agents
7+
agents.state.json
8+
.env

packages/cli/agents.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: "1"
2+
3+
providers:
4+
bailian:
5+
api_key: ${DASHSCOPE_API_KEY}
6+
workspace_id: ${BAILIAN_WORKSPACE_ID}
7+
8+
defaults:
9+
provider: bailian
10+
11+
environments:
12+
dev:
13+
config:
14+
type: cloud
15+
networking:
16+
type: unrestricted
17+
18+
agents:
19+
assistant:
20+
description: "General-purpose assistant"
21+
model: qwen3.7-max
22+
instructions: |
23+
You are a helpful assistant.
24+
environment: dev
25+
tools:
26+
builtin: [bash, read, glob, grep]

packages/commands/src/commands/agent/_engine/config-loader.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ import {
66
} from "@openagentpack/sdk";
77
import { ensureCredentials } from "./credentials.ts";
88
import { loadFileState } from "./file-state-manager.ts";
9+
import { type HostContext, installSdkTransport } from "./transport.ts";
10+
11+
export { CREDENTIALS_NOTE } from "./credentials.ts";
912

1013
/**
1114
* Build a full ProjectRuntimeContext from a config file path — the standard
1215
* entry point for agent commands that need the SDK engine. Mirrors OpenAgentPack
1316
* CLI's buildCliRuntime: resolve config → load local state → assemble runtime.
17+
* Takes the host context first so every SDK-engine command wires the
18+
* instrumented transport (UA / tracking headers / verbose) by construction.
1419
*/
1520
export async function buildAgentRuntime(
21+
host: HostContext,
1622
filePath: string,
17-
options: { resolveEnv?: boolean; projectName?: string; statePath?: string } = {},
23+
options: {
24+
resolveEnv?: boolean;
25+
projectName?: string;
26+
statePath?: string;
27+
} = {},
1828
): Promise<ProjectRuntimeContext & { configPath: string }> {
29+
installSdkTransport(host);
1930
ensureCredentials();
2031
const { config, configPath, projectName } = await resolveProjectConfig(filePath, options);
2132
const state = await loadFileState(configPath, options.statePath, projectName);
Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,59 @@
11
import { bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk";
2+
import { readConfigFile } from "bailian-cli-core";
23

34
let bootstrapped = false;
45

6+
/**
7+
* Shared `--help` note documenting where agent commands get provider
8+
* credentials. Mirrors the credential-source hint bl's native commands surface
9+
* (knowledge / usage / token-plan), adapted for the SDK's env-based resolution
10+
* and the {@link bridgeBailianCredentials} fallback. Attach to every command
11+
* that loads agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
12+
*/
13+
export const CREDENTIALS_NOTE = [
14+
"Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).",
15+
"For the bailian provider, bl fills these from your login as a fallback: `bl auth login` (API key) and `bl config set workspace_id <id>`.",
16+
];
17+
18+
/**
19+
* Bridge bl's own login state into the env vars the OpenAgentPack SDK reads for
20+
* the bailian provider. bl persists `api_key` / `workspace_id` in
21+
* `~/.bailian/config.json` (via `bl auth login` / `bl config set`); mirror them
22+
* onto `DASHSCOPE_API_KEY` / `BAILIAN_WORKSPACE_ID` so users don't have to
23+
* re-declare the same credentials for `bl agent *`.
24+
*
25+
* Lowest priority: only fills a var that is still unset, so anything already in
26+
* the environment (shell export, `.env`, or `~/.agents/config.json` — which the
27+
* SDK bootstrap has already applied) wins. Missing values are left alone; the
28+
* SDK surfaces its own error when interpolation can't resolve, and non-bailian
29+
* providers (claude/qoder/ark) don't need DashScope credentials at all.
30+
*/
31+
export function bridgeBailianCredentials(): void {
32+
const file = readConfigFile();
33+
if (!process.env.DASHSCOPE_API_KEY?.trim() && file.api_key) {
34+
process.env.DASHSCOPE_API_KEY = file.api_key;
35+
}
36+
if (!process.env.BAILIAN_WORKSPACE_ID?.trim() && file.workspace_id) {
37+
process.env.BAILIAN_WORKSPACE_ID = file.workspace_id;
38+
}
39+
}
40+
541
/**
642
* Lazily load `.env` and `~/.agents/config.json` into `process.env` so the
743
* OpenAgentPack SDK can resolve provider credentials (e.g. DASHSCOPE_API_KEY,
8-
* BAILIAN_WORKSPACE_ID). Safe to call repeatedly — only the first call does I/O.
44+
* BAILIAN_WORKSPACE_ID), then bridge bl's own config as a fallback. Safe to call
45+
* repeatedly — only the first call does I/O.
46+
*
47+
* Effective precedence for bailian provider fields:
48+
* ~/.agents/config.json > shell env > .env > ~/.bailian/config.json
949
*
1050
* NOTE: agent commands declare `auth: "none"` and let the SDK own credential
11-
* resolution. This is the documented tradeoff of the wholesale SDK integration;
12-
* bl's own `--api-key` / `bl auth login` flow is bridged separately as a follow-up.
51+
* resolution. The bl-config bridge is a best-effort fallback; it never overrides
52+
* a value the SDK bootstrap already resolved.
1353
*/
1454
export function ensureCredentials(): void {
1555
if (bootstrapped) return;
1656
bootstrapped = true;
1757
bootstrapRuntimeCredentialsSync();
58+
bridgeBailianCredentials();
1859
}

packages/commands/src/commands/agent/_engine/errors.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,56 @@
11
import { UserError } from "@openagentpack/sdk";
2-
import { BailianError, ExitCode } from "bailian-cli-core";
2+
import { type ApiErrorBody, BailianError, ExitCode, mapApiError } from "bailian-cli-core";
3+
4+
/**
5+
* Structural shape of the SDK's `ApiError` (thrown by provider clients on HTTP
6+
* 4xx/5xx). Matched on fields instead of `instanceof` because the installed SDK
7+
* version does not export the class yet, and structural matching keeps this
8+
* check stable across SDK versions either way.
9+
*/
10+
interface SdkApiErrorLike extends Error {
11+
statusCode: number;
12+
responseBody: string;
13+
}
14+
15+
function isSdkApiError(error: Error): error is SdkApiErrorLike {
16+
const candidate = error as Partial<SdkApiErrorLike>;
17+
return typeof candidate.statusCode === "number" && typeof candidate.responseBody === "string";
18+
}
19+
20+
/**
21+
* The SDK embeds the raw response body in its error message; recover the
22+
* structured fields (message / code / request_id) when the body is JSON so
23+
* `mapApiError` surfaces a clean server message plus api metadata. Non-JSON
24+
* bodies pass through verbatim as the message.
25+
*/
26+
function parseSdkResponseBody(raw: string): ApiErrorBody {
27+
try {
28+
const parsed: unknown = JSON.parse(raw);
29+
if (parsed && typeof parsed === "object") return parsed as ApiErrorBody;
30+
} catch {
31+
/* non-JSON body */
32+
}
33+
return { message: raw.trim() || undefined };
34+
}
335

436
/**
537
* Run an SDK-backed operation, translating SDK error types into BailianError so
638
* bl's error handler produces the right exit code and hint formatting.
7-
* SDK `UserError` → USAGE/GENERAL; any other Error → GENERAL (message passed
8-
* through, per bl's "don't translate server errors" boundary).
39+
* SDK `UserError` → USAGE; SDK `ApiError` (server HTTP error) → GENERAL via
40+
* `mapApiError` (server message passed through verbatim, with
41+
* httpStatus/apiCode/requestId metadata for --output json); any other Error →
42+
* GENERAL (message passed through, per bl's "don't translate server errors"
43+
* boundary).
944
*/
1045
export async function withAgentErrors<T>(fn: () => Promise<T>): Promise<T> {
1146
try {
1247
return await fn();
1348
} catch (error) {
1449
if (error instanceof BailianError) throw error;
1550
if (error instanceof UserError) throw new BailianError(error.message, ExitCode.USAGE);
51+
if (error instanceof Error && isSdkApiError(error)) {
52+
throw mapApiError(error.statusCode, parseSdkResponseBody(error.responseBody));
53+
}
1654
if (error instanceof Error) throw new BailianError(error.message, ExitCode.GENERAL);
1755
throw error;
1856
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as sdk from "@openagentpack/sdk";
2+
import {
3+
createInstrumentedFetch,
4+
type FetchImplementation,
5+
type Identity,
6+
type Settings,
7+
} from "bailian-cli-core";
8+
9+
/** The slice of CommandContext the transport wrapper needs (UA identity + verbose). */
10+
export interface HostContext {
11+
identity: Identity;
12+
settings: Settings;
13+
}
14+
15+
let installed = false;
16+
17+
/**
18+
* Route the SDK's provider-client requests through the CLI's instrumented
19+
* fetch (UA, host-gated tracking headers, --verbose logging). Feature-detected:
20+
* `setDefaultFetch` landed after @openagentpack/sdk 0.1.0 — on older versions
21+
* this is a silent no-op and the SDK keeps using the global fetch as before.
22+
*/
23+
export function installSdkTransport(host: HostContext): void {
24+
if (installed) return;
25+
const setDefaultFetch = (sdk as Record<string, unknown>).setDefaultFetch;
26+
if (typeof setDefaultFetch !== "function") return;
27+
(setDefaultFetch as (fetchImpl: FetchImplementation) => void)(createInstrumentedFetch(host));
28+
installed = true;
29+
}

packages/commands/src/commands/agent/apply.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
import { emitBare, emitResult } from "bailian-cli-runtime";
99
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
1010
import { formatResourceLabel } from "./_engine/address-utils.ts";
11-
import { assertProviderConfigured, buildAgentRuntime } from "./_engine/config-loader.ts";
11+
import {
12+
assertProviderConfigured,
13+
buildAgentRuntime,
14+
CREDENTIALS_NOTE,
15+
} from "./_engine/config-loader.ts";
1216
import { withStdoutProtected } from "./_engine/console-capture.ts";
1317
import { withAgentErrors } from "./_engine/errors.ts";
1418
import { renderAgentFeedback } from "./_engine/feedback.ts";
@@ -45,14 +49,15 @@ export default defineCommand({
4549
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
4650
flags: APPLY_FLAGS,
4751
exampleArgs: ["--yes", "--provider bailian --yes"],
52+
notes: CREDENTIALS_NOTE,
4853
async run(ctx) {
4954
const { settings, flags } = ctx;
5055
const format = detectOutputFormat(settings.output);
5156
const file = flags.file ?? "agents.yaml";
5257

5358
const planned = await withAgentErrors(() =>
5459
withStdoutProtected(async () => {
55-
const runtime = await buildAgentRuntime(file);
60+
const runtime = await buildAgentRuntime(ctx, file);
5661
assertProviderConfigured(runtime, flags.provider);
5762
return planProjectContext(runtime, {
5863
provider: flags.provider,

packages/commands/src/commands/agent/destroy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { emitBare, emitResult } from "bailian-cli-runtime";
99
import { destroyPlannedProjectResources, planDestroyProjectContext } from "@openagentpack/sdk";
1010
import { formatResourceLabel } from "./_engine/address-utils.ts";
11-
import { buildAgentRuntime } from "./_engine/config-loader.ts";
11+
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
1212
import { withStdoutProtected } from "./_engine/console-capture.ts";
1313
import { withAgentErrors } from "./_engine/errors.ts";
1414

@@ -34,14 +34,15 @@ export default defineCommand({
3434
usageArgs: "[--file <path>] [--yes] [--cascade]",
3535
flags: DESTROY_FLAGS,
3636
exampleArgs: ["--yes", "--yes --cascade"],
37+
notes: CREDENTIALS_NOTE,
3738
async run(ctx) {
3839
const { settings, flags } = ctx;
3940
const format = detectOutputFormat(settings.output);
4041
const file = flags.file ?? "agents.yaml";
4142

4243
const planned = await withAgentErrors(() =>
4344
withStdoutProtected(async () => {
44-
const runtime = await buildAgentRuntime(file);
45+
const runtime = await buildAgentRuntime(ctx, file);
4546
return planDestroyProjectContext(runtime);
4647
}),
4748
);

packages/commands/src/commands/agent/init.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ agents.state.json
1818
const PROVIDERS = ["bailian", "claude", "qoder", "ark", "all"] as const;
1919

2020
const PROVIDER_BLOCKS: Record<string, string> = {
21-
bailian: ` bailian:\n api_key: \${DASHSCOPE_API_KEY}\n workspace_id: \${BAILIAN_WORKSPACE_ID}`,
21+
bailian: ` bailian:\n # bl auth login sets DASHSCOPE_API_KEY; bl config set workspace_id <id> sets BAILIAN_WORKSPACE_ID\n api_key: \${DASHSCOPE_API_KEY}\n workspace_id: \${BAILIAN_WORKSPACE_ID}`,
2222
claude: ` claude:\n api_key: \${ANTHROPIC_API_KEY}`,
2323
qoder: ` qoder:\n api_key: \${QODER_PAT}\n gateway: "https://api.qoder.com/api/v1/cloud"`,
2424
ark: ` ark:\n api_key: \${ARK_API_KEY}`,
@@ -135,6 +135,11 @@ export default defineCommand({
135135
emitResult({ created: file, provider, agent: agentName }, format);
136136
} else {
137137
emitBare(`Created ${file}`);
138+
if (provider === "bailian" || provider === "all") {
139+
emitBare(
140+
"Credentials: run `bl auth login` and `bl config set workspace_id <id>`, or set DASHSCOPE_API_KEY / BAILIAN_WORKSPACE_ID.",
141+
);
142+
}
138143
emitBare("Next: edit agents.yaml, then run `bl agent plan`.");
139144
}
140145
},

0 commit comments

Comments
 (0)