mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(agent): agent相关cli命令的client层功能,对齐cli client的基础能力
This commit is contained in:
@@ -50,6 +50,10 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore` / `ctx.configStore` 的窄接口操作落盘。
|
||||
|
||||
### 例外:agent 命令的 SDK 凭证桥接
|
||||
|
||||
`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`。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. core 层(类型 + 解析)
|
||||
|
||||
@@ -2,4 +2,7 @@ node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
outputs/
|
||||
outputs/
|
||||
# agents
|
||||
agents.state.json
|
||||
.env
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
version: "1"
|
||||
|
||||
providers:
|
||||
bailian:
|
||||
api_key: ${DASHSCOPE_API_KEY}
|
||||
workspace_id: ${BAILIAN_WORKSPACE_ID}
|
||||
|
||||
defaults:
|
||||
provider: bailian
|
||||
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
type: cloud
|
||||
networking:
|
||||
type: unrestricted
|
||||
|
||||
agents:
|
||||
assistant:
|
||||
description: "General-purpose assistant"
|
||||
model: qwen3.7-max
|
||||
instructions: |
|
||||
You are a helpful assistant.
|
||||
environment: dev
|
||||
tools:
|
||||
builtin: [bash, read, glob, grep]
|
||||
@@ -6,16 +6,27 @@ import {
|
||||
} from "@openagentpack/sdk";
|
||||
import { ensureCredentials } from "./credentials.ts";
|
||||
import { loadFileState } from "./file-state-manager.ts";
|
||||
import { type HostContext, installSdkTransport } from "./transport.ts";
|
||||
|
||||
export { CREDENTIALS_NOTE } from "./credentials.ts";
|
||||
|
||||
/**
|
||||
* Build a full ProjectRuntimeContext from a config file path — the standard
|
||||
* entry point for agent commands that need the SDK engine. Mirrors OpenAgentPack
|
||||
* CLI's buildCliRuntime: resolve config → load local state → assemble runtime.
|
||||
* Takes the host context first so every SDK-engine command wires the
|
||||
* instrumented transport (UA / tracking headers / verbose) by construction.
|
||||
*/
|
||||
export async function buildAgentRuntime(
|
||||
host: HostContext,
|
||||
filePath: string,
|
||||
options: { resolveEnv?: boolean; projectName?: string; statePath?: string } = {},
|
||||
options: {
|
||||
resolveEnv?: boolean;
|
||||
projectName?: string;
|
||||
statePath?: string;
|
||||
} = {},
|
||||
): Promise<ProjectRuntimeContext & { configPath: string }> {
|
||||
installSdkTransport(host);
|
||||
ensureCredentials();
|
||||
const { config, configPath, projectName } = await resolveProjectConfig(filePath, options);
|
||||
const state = await loadFileState(configPath, options.statePath, projectName);
|
||||
|
||||
@@ -1,18 +1,59 @@
|
||||
import { bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk";
|
||||
import { readConfigFile } from "bailian-cli-core";
|
||||
|
||||
let bootstrapped = false;
|
||||
|
||||
/**
|
||||
* Shared `--help` note documenting where agent commands get provider
|
||||
* credentials. Mirrors the credential-source hint bl's native commands surface
|
||||
* (knowledge / usage / token-plan), adapted for the SDK's env-based resolution
|
||||
* and the {@link bridgeBailianCredentials} fallback. Attach to every command
|
||||
* that loads agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
|
||||
*/
|
||||
export const CREDENTIALS_NOTE = [
|
||||
"Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).",
|
||||
"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>`.",
|
||||
];
|
||||
|
||||
/**
|
||||
* Bridge bl's own login state into the env vars the OpenAgentPack SDK reads for
|
||||
* the bailian provider. bl persists `api_key` / `workspace_id` in
|
||||
* `~/.bailian/config.json` (via `bl auth login` / `bl config set`); mirror them
|
||||
* onto `DASHSCOPE_API_KEY` / `BAILIAN_WORKSPACE_ID` so users don't have to
|
||||
* re-declare the same credentials for `bl agent *`.
|
||||
*
|
||||
* Lowest priority: only fills a var that is still unset, so anything already in
|
||||
* the environment (shell export, `.env`, or `~/.agents/config.json` — which the
|
||||
* SDK bootstrap has already applied) wins. Missing values are left alone; the
|
||||
* SDK surfaces its own error when interpolation can't resolve, and non-bailian
|
||||
* providers (claude/qoder/ark) don't need DashScope credentials at all.
|
||||
*/
|
||||
export function bridgeBailianCredentials(): void {
|
||||
const file = readConfigFile();
|
||||
if (!process.env.DASHSCOPE_API_KEY?.trim() && file.api_key) {
|
||||
process.env.DASHSCOPE_API_KEY = file.api_key;
|
||||
}
|
||||
if (!process.env.BAILIAN_WORKSPACE_ID?.trim() && file.workspace_id) {
|
||||
process.env.BAILIAN_WORKSPACE_ID = file.workspace_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily load `.env` and `~/.agents/config.json` into `process.env` so the
|
||||
* OpenAgentPack SDK can resolve provider credentials (e.g. DASHSCOPE_API_KEY,
|
||||
* BAILIAN_WORKSPACE_ID). Safe to call repeatedly — only the first call does I/O.
|
||||
* BAILIAN_WORKSPACE_ID), then bridge bl's own config as a fallback. Safe to call
|
||||
* repeatedly — only the first call does I/O.
|
||||
*
|
||||
* Effective precedence for bailian provider fields:
|
||||
* ~/.agents/config.json > shell env > .env > ~/.bailian/config.json
|
||||
*
|
||||
* NOTE: agent commands declare `auth: "none"` and let the SDK own credential
|
||||
* resolution. This is the documented tradeoff of the wholesale SDK integration;
|
||||
* bl's own `--api-key` / `bl auth login` flow is bridged separately as a follow-up.
|
||||
* resolution. The bl-config bridge is a best-effort fallback; it never overrides
|
||||
* a value the SDK bootstrap already resolved.
|
||||
*/
|
||||
export function ensureCredentials(): void {
|
||||
if (bootstrapped) return;
|
||||
bootstrapped = true;
|
||||
bootstrapRuntimeCredentialsSync();
|
||||
bridgeBailianCredentials();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
import { UserError } from "@openagentpack/sdk";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { type ApiErrorBody, BailianError, ExitCode, mapApiError } from "bailian-cli-core";
|
||||
|
||||
/**
|
||||
* Structural shape of the SDK's `ApiError` (thrown by provider clients on HTTP
|
||||
* 4xx/5xx). Matched on fields instead of `instanceof` because the installed SDK
|
||||
* version does not export the class yet, and structural matching keeps this
|
||||
* check stable across SDK versions either way.
|
||||
*/
|
||||
interface SdkApiErrorLike extends Error {
|
||||
statusCode: number;
|
||||
responseBody: string;
|
||||
}
|
||||
|
||||
function isSdkApiError(error: Error): error is SdkApiErrorLike {
|
||||
const candidate = error as Partial<SdkApiErrorLike>;
|
||||
return typeof candidate.statusCode === "number" && typeof candidate.responseBody === "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* The SDK embeds the raw response body in its error message; recover the
|
||||
* structured fields (message / code / request_id) when the body is JSON so
|
||||
* `mapApiError` surfaces a clean server message plus api metadata. Non-JSON
|
||||
* bodies pass through verbatim as the message.
|
||||
*/
|
||||
function parseSdkResponseBody(raw: string): ApiErrorBody {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") return parsed as ApiErrorBody;
|
||||
} catch {
|
||||
/* non-JSON body */
|
||||
}
|
||||
return { message: raw.trim() || undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an SDK-backed operation, translating SDK error types into BailianError so
|
||||
* bl's error handler produces the right exit code and hint formatting.
|
||||
* SDK `UserError` → USAGE/GENERAL; any other Error → GENERAL (message passed
|
||||
* through, per bl's "don't translate server errors" boundary).
|
||||
* SDK `UserError` → USAGE; SDK `ApiError` (server HTTP error) → GENERAL via
|
||||
* `mapApiError` (server message passed through verbatim, with
|
||||
* httpStatus/apiCode/requestId metadata for --output json); any other Error →
|
||||
* GENERAL (message passed through, per bl's "don't translate server errors"
|
||||
* boundary).
|
||||
*/
|
||||
export async function withAgentErrors<T>(fn: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
@@ -13,6 +48,9 @@ export async function withAgentErrors<T>(fn: () => Promise<T>): Promise<T> {
|
||||
} catch (error) {
|
||||
if (error instanceof BailianError) throw error;
|
||||
if (error instanceof UserError) throw new BailianError(error.message, ExitCode.USAGE);
|
||||
if (error instanceof Error && isSdkApiError(error)) {
|
||||
throw mapApiError(error.statusCode, parseSdkResponseBody(error.responseBody));
|
||||
}
|
||||
if (error instanceof Error) throw new BailianError(error.message, ExitCode.GENERAL);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as sdk from "@openagentpack/sdk";
|
||||
import {
|
||||
createInstrumentedFetch,
|
||||
type FetchImplementation,
|
||||
type Identity,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
/** The slice of CommandContext the transport wrapper needs (UA identity + verbose). */
|
||||
export interface HostContext {
|
||||
identity: Identity;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
let installed = false;
|
||||
|
||||
/**
|
||||
* Route the SDK's provider-client requests through the CLI's instrumented
|
||||
* fetch (UA, host-gated tracking headers, --verbose logging). Feature-detected:
|
||||
* `setDefaultFetch` landed after @openagentpack/sdk 0.1.0 — on older versions
|
||||
* this is a silent no-op and the SDK keeps using the global fetch as before.
|
||||
*/
|
||||
export function installSdkTransport(host: HostContext): void {
|
||||
if (installed) return;
|
||||
const setDefaultFetch = (sdk as Record<string, unknown>).setDefaultFetch;
|
||||
if (typeof setDefaultFetch !== "function") return;
|
||||
(setDefaultFetch as (fetchImpl: FetchImplementation) => void)(createInstrumentedFetch(host));
|
||||
installed = true;
|
||||
}
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
import { assertProviderConfigured, buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import {
|
||||
assertProviderConfigured,
|
||||
buildAgentRuntime,
|
||||
CREDENTIALS_NOTE,
|
||||
} from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
@@ -45,6 +49,7 @@ export default defineCommand({
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
|
||||
flags: APPLY_FLAGS,
|
||||
exampleArgs: ["--yes", "--provider bailian --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -52,7 +57,7 @@ export default defineCommand({
|
||||
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
assertProviderConfigured(runtime, flags.provider);
|
||||
return planProjectContext(runtime, {
|
||||
provider: flags.provider,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { destroyPlannedProjectResources, planDestroyProjectContext } from "@openagentpack/sdk";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -34,6 +34,7 @@ export default defineCommand({
|
||||
usageArgs: "[--file <path>] [--yes] [--cascade]",
|
||||
flags: DESTROY_FLAGS,
|
||||
exampleArgs: ["--yes", "--yes --cascade"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -41,7 +42,7 @@ export default defineCommand({
|
||||
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return planDestroyProjectContext(runtime);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ agents.state.json
|
||||
const PROVIDERS = ["bailian", "claude", "qoder", "ark", "all"] as const;
|
||||
|
||||
const PROVIDER_BLOCKS: Record<string, string> = {
|
||||
bailian: ` bailian:\n api_key: \${DASHSCOPE_API_KEY}\n workspace_id: \${BAILIAN_WORKSPACE_ID}`,
|
||||
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}`,
|
||||
claude: ` claude:\n api_key: \${ANTHROPIC_API_KEY}`,
|
||||
qoder: ` qoder:\n api_key: \${QODER_PAT}\n gateway: "https://api.qoder.com/api/v1/cloud"`,
|
||||
ark: ` ark:\n api_key: \${ARK_API_KEY}`,
|
||||
@@ -135,6 +135,11 @@ export default defineCommand({
|
||||
emitResult({ created: file, provider, agent: agentName }, format);
|
||||
} else {
|
||||
emitBare(`Created ${file}`);
|
||||
if (provider === "bailian" || provider === "all") {
|
||||
emitBare(
|
||||
"Credentials: run `bl auth login` and `bl config set workspace_id <id>`, or set DASHSCOPE_API_KEY / BAILIAN_WORKSPACE_ID.",
|
||||
);
|
||||
}
|
||||
emitBare("Next: edit agents.yaml, then run `bl agent plan`.");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { planProjectContext } from "@openagentpack/sdk";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
import { assertProviderConfigured, buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import {
|
||||
assertProviderConfigured,
|
||||
buildAgentRuntime,
|
||||
CREDENTIALS_NOTE,
|
||||
} from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
@@ -40,6 +44,7 @@ export default defineCommand({
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
|
||||
flags: PLAN_FLAGS,
|
||||
exampleArgs: ["", "--provider bailian", "--no-refresh"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -47,7 +52,7 @@ export default defineCommand({
|
||||
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
assertProviderConfigured(runtime, flags.provider);
|
||||
return planProjectContext(runtime, {
|
||||
provider: flags.provider,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { createSessionForAgent } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { parseMemoryStores } from "./_engine/session-render.ts";
|
||||
@@ -22,7 +22,11 @@ const SESSION_CREATE_FLAGS = {
|
||||
valueHint: "<name>",
|
||||
description: "Override agent's declared environment",
|
||||
},
|
||||
vault: { type: "string", valueHint: "<name>", description: "Override agent's declared vault" },
|
||||
vault: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Override agent's declared vault",
|
||||
},
|
||||
memoryStores: {
|
||||
type: "string",
|
||||
valueHint: "<names>",
|
||||
@@ -42,6 +46,7 @@ export default defineCommand({
|
||||
usageArgs: "[--agent <name>] [--environment <name>] [--title <title>] [--file <path>]",
|
||||
flags: SESSION_CREATE_FLAGS,
|
||||
exampleArgs: ["", "--agent assistant", "--agent assistant --title 'debug run'"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -49,7 +54,7 @@ export default defineCommand({
|
||||
|
||||
const run = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return createSessionForAgent(runtime, {
|
||||
agent: flags.agent,
|
||||
provider: flags.provider,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { deleteSession } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -17,7 +17,11 @@ const SESSION_DELETE_FLAGS = {
|
||||
valueHint: "<path>",
|
||||
description: "Config file path (default: agents.yaml)",
|
||||
},
|
||||
provider: { type: "string", valueHint: "<name>", description: "Target provider" },
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Target provider",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -26,6 +30,7 @@ export default defineCommand({
|
||||
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
|
||||
flags: SESSION_DELETE_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -33,7 +38,7 @@ export default defineCommand({
|
||||
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
await deleteSession(runtime, flags.sessionId, flags.provider);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-co
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import { listSessionEvents } from "@openagentpack/sdk";
|
||||
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { fetchAllPages } from "./_engine/pagination.ts";
|
||||
@@ -19,9 +19,20 @@ const SESSION_EVENTS_FLAGS = {
|
||||
valueHint: "<path>",
|
||||
description: "Config file path (default: agents.yaml)",
|
||||
},
|
||||
provider: { type: "string", valueHint: "<name>", description: "Target provider" },
|
||||
limit: { type: "number", valueHint: "<n>", description: "Maximum number of events to fetch" },
|
||||
all: { type: "switch", description: "Fetch all pages by following the cursor" },
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Target provider",
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Maximum number of events to fetch",
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description: "Fetch all pages by following the cursor",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -30,6 +41,7 @@ export default defineCommand({
|
||||
usageArgs: "--session-id <id> [--limit <n>] [--all] [--file <path>]",
|
||||
flags: SESSION_EVENTS_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123", "--session-id sess_abc123 --all"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -37,14 +49,18 @@ export default defineCommand({
|
||||
|
||||
const { items: events, hasMore } = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return fetchAllPages(async (page) => {
|
||||
const result = await listSessionEvents(runtime, flags.sessionId, {
|
||||
provider: flags.provider,
|
||||
limit: flags.limit,
|
||||
page_token: page,
|
||||
});
|
||||
return { items: result.events, hasMore: result.has_more, nextPage: result.next_page };
|
||||
return {
|
||||
items: result.events,
|
||||
hasMore: result.has_more,
|
||||
nextPage: result.next_page,
|
||||
};
|
||||
}, flags.all);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { getSession } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -17,7 +17,11 @@ const SESSION_GET_FLAGS = {
|
||||
valueHint: "<path>",
|
||||
description: "Config file path (default: agents.yaml)",
|
||||
},
|
||||
provider: { type: "string", valueHint: "<name>", description: "Target provider" },
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Target provider",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -26,6 +30,7 @@ export default defineCommand({
|
||||
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
|
||||
flags: SESSION_GET_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -33,7 +38,7 @@ export default defineCommand({
|
||||
|
||||
const session = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return getSession(runtime, flags.sessionId, flags.provider);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import { listSessionSummaries } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { fetchAllPages } from "./_engine/pagination.ts";
|
||||
@@ -12,9 +12,20 @@ const SESSION_LIST_FLAGS = {
|
||||
valueHint: "<path>",
|
||||
description: "Config file path (default: agents.yaml)",
|
||||
},
|
||||
agent: { type: "string", valueHint: "<name>", description: "Filter by agent name" },
|
||||
all: { type: "switch", description: "Fetch all pages by following the cursor" },
|
||||
provider: { type: "string", valueHint: "<name>", description: "Target provider" },
|
||||
agent: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Filter by agent name",
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description: "Fetch all pages by following the cursor",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Target provider",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -23,6 +34,7 @@ export default defineCommand({
|
||||
usageArgs: "[--agent <name>] [--all] [--provider <name>] [--file <path>]",
|
||||
flags: SESSION_LIST_FLAGS,
|
||||
exampleArgs: ["", "--agent assistant", "--all"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -30,14 +42,18 @@ export default defineCommand({
|
||||
|
||||
const { items: summaries, hasMore } = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return fetchAllPages(async (page) => {
|
||||
const result = await listSessionSummaries(runtime, {
|
||||
agent: flags.agent,
|
||||
provider: flags.provider,
|
||||
filter: page ? { page } : undefined,
|
||||
});
|
||||
return { items: result.summaries, hasMore: result.hasMore, nextPage: result.nextPage };
|
||||
return {
|
||||
items: result.summaries,
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
};
|
||||
}, flags.all);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { startSessionRun, startSessionRunPolling } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import {
|
||||
@@ -31,15 +31,26 @@ const SESSION_RUN_FLAGS = {
|
||||
valueHint: "<name>",
|
||||
description: "Override agent's declared environment",
|
||||
},
|
||||
vault: { type: "string", valueHint: "<name>", description: "Override agent's declared vault" },
|
||||
vault: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Override agent's declared vault",
|
||||
},
|
||||
memoryStores: {
|
||||
type: "string",
|
||||
valueHint: "<names>",
|
||||
description: "Override agent's memory stores (comma-separated)",
|
||||
},
|
||||
title: { type: "string", valueHint: "<title>", description: "Session title" },
|
||||
provider: { type: "string", valueHint: "<name>", description: "Target provider" },
|
||||
noStream: { type: "switch", description: "Use polling instead of SSE streaming" },
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Target provider",
|
||||
},
|
||||
noStream: {
|
||||
type: "switch",
|
||||
description: "Use polling instead of SSE streaming",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -48,6 +59,7 @@ export default defineCommand({
|
||||
usageArgs: "--prompt <text> [--agent <name>] [--no-stream] [--file <path>]",
|
||||
flags: SESSION_RUN_FLAGS,
|
||||
exampleArgs: ['--prompt "hello"', '--agent assistant --prompt "summarize this repo"'],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -65,7 +77,7 @@ export default defineCommand({
|
||||
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
if (flags.noStream) {
|
||||
const run = await startSessionRunPolling(runtime, flags.prompt, runOptions);
|
||||
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { sendSessionMessagePolling, sendSessionMessageStreaming } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderCollectedEvents, streamAndRenderEvents } from "./_engine/session-render.ts";
|
||||
@@ -23,8 +23,15 @@ const SESSION_SEND_FLAGS = {
|
||||
valueHint: "<path>",
|
||||
description: "Config file path (default: agents.yaml)",
|
||||
},
|
||||
provider: { type: "string", valueHint: "<name>", description: "Target provider" },
|
||||
noStream: { type: "switch", description: "Use polling instead of SSE streaming" },
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Target provider",
|
||||
},
|
||||
noStream: {
|
||||
type: "switch",
|
||||
description: "Use polling instead of SSE streaming",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -33,6 +40,7 @@ export default defineCommand({
|
||||
usageArgs: "--session-id <id> --message <text> [--no-stream] [--file <path>]",
|
||||
flags: SESSION_SEND_FLAGS,
|
||||
exampleArgs: ['--session-id sess_abc123 --message "continue"'],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -41,7 +49,7 @@ export default defineCommand({
|
||||
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
if (flags.noStream) {
|
||||
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
|
||||
provider: flags.provider,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { importResource, parseStateAddress } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -37,6 +37,7 @@ export default defineCommand({
|
||||
"--address <provider.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
|
||||
flags: STATE_IMPORT_FLAGS,
|
||||
exampleArgs: ["--address bailian.agent.assistant --remote-id agent-abc123"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -44,8 +45,10 @@ export default defineCommand({
|
||||
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const parsed = parseStateAddress(flags.address, { requireProvider: true });
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
const parsed = parseStateAddress(flags.address, {
|
||||
requireProvider: true,
|
||||
});
|
||||
await importResource(runtime, parsed, flags.remoteId, {
|
||||
resourceVersion: flags.resourceVersion,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -18,6 +18,7 @@ export default defineCommand({
|
||||
usageArgs: "[--file <path>]",
|
||||
flags: STATE_LIST_FLAGS,
|
||||
exampleArgs: ["", "--file agents.yaml"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -25,7 +26,7 @@ export default defineCommand({
|
||||
|
||||
const resources = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return runtime.state.listResources();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { parseStateAddress } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -31,6 +31,7 @@ export default defineCommand({
|
||||
usageArgs: "--address <provider.type.name> [--file <path>]",
|
||||
flags: STATE_RM_FLAGS,
|
||||
exampleArgs: ["--address bailian.agent.assistant"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -38,8 +39,10 @@ export default defineCommand({
|
||||
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const parsed = parseStateAddress(flags.address, { requireProvider: false });
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
const parsed = parseStateAddress(flags.address, {
|
||||
requireProvider: false,
|
||||
});
|
||||
const found = runtime.state.findResource(parsed);
|
||||
if (!found) {
|
||||
throw new BailianError(`Resource not found: ${flags.address}`, ExitCode.GENERAL);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { parseStateAddress } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime } from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
@@ -31,6 +31,7 @@ export default defineCommand({
|
||||
usageArgs: "--address <provider.type.name> [--file <path>]",
|
||||
flags: STATE_SHOW_FLAGS,
|
||||
exampleArgs: ["--address bailian.agent.assistant"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -38,8 +39,10 @@ export default defineCommand({
|
||||
|
||||
const found = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(file);
|
||||
const parsed = parseStateAddress(flags.address, { requireProvider: false });
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
const parsed = parseStateAddress(flags.address, {
|
||||
requireProvider: false,
|
||||
});
|
||||
return runtime.state.findResource(parsed);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { resolveProjectConfig, validateProjectConfig } from "@openagentpack/sdk";
|
||||
import { ensureCredentials } from "./_engine/credentials.ts";
|
||||
import { CREDENTIALS_NOTE, ensureCredentials } from "./_engine/credentials.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
const VALIDATE_FLAGS = {
|
||||
@@ -24,6 +24,7 @@ export default defineCommand({
|
||||
usageArgs: "[--file <path>]",
|
||||
flags: VALIDATE_FLAGS,
|
||||
exampleArgs: ["", "--file agents.yaml"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { UserError } from "@openagentpack/sdk";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { withAgentErrors } from "../src/commands/agent/_engine/errors.ts";
|
||||
|
||||
/**
|
||||
* Structural stand-in for the SDK's internal `ApiError` (not exported by the
|
||||
* installed SDK version): withAgentErrors matches on statusCode/responseBody
|
||||
* fields, so any Error carrying them must map through mapApiError.
|
||||
*/
|
||||
class FakeSdkApiError extends Error {
|
||||
constructor(
|
||||
readonly statusCode: number,
|
||||
readonly responseBody: string,
|
||||
) {
|
||||
super(`Bailian API ${statusCode}: ${responseBody}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function catchMapped(error: unknown): Promise<BailianError> {
|
||||
try {
|
||||
await withAgentErrors(() => Promise.reject(error));
|
||||
} catch (mapped) {
|
||||
expect(mapped).toBeInstanceOf(BailianError);
|
||||
return mapped as BailianError;
|
||||
}
|
||||
throw new Error("expected withAgentErrors to throw");
|
||||
}
|
||||
|
||||
test("BailianError passes through untouched", async () => {
|
||||
const original = new BailianError("already mapped", ExitCode.AUTH);
|
||||
const mapped = await catchMapped(original);
|
||||
expect(mapped).toBe(original);
|
||||
});
|
||||
|
||||
test("SDK UserError maps to USAGE", async () => {
|
||||
const mapped = await catchMapped(new UserError("bad agents.yaml"));
|
||||
expect(mapped.exitCode).toBe(ExitCode.USAGE);
|
||||
expect(mapped.message).toBe("bad agents.yaml");
|
||||
});
|
||||
|
||||
test("SDK ApiError with DashScope-style JSON body surfaces clean message and api metadata", async () => {
|
||||
const body = JSON.stringify({
|
||||
code: "InvalidParameter",
|
||||
message: "agent name already exists",
|
||||
request_id: "req-123",
|
||||
});
|
||||
const mapped = await catchMapped(new FakeSdkApiError(400, body));
|
||||
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(mapped.message).toBe("agent name already exists");
|
||||
expect(mapped.api).toEqual({
|
||||
httpStatus: 400,
|
||||
apiCode: "InvalidParameter",
|
||||
requestId: "req-123",
|
||||
});
|
||||
});
|
||||
|
||||
test("SDK ApiError with OpenAI-style error envelope extracts message and type", async () => {
|
||||
const body = JSON.stringify({
|
||||
error: { message: "model does not exist", type: "invalid_request_error" },
|
||||
request_id: "req-456",
|
||||
});
|
||||
const mapped = await catchMapped(new FakeSdkApiError(404, body));
|
||||
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(mapped.message).toBe("model does not exist");
|
||||
expect(mapped.api?.apiCode).toBe("invalid_request_error");
|
||||
expect(mapped.api?.requestId).toBe("req-456");
|
||||
});
|
||||
|
||||
test("SDK ApiError with non-JSON body passes raw text through as message", async () => {
|
||||
const mapped = await catchMapped(new FakeSdkApiError(502, "Bad Gateway"));
|
||||
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(mapped.message).toBe("Bad Gateway");
|
||||
expect(mapped.api?.httpStatus).toBe(502);
|
||||
});
|
||||
|
||||
test("SDK ApiError with empty body falls back to HTTP status message", async () => {
|
||||
const mapped = await catchMapped(new FakeSdkApiError(503, ""));
|
||||
expect(mapped.message).toBe("HTTP 503");
|
||||
expect(mapped.api?.httpStatus).toBe(503);
|
||||
});
|
||||
|
||||
test("plain Error maps to GENERAL with message passed through", async () => {
|
||||
const mapped = await catchMapped(new Error("boom"));
|
||||
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(mapped.message).toBe("boom");
|
||||
expect(mapped.api).toBeUndefined();
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { bridgeBailianCredentials } from "../src/commands/agent/_engine/credentials.ts";
|
||||
|
||||
/**
|
||||
* bridgeBailianCredentials 把 ~/.bailian/config.json 的 api_key / workspace_id
|
||||
* 作为最低优先级兜底填入 DASHSCOPE_API_KEY / BAILIAN_WORKSPACE_ID。
|
||||
* 用临时 config dir + env 保存恢复隔离,验证优先级与不抛错语义。
|
||||
*/
|
||||
async function inScenario(
|
||||
scenario: {
|
||||
config?: { api_key?: string; workspace_id?: string };
|
||||
env?: { DASHSCOPE_API_KEY?: string; BAILIAN_WORKSPACE_ID?: string };
|
||||
},
|
||||
assert: () => void,
|
||||
): Promise<void> {
|
||||
const savedConfigDir = process.env.BAILIAN_CONFIG_DIR;
|
||||
const savedApiKey = process.env.DASHSCOPE_API_KEY;
|
||||
const savedWorkspace = process.env.BAILIAN_WORKSPACE_ID;
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "bl-cred-bridge-"));
|
||||
process.env.BAILIAN_CONFIG_DIR = dir;
|
||||
if (scenario.config) {
|
||||
writeFileSync(join(dir, "config.json"), JSON.stringify(scenario.config), "utf-8");
|
||||
}
|
||||
|
||||
// 显式设置/清除 env,避免继承宿主环境干扰断言
|
||||
if (scenario.env?.DASHSCOPE_API_KEY === undefined) delete process.env.DASHSCOPE_API_KEY;
|
||||
else process.env.DASHSCOPE_API_KEY = scenario.env.DASHSCOPE_API_KEY;
|
||||
if (scenario.env?.BAILIAN_WORKSPACE_ID === undefined) delete process.env.BAILIAN_WORKSPACE_ID;
|
||||
else process.env.BAILIAN_WORKSPACE_ID = scenario.env.BAILIAN_WORKSPACE_ID;
|
||||
|
||||
try {
|
||||
assert();
|
||||
} finally {
|
||||
restore("BAILIAN_CONFIG_DIR", savedConfigDir);
|
||||
restore("DASHSCOPE_API_KEY", savedApiKey);
|
||||
restore("BAILIAN_WORKSPACE_ID", savedWorkspace);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function restore(key: string, value: string | undefined): void {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
|
||||
test("bridge:env 已有值时不被 bl config 覆盖(最低优先级)", async () => {
|
||||
await inScenario(
|
||||
{
|
||||
config: { api_key: "sk-from-config", workspace_id: "ws-from-config" },
|
||||
env: { DASHSCOPE_API_KEY: "sk-from-env", BAILIAN_WORKSPACE_ID: "ws-from-env" },
|
||||
},
|
||||
() => {
|
||||
bridgeBailianCredentials();
|
||||
expect(process.env.DASHSCOPE_API_KEY).toBe("sk-from-env");
|
||||
expect(process.env.BAILIAN_WORKSPACE_ID).toBe("ws-from-env");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("bridge:env 缺失且 bl config 有值时填充", async () => {
|
||||
await inScenario(
|
||||
{ config: { api_key: "sk-from-config", workspace_id: "ws-from-config" }, env: {} },
|
||||
() => {
|
||||
bridgeBailianCredentials();
|
||||
expect(process.env.DASHSCOPE_API_KEY).toBe("sk-from-config");
|
||||
expect(process.env.BAILIAN_WORKSPACE_ID).toBe("ws-from-config");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("bridge:仅缺失项被填,已有项保留(逐字段独立)", async () => {
|
||||
await inScenario(
|
||||
{
|
||||
config: { api_key: "sk-from-config", workspace_id: "ws-from-config" },
|
||||
env: { DASHSCOPE_API_KEY: "sk-from-env" },
|
||||
},
|
||||
() => {
|
||||
bridgeBailianCredentials();
|
||||
expect(process.env.DASHSCOPE_API_KEY).toBe("sk-from-env");
|
||||
expect(process.env.BAILIAN_WORKSPACE_ID).toBe("ws-from-config");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("bridge:env 与 bl config 皆缺失时不抛错且不写入", async () => {
|
||||
await inScenario({ env: {} }, () => {
|
||||
expect(() => bridgeBailianCredentials()).not.toThrow();
|
||||
expect(process.env.DASHSCOPE_API_KEY).toBeUndefined();
|
||||
expect(process.env.BAILIAN_WORKSPACE_ID).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -19,8 +19,9 @@ export {
|
||||
videoGeneratePath,
|
||||
} from "./endpoints.ts";
|
||||
export { CHANNEL, SOURCE_CONFIG, TAGS, trackingHeaders } from "./headers.ts";
|
||||
export type { RequestOpts } from "./http.ts";
|
||||
export type { HttpDeps, RequestOpts } from "./http.ts";
|
||||
export { request, requestJson } from "./http.ts";
|
||||
export { createInstrumentedFetch, type FetchImplementation } from "./instrumented-fetch.ts";
|
||||
export { Client, type ClientRequestOpts, type ClientOpenApiQueryOpts } from "./client.ts";
|
||||
export {
|
||||
buildAcsCanonicalQuery,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { maskToken } from "../utils/token.ts";
|
||||
import type { HttpDeps } from "./http.ts";
|
||||
import { trackingHeaders } from "./headers.ts";
|
||||
|
||||
/**
|
||||
* fetch-compatible signature, structurally identical to the SDK-side `FetchLike`
|
||||
* seam. Declared locally so core stays free of SDK imports.
|
||||
*/
|
||||
export type FetchImplementation = (
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response>;
|
||||
|
||||
/**
|
||||
* Tracking headers are DashScope-specific: only attach them to Alibaba Cloud
|
||||
* hosts, never to third-party providers (Anthropic / Ark / Qoder) that an
|
||||
* embedded SDK may also call through this fetch.
|
||||
*/
|
||||
function isAlibabaCloudHost(url: string): boolean {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname === "aliyuncs.com" || hostname.endsWith(".aliyuncs.com");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function requestUrl(input: string | URL | Request): string {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.href;
|
||||
return input.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* A transparent fetch wrapper carrying the client-layer cross-cutting request
|
||||
* concerns (UA, tracking headers, `--verbose` logging) for network stacks that
|
||||
* bypass {@link request} — e.g. an embedded SDK's provider clients. Deliberately
|
||||
* transport-only: no auth injection, no baseUrl handling, no timeout, and no
|
||||
* error mapping, so the caller's response semantics (status handling, SSE,
|
||||
* conflict detection) stay intact.
|
||||
*/
|
||||
export function createInstrumentedFetch(deps: HttpDeps): FetchImplementation {
|
||||
return async (input, init = {}) => {
|
||||
const url = requestUrl(input);
|
||||
const headers = new Headers(
|
||||
init.headers ?? (input instanceof Request ? input.headers : undefined),
|
||||
);
|
||||
|
||||
if (!headers.has("user-agent")) {
|
||||
headers.set("User-Agent", `${deps.identity.clientName}/${deps.identity.version}`);
|
||||
}
|
||||
if (isAlibabaCloudHost(url)) {
|
||||
for (const [name, value] of Object.entries(trackingHeaders())) {
|
||||
headers.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (deps.settings.verbose) {
|
||||
console.error(`> ${init.method ?? "GET"} ${url}`);
|
||||
const auth = headers.get("authorization");
|
||||
if (auth) console.error(`> Auth: ${maskToken(auth.replace(/^Bearer /, ""))}`);
|
||||
}
|
||||
|
||||
const res = await fetch(input, { ...init, headers });
|
||||
|
||||
if (deps.settings.verbose) {
|
||||
console.error(`< ${res.status} ${res.statusText}`);
|
||||
const reqId = res.headers.get("x-request-id");
|
||||
if (reqId) {
|
||||
console.error(`request_id: ${reqId}`);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import type { Identity, Settings } from "../src/index.ts";
|
||||
import { createInstrumentedFetch, SOURCE_CONFIG } from "../src/index.ts";
|
||||
|
||||
const identity: Identity = {
|
||||
binName: "bl",
|
||||
clientName: "bailian-cli",
|
||||
version: "1.2.3",
|
||||
npmPackage: "bailian-cli",
|
||||
};
|
||||
|
||||
const settings: Settings = { timeout: 60, verbose: false } as Settings;
|
||||
|
||||
interface CapturedRequest {
|
||||
url: string;
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
/** Run the wrapper against a stubbed globalThis.fetch and capture what reaches it. */
|
||||
async function capture(
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit,
|
||||
): Promise<CapturedRequest> {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured: CapturedRequest | undefined;
|
||||
globalThis.fetch = (async (fetchInput: string | URL | Request, fetchInit?: RequestInit) => {
|
||||
captured = {
|
||||
url:
|
||||
typeof fetchInput === "string"
|
||||
? fetchInput
|
||||
: fetchInput instanceof URL
|
||||
? fetchInput.href
|
||||
: fetchInput.url,
|
||||
headers: new Headers(fetchInit?.headers),
|
||||
};
|
||||
return new Response("{}", { status: 200 });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
await createInstrumentedFetch({ identity, settings })(input, init);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
if (!captured) throw new Error("stubbed fetch was not called");
|
||||
return captured;
|
||||
}
|
||||
|
||||
test("adds UA and tracking header on Alibaba Cloud hosts", async () => {
|
||||
const { headers } = await capture(
|
||||
"https://ws-1.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/agents",
|
||||
{ method: "POST", headers: { Authorization: "Bearer k" } },
|
||||
);
|
||||
expect(headers.get("user-agent")).toBe("bailian-cli/1.2.3");
|
||||
expect(headers.get("x-dashscope-source-config")).toBe(SOURCE_CONFIG);
|
||||
expect(headers.get("authorization")).toBe("Bearer k");
|
||||
});
|
||||
|
||||
test("adds UA but no tracking header on third-party hosts", async () => {
|
||||
const { headers } = await capture("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
});
|
||||
expect(headers.get("user-agent")).toBe("bailian-cli/1.2.3");
|
||||
expect(headers.get("x-dashscope-source-config")).toBeNull();
|
||||
});
|
||||
|
||||
test("does not override a caller-provided User-Agent", async () => {
|
||||
const { headers } = await capture("https://dashscope.aliyuncs.com/api/v1/tasks/t1", {
|
||||
headers: { "User-Agent": "custom/9.9" },
|
||||
});
|
||||
expect(headers.get("user-agent")).toBe("custom/9.9");
|
||||
});
|
||||
|
||||
test("does not invent a Content-Type (FormData boundary safety)", async () => {
|
||||
const { headers } = await capture("https://dashscope.aliyuncs.com/api/v1/files", {
|
||||
method: "POST",
|
||||
});
|
||||
expect(headers.get("content-type")).toBeNull();
|
||||
});
|
||||
|
||||
test("passes non-URL-parseable inputs through without tracking headers", async () => {
|
||||
const { url, headers } = await capture("/relative/path");
|
||||
expect(url).toBe("/relative/path");
|
||||
expect(headers.get("x-dashscope-source-config")).toBeNull();
|
||||
});
|
||||
@@ -46,6 +46,11 @@ Index: [index.md](index.md)
|
||||
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
|
||||
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -72,6 +77,11 @@ bl agent apply --provider bailian --yes
|
||||
| `--yes` | switch | no | Confirm and destroy without an interactive prompt (required) |
|
||||
| `--cascade` | switch | no | Auto-delete dependent resources (e.g. sessions referencing an environment) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -130,6 +140,11 @@ bl agent init --provider all
|
||||
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
|
||||
| `--refresh-only` | switch | no | Refresh state and show drift without planning remote mutations |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -164,6 +179,11 @@ bl agent plan --no-refresh
|
||||
| `--title <title>` | string | no | Session title |
|
||||
| `--provider <name>` | string | no | Target provider (multi-provider agents) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -194,6 +214,11 @@ bl agent session create --agent assistant --title 'debug run'
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--provider <name>` | string | no | Target provider |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -218,6 +243,11 @@ bl agent session delete --session-id sess_abc123
|
||||
| `--limit <n>` | number | no | Maximum number of events to fetch |
|
||||
| `--all` | switch | no | Fetch all pages by following the cursor |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -244,6 +274,11 @@ bl agent session events --session-id sess_abc123 --all
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--provider <name>` | string | no | Target provider |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -267,6 +302,11 @@ bl agent session get --session-id sess_abc123
|
||||
| `--all` | switch | no | Fetch all pages by following the cursor |
|
||||
| `--provider <name>` | string | no | Target provider |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -303,6 +343,11 @@ bl agent session list --all
|
||||
| `--provider <name>` | string | no | Target provider |
|
||||
| `--no-stream` | switch | no | Use polling instead of SSE streaming |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -331,6 +376,11 @@ bl agent session run --agent assistant --prompt "summarize this repo"
|
||||
| `--provider <name>` | string | no | Target provider |
|
||||
| `--no-stream` | switch | no | Use polling instead of SSE streaming |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -354,6 +404,11 @@ bl agent session send --session-id sess_abc123 --message "continue"
|
||||
| `--resource-version <n>` | number | no | Resource version (for versioned resources like agents) |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -374,6 +429,11 @@ bl agent state import --address bailian.agent.assistant --remote-id agent-abc123
|
||||
| --------------- | ------ | -------- | --------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -399,6 +459,11 @@ bl agent state list --file agents.yaml
|
||||
| `--address <provider.type.name>` | string | yes | Resource state address (required) |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -420,6 +485,11 @@ bl agent state rm --address bailian.agent.assistant
|
||||
| `--address <provider.type.name>` | string | yes | Resource state address (required) |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -440,6 +510,11 @@ bl agent state show --address bailian.agent.assistant
|
||||
| --------------- | ------ | -------- | --------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_WORKSPACE_ID}).
|
||||
- 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>`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user