Compare commits

...

3 Commits

Author SHA1 Message Date
lisheng.lisheng 50ed680ade feat: enhance credential handling for managed-agent and memory APIs
- Updated README.md to clarify API key usage and access restrictions for TokenPlan and pay-as-you-go keys.
- Introduced shared credential validation logic to prevent TokenPlan keys from being used in incompatible contexts.
- Enhanced error messaging for credential resolution failures in managed-agent and memory plugins.
- Added tests for credential classification and workspace endpoint composition.
- Updated documentation to reflect changes in credential handling and workspace-scoped agentstudio endpoint requirements.
2026-08-15 16:50:08 +08:00
lisheng.lisheng f919ebae3c feat(dsh): remote managed-agent as on-demand tool + bl managed-agent run
Rework the managed-agent integration so a dsh user can, in plain
language, have a Bailian cloud agent created and run a task — no
hand-written agents.yaml, no prior apply.

New `bl managed-agent run --prompt <task> [--instructions] [--model]
[--agent]`: one step that idempotently materializes a cloud agent + its
environment, then opens a session and streams the result. It mirrors the
OpenAgentPack webui backend's ensure+run recipe (resolveProjectConfigFrom
Object → syncAgentResourcesWithStateBackend → readProjectRuntime +
startSessionRun) from an in-memory config, reusing the existing
credential spine in _engine/credentials.ts. State persists under the bl
config dir (~/.bailian/managed-agent/<agent>/), never the user's cwd, so
repeat runs with the same --agent reuse the materialized agent. Unlike
apply it provisions without --yes, since running is the intent.

dsh side: replace the SubagentProvider with a plain tool
`bailian_run_remote_task` (packages/dsh/src/tool-managed-agent). The
subagent seam did not fit: in the web profile every tool-subagent row is
disabled in the host plane (delegation lives in agent presets), a
provider fixes one agent identity in config, and the default numeric
maxDepth would fail-mount a no-depthLimit provider. As a tool the model
calls it directly and fills `instructions` from the user's intent, so the
remote agent's role is defined per task. Enabled by default — it creates
nothing at load, only on invocation.

LLM row: configure the base bundle's existing llm-pi-ai row instead of
mounting a second pi-ai instance (a second instance re-declares pi-ai's
global configurable-provider catalog and fails boot on a duplicate
amazon-bedrock). TokenPlan reads a dedicated BAILIAN_TOKENPLAN_API_KEY,
not DASHSCOPE_API_KEY: TokenPlan (sk-sp-) and pay-as-you-go (sk-ws-) keys
401 each other's endpoints, so sharing one var would silently break
whichever plugin lost.

Note: the ensure+run happy path could not be verified end-to-end on the
available account — agentstudio returns 404 there, and the existing
`managed-agent apply` 404s identically against the same endpoint/key, so
the failure is account/service provisioning, not this change. Command
wiring, dry-run, config assembly, credential injection and URL
construction were all verified.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 10:29:24 +08:00
lisheng.lisheng 9e9911aa86 feat(dsh): add bailian-cli-dsh plugin bundle for DeepSeek Harness
Expose Bailian capabilities to dsh through its service seams as one
package with six subpath plugin entries and a `dsh.bundle` patch:

- TokenPlan as an LLM provider
- bailian_vision_describe / bailian_image_generate tools over `bl`
- knowledge-base retrieval as a WebSearchProvider (`bailian-kb`)
- cross-session memory: tools, pre-step recall, turn-close persist
- managed-agent as a SubagentProvider

TokenPlan configures the base bundle's existing pi-ai row rather than
mounting a second `dsh-llm-pi-ai` instance. A second instance cannot
work: pi-ai re-declares its entire built-in provider catalog to
`registerConfigurableProviders`, and that directory is global, so boot
fails with a duplicate on `amazon-bedrock`.

Routes and vision support were probed against the live gateway.
qwen3.8-max, qwen3.7-plus, qwen3.6-flash and glm-5.2 read images;
qwen3.7-max rejects them with HTTP 400; the DeepSeek routes accept image
content without erroring yet stay blind. The DeepSeek entries therefore
do not declare image input — claiming it would turn a clean refusal into
a silently wrong answer — and `bailian_vision_describe` serves them by
returning text instead.

Also fix `bl memory` against the v2 API, each verified live:

- `profile get` used /profiles, which returns HTTP 500. The documented
  and working endpoint is /user_profile.
- `add` read `response.memory_ids`, which the service never returns. It
  returns `memory_nodes`, so text output always printed "IDs: none".
- `MemoryNode.created_at`/`updated_at` are unix seconds, not strings, and
  `UserProfileResponse.profile` did not match the wire shape.
- Add the missing request parameters: --meta-data, --project-id,
  --project-ids, --min-score, --enable-rerank, --plan-version,
  --enable-judge, --enable-rewrite, --timestamp.
- Add `memory profile list|detail|update|delete`, covering the four v2
  profile-schema operations the CLI was missing.

`plan_version: lite` is ignored by the service and still bills pro;
`enable_rerank: false` is what actually selects lite, which is ~50x
cheaper per search. The CLI flag and the memory plugin both send the
parameter that works.

Disable pnpm's autoInstallPeers: the @deepseek-ai/dsh-* rc line peers on
three packages that were never published to npm, which 404s the whole
workspace install. Verified the existing packages still build.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 01:47:18 +08:00
40 changed files with 5122 additions and 473 deletions
+3
View File
@@ -52,3 +52,6 @@ packages/cli/scene/**/outputs/
# Local scratch / plan drafts (never commit)
.scratch/
# pnpm pack output
*.tgz
+10
View File
@@ -30,6 +30,10 @@ import {
memoryDelete,
memoryProfileCreate,
memoryProfileGet,
memoryProfileList,
memoryProfileDetail,
memoryProfileUpdate,
memoryProfileDelete,
knowledgeRetrieve,
knowledgeSearch,
knowledgeChat,
@@ -98,6 +102,7 @@ import {
managedAgentValidate,
managedAgentPlan,
managedAgentApply,
managedAgentRun,
managedAgentDestroy,
managedAgentStateList,
managedAgentStateShow,
@@ -149,6 +154,10 @@ export const commands: Record<string, AnyCommand> = {
"memory delete": memoryDelete,
"memory profile create": memoryProfileCreate,
"memory profile get": memoryProfileGet,
"memory profile list": memoryProfileList,
"memory profile detail": memoryProfileDetail,
"memory profile update": memoryProfileUpdate,
"memory profile delete": memoryProfileDelete,
"knowledge retrieve": knowledgeRetrieve,
"knowledge search": knowledgeSearch,
"knowledge chat": knowledgeChat,
@@ -217,6 +226,7 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
"managed-agent apply": managedAgentApply,
"managed-agent run": managedAgentRun,
"managed-agent destroy": managedAgentDestroy,
"managed-agent state list": managedAgentStateList,
"managed-agent state show": managedAgentStateShow,
@@ -50,6 +50,7 @@ export interface CredentialHost {
*/
export const CREDENTIALS_NOTE = [
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
"The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.",
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
];
@@ -85,13 +86,19 @@ export function prepareProviderEnv(): void {
* the block references them and the interpolated value is empty (a literal in
* agents.yaml is respected).
*
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
* paths onto it verbatim; a value already ending in the suffix is left as-is.
* It is filled even without a credential — `client.baseUrl` is readable
* credential-less (defaults to the CLI's model-domain base URL) — so offline
* commands (which skip the credential assert) still satisfy the SDK's
* "workspace_id or base_url" schema. With no credential the `api_key` is left
* untouched: online commands reject it via {@link assertProviderCredentials}.
* `base_url` is composed from the workspace when one is known — block
* `workspace_id` (agents.yaml literal or interpolated `${BAILIAN_WORKSPACE_ID}`)
* first, then bl's configured `workspace_id` — because agentstudio is served
* only on the workspace-scoped host; the bare model-domain origin 404s it
* (managed-agents API overview: `https://{workspace_id}.cn-beijing.maas.
* aliyuncs.com/api/v1/agentstudio`, region cn-beijing only). Only with no
* workspace at all does the model-domain origin get {@link AGENTSTUDIO_API_PATH}
* suffixed. A value already ending in the suffix is left as-is. base_url is
* filled even without a credential — `client.baseUrl` is readable
* credential-less — so offline commands (which skip the credential assert)
* still satisfy the SDK's "workspace_id or base_url" schema. With no
* credential the `api_key` is left untouched: online commands reject it via
* {@link assertProviderCredentials}.
*/
export function injectProviderCredentials(
providers: Record<string, unknown>,
@@ -103,16 +110,27 @@ export function injectProviderCredentials(
const cred = host.client.exportApiCredential();
if (cred) block.api_key = cred.token;
if ("base_url" in block && !block.base_url) {
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = host.client.baseUrl.replace(/\/+$/, "");
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
? origin
: `${origin}${AGENTSTUDIO_API_PATH}`;
if ("workspace_id" in block && !block.workspace_id) {
// agents.yaml interpolation already replaced `${BAILIAN_WORKSPACE_ID}` in
// file-based flows; the inline runtime passes an object config that never
// interpolates, so read the env var here too (prepareProviderEnv
// placeholders it to "" when unset). bl's configured workspace_id is the
// last resort.
block.workspace_id =
process.env.BAILIAN_WORKSPACE_ID?.trim() || host.settings.workspaceId || "";
}
if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) {
block.workspace_id = host.settings.workspaceId;
if ("base_url" in block && !block.base_url) {
const workspaceId = typeof block.workspace_id === "string" ? block.workspace_id.trim() : "";
if (workspaceId) {
block.base_url = `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${AGENTSTUDIO_API_PATH}`;
} else {
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = host.client.baseUrl.replace(/\/+$/, "");
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
? origin
: `${origin}${AGENTSTUDIO_API_PATH}`;
}
}
}
@@ -0,0 +1,124 @@
import { mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import {
type BackendRuntimeInput,
LocalFileStateBackend,
resolveProjectConfigFromObject,
} from "@openagentpack/sdk";
import { getConfigDir } from "bailian-cli-core";
import {
assertProviderCredentials,
type CredentialHost,
injectProviderCredentials,
normalizeInterpolatedProviderBlocks,
prepareProviderEnv,
scrubCredentialEnv,
} from "./credentials.ts";
import { type HostContext, installSdkTransport } from "./transport.ts";
/** Default agent identity `bl managed-agent run` materializes and reuses. */
export const DEFAULT_INLINE_AGENT = "dsh-remote-runner";
/** Default model for the materialized agent. */
export const DEFAULT_INLINE_MODEL = "qwen3.8-max";
/** Default role when the caller supplies no `--instructions`. */
export const DEFAULT_INLINE_INSTRUCTIONS = "You are a helpful assistant. Complete the task.";
/** Environment name declared in the inline config; one cloud env per agent. */
const INLINE_ENVIRONMENT = "cloud";
export interface InlineAgentOptions {
agentName: string;
instructions: string;
model: string;
/** Override the persisted state location (defaults under the bl config dir). */
statePath?: string;
}
/**
* Slugify an agent name into a filesystem- and project-id-safe token. The state
* for each distinct agent lives in its own directory so repeat runs reuse the
* same materialized remote agent.
*/
function slugify(agentName: string): string {
const slug = agentName
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug.length > 0 ? slug : "agent";
}
/** Where a materialized agent's state is persisted (not the user's cwd). */
export function inlineStatePath(agentName: string): string {
return join(getConfigDir(), "managed-agent", slugify(agentName), "state.json");
}
/**
* The minimal in-memory project config that materializes into one cloud agent.
* `providers.bailian` carries empty `api_key`/`base_url`/`workspace_id`
* placeholders so {@link injectProviderCredentials} fills them from bl's auth
* chain and workspace sources (it only writes fields the block already
* declares). `workspace_id` lets injection compose the workspace-scoped
* agentstudio host instead of the model-domain origin.
*/
export function buildInlineConfig(opts: InlineAgentOptions): Record<string, unknown> {
return {
version: "1",
providers: {
bailian: { api_key: "", base_url: "", workspace_id: "" },
},
defaults: { provider: "bailian" },
environments: {
[INLINE_ENVIRONMENT]: {
description: "Bailian CLI cloud environment",
config: { type: "cloud", networking: { type: "unrestricted" } },
},
},
agents: {
[opts.agentName]: {
description: opts.agentName,
model: opts.model,
instructions: opts.instructions,
environment: INLINE_ENVIRONMENT,
provider: "bailian",
},
},
};
}
/**
* Build the `BackendRuntimeInput` shared by ensure (`syncAgentResourcesWith
* StateBackend`) and run (`readProjectRuntime` + `startSessionRun`). Mirrors the
* credential spine of {@link buildAgentRuntime} but sources config from an
* in-memory object instead of a file, so no `agents.yaml` or `apply` is required.
*/
export async function buildInlineBackendInput(
host: HostContext & CredentialHost,
opts: InlineAgentOptions,
): Promise<BackendRuntimeInput> {
installSdkTransport(host);
prepareProviderEnv();
const rawConfig = buildInlineConfig(opts);
const { config, projectName } = await resolveProjectConfigFromObject(rawConfig, {
projectName: slugify(opts.agentName),
});
normalizeInterpolatedProviderBlocks(config.providers);
injectProviderCredentials(config.providers, host);
scrubCredentialEnv();
assertProviderCredentials(config.providers);
const statePath = opts.statePath ?? inlineStatePath(opts.agentName);
mkdirSync(dirname(statePath), { recursive: true });
const stateBackend = new LocalFileStateBackend({ statePath });
return {
projectName,
config,
stateBackend,
stateScope: { projectId: slugify(opts.agentName) },
providers: config.providers,
};
}
@@ -0,0 +1,137 @@
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import {
readProjectRuntime,
startSessionRun,
startSessionRunPolling,
syncAgentResourcesWithStateBackend,
} from "@openagentpack/sdk";
import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import {
buildInlineBackendInput,
DEFAULT_INLINE_AGENT,
DEFAULT_INLINE_INSTRUCTIONS,
DEFAULT_INLINE_MODEL,
} from "./_engine/inline-runtime.ts";
import { renderCollectedEvents, streamAndRenderEvents } from "./_engine/session-render.ts";
const RUN_FLAGS = {
prompt: {
type: "string",
valueHint: "<text>",
description: "Task to run (required)",
required: true,
},
instructions: {
type: "string",
valueHint: "<text>",
description: "Role/system instructions for the remote agent (default: generic assistant)",
},
model: {
type: "string",
valueHint: "<id>",
description: `Model for the remote agent (default: ${DEFAULT_INLINE_MODEL})`,
},
agent: {
type: "string",
valueHint: "<name>",
description: `Agent identity to create/reuse (default: ${DEFAULT_INLINE_AGENT})`,
},
noStream: {
type: "switch",
description: "Use polling instead of SSE streaming",
},
} satisfies FlagsDef;
export default defineCommand({
description: "Provision (if needed) a cloud agent and run a task in one step",
auth: "apiKey",
usageArgs: "--prompt <text> [--instructions <text>] [--model <id>] [--agent <name>]",
flags: RUN_FLAGS,
exampleArgs: [
'--prompt "Summarize the latest AI news"',
'--prompt "Audit this dependency tree" --instructions "You are a security expert" --model qwen3.8-max',
],
notes: [
...CREDENTIALS_NOTE,
"Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.",
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const asJson = format === "json";
const agentName = flags.agent ?? DEFAULT_INLINE_AGENT;
const model = flags.model ?? DEFAULT_INLINE_MODEL;
const instructions = flags.instructions ?? DEFAULT_INLINE_INSTRUCTIONS;
if (settings.dryRun) {
emitResult(
{
would_run: {
prompt: flags.prompt,
agent: agentName,
model,
instructions,
mode: flags.noStream ? "polling" : "streaming",
},
},
format,
);
return;
}
await withAgentErrors(() =>
withStdoutProtected(async () => {
const input = await buildInlineBackendInput(ctx, { agentName, instructions, model });
// Ensure the remote agent + its cloud environment exist. Idempotent:
// a repeat run with the same agent name reuses the materialized state.
if (!asJson) process.stderr.write(`Ensuring cloud agent "${agentName}"…\n`);
const sync = await syncAgentResourcesWithStateBackend(input, agentName, {
policy: "force",
quiet: true,
});
if (sync.status !== "completed") {
const detail =
sync.error ??
sync.diagnostics.find((diag) => diag.severity === "error")?.message ??
`provisioning ended with status "${sync.status}"`;
throw new BailianError(
`Failed to provision cloud agent "${agentName}": ${detail}`,
ExitCode.GENERAL,
);
}
// Run the task inside a runtime bound to the just-materialized state.
await readProjectRuntime(input, async (runtime) => {
if (flags.noStream) {
const run = await startSessionRunPolling(runtime, flags.prompt, { agent: agentName });
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
renderCollectedEvents(run, asJson, {
session_id: run.session.id,
provider: run.provider,
agent: run.agentName,
});
} else {
const run = await startSessionRun(runtime, flags.prompt, { agent: agentName });
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
await streamAndRenderEvents(run.events, asJson, {
session_id: run.session.id,
provider: run.provider,
agent: run.agentName,
});
}
});
}),
);
},
});
+28 -2
View File
@@ -28,6 +28,16 @@ const ADD_FLAGS = {
valueHint: "<id>",
description: "Memory library ID (isolate memory space)",
},
projectId: {
type: "string",
valueHint: "<id>",
description: "Memory extraction rule ID (defaults to the library's default rule)",
},
metaData: {
type: "string",
valueHint: "<json>",
description: 'Custom metadata JSON object: {"location":"Beijing"}',
},
} satisfies FlagsDef;
type AddFlags = ParsedFlags<typeof ADD_FLAGS>;
@@ -40,6 +50,7 @@ export default defineCommand({
'--user-id user1 --content "The user likes Python programming"',
'--user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'',
'--user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx',
'--user-id user1 --content "Lives in Beijing" --meta-data \'{"source":"onboarding"}\'',
],
validate: (f: AddFlags) =>
!f.messages && !f.content ? "Provide --messages or --content." : undefined,
@@ -63,6 +74,15 @@ export default defineCommand({
if (flags.profileSchema) body.profile_schema = flags.profileSchema;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.projectId) body.project_id = flags.projectId;
if (flags.metaData) {
try {
body.meta_data = JSON.parse(flags.metaData);
} catch {
throw new UsageError("--meta-data must be valid JSON object");
}
}
const format = detectOutputFormat(settings.output);
@@ -78,8 +98,14 @@ export default defineCommand({
});
if (settings.quiet || format === "text") {
const ids = response.memory_ids?.join(", ") || "none";
emitBare(`Memory added. IDs: ${ids}`);
const nodes = response.memory_nodes ?? [];
if (nodes.length === 0) {
emitBare("No memory fragments were extracted.");
} else {
for (const node of nodes) {
emitBare(`[${node.event ?? "ADD"}] ${node.memory_node_id} ${node.content}`);
}
}
} else {
emitResult(response, format);
}
@@ -24,6 +24,11 @@ export default defineCommand({
},
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
projectId: {
type: "string",
valueHint: "<id>",
description: "Memory extraction rule ID (defaults to the library's default rule)",
},
},
exampleArgs: ["--user-id user1", "--user-id user1 --page-size 20 --page 2"],
async run(ctx) {
@@ -36,6 +41,7 @@ export default defineCommand({
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize));
if (flags.page !== undefined) params.set("page_num", String(flags.page));
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
if (flags.projectId) params.set("project_id", flags.projectId);
const path = `${memoryListPath()}?${params.toString()}`;
@@ -0,0 +1,44 @@
import { defineCommand, profileSchemaItemPath, detectOutputFormat } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Delete a profile schema",
auth: "apiKey",
usageArgs: "--schema-id <id> [flags]",
flags: {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
},
exampleArgs: ["--schema-id schema_xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const query = params.toString();
const base = profileSchemaItemPath(flags.schemaId);
const path = query ? `${base}?${query}` : base;
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "DELETE" }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path,
method: "DELETE",
});
if (settings.quiet || format === "text") {
emitBare(`Profile schema ${flags.schemaId} deleted.`);
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,52 @@
import {
defineCommand,
profileSchemaItemPath,
detectOutputFormat,
type ProfileSchemaGetResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Show a profile schema and its attribute IDs",
auth: "apiKey",
usageArgs: "--schema-id <id> [flags]",
flags: {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
},
exampleArgs: ["--schema-id schema_xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const query = params.toString();
const base = profileSchemaItemPath(flags.schemaId);
const path = query ? `${base}?${query}` : base;
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaGetResponse>({
path,
method: "GET",
});
if (settings.quiet || format === "text") {
emitBare(`${response.name}${response.description ? `${response.description}` : ""}`);
for (const attribute of response.attributes ?? []) {
emitBare(` [${attribute.attribute_id}] ${attribute.name}`);
}
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,55 @@
import {
defineCommand,
profileSchemaPath,
detectOutputFormat,
type ProfileSchemaListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "List profile schemas",
auth: "apiKey",
usageArgs: "[flags]",
flags: {
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
pageSize: { type: "number", valueHint: "<n>", description: "Results per page (default: 10)" },
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
},
exampleArgs: ["", "--page-size 20 --page 2"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize));
if (flags.page !== undefined) params.set("page_num", String(flags.page));
const query = params.toString();
const path = query ? `${profileSchemaPath()}?${query}` : profileSchemaPath();
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaListResponse>({
path,
method: "GET",
});
if (settings.quiet || format === "text") {
const schemas = response.profile_schemas ?? [];
if (schemas.length === 0) {
emitBare("No profile schemas found.");
} else {
for (const schema of schemas) {
emitBare(`[${schema.profile_schema_id}] ${schema.name}`);
}
if (response.total !== undefined) emitBare(`\nTotal: ${response.total}`);
}
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,81 @@
import {
defineCommand,
UsageError,
profileSchemaItemPath,
detectOutputFormat,
type ProfileSchemaUpdateRequest,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import type { FlagsDef, ParsedFlags } from "bailian-cli-core";
const UPDATE_FLAGS = {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
name: { type: "string", valueHint: "<name>", description: "New schema name" },
description: { type: "string", valueHint: "<text>", description: "New schema description" },
attributeOps: {
type: "string",
valueHint: "<json>",
description:
'Attribute operations JSON array: [{"op":"add","name":"plan"},{"op":"delete","attribute_id":"attr_1"}]',
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
} satisfies FlagsDef;
type UpdateFlags = ParsedFlags<typeof UPDATE_FLAGS>;
export default defineCommand({
description: "Update a profile schema's name, description, or attributes",
auth: "apiKey",
usageArgs: "--schema-id <id> [--name <name>] [--attribute-ops <json>] [flags]",
flags: UPDATE_FLAGS,
notes: ["Attribute IDs for update/delete operations come from `memory profile detail`."],
exampleArgs: [
'--schema-id schema_xxx --name "user_basic_v2"',
'--schema-id schema_xxx --attribute-ops \'[{"op":"add","name":"plan","description":"subscription plan"}]\'',
'--schema-id schema_xxx --attribute-ops \'[{"op":"delete","attribute_id":"attr_1"}]\'',
],
validate: (f: UpdateFlags) =>
!f.name && !f.description && !f.attributeOps
? "Provide --name, --description, or --attribute-ops."
: undefined,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const body: ProfileSchemaUpdateRequest = {};
if (flags.name) body.name = flags.name;
if (flags.description) body.description = flags.description;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.attributeOps) {
try {
body.attributes_operations = JSON.parse(flags.attributeOps);
} catch {
throw new UsageError("--attribute-ops must be valid JSON array");
}
}
const path = profileSchemaItemPath(flags.schemaId);
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "PATCH", request: body }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path,
method: "PATCH",
body,
});
if (settings.quiet || format === "text") {
emitBare(`Profile schema ${flags.schemaId} updated.`);
} else {
emitResult(response, format);
}
},
});
@@ -24,6 +24,38 @@ const SEARCH_FLAGS = {
description: "Number of results to return (default: 10)",
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
projectIds: {
type: "array",
valueHint: "<id>",
description: "Memory extraction rule ID for hybrid retrieval (repeatable)",
},
minScore: {
type: "number",
valueHint: "<n>",
description: "Minimum similarity score, 0-1 (default: 0.3)",
},
enableRerank: {
type: "boolean",
valueHint: "<bool>",
description:
"Rerank results. Also selects the billing tier: false bills lite, true bills pro (~50x). (default: true)",
},
planVersion: {
type: "string",
valueHint: "<lite|pro>",
description:
"Documented billing tier. The service currently honors --enable-rerank instead, so prefer that flag",
},
enableJudge: {
type: "boolean",
valueHint: "<bool>",
description: "Enable the intent-discrimination callback (default: false)",
},
enableRewrite: {
type: "boolean",
valueHint: "<bool>",
description: "Enable query rewriting (default: false)",
},
} satisfies FlagsDef;
type SearchFlags = ParsedFlags<typeof SEARCH_FLAGS>;
@@ -35,6 +67,7 @@ export default defineCommand({
exampleArgs: [
'--user-id user1 --query "programming preferences"',
'--user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5',
'--user-id user1 --query "preferences" --enable-rerank false --min-score 0.5',
],
validate: (f: SearchFlags) =>
!f.query && !f.messages ? "Provide --query or --messages." : undefined,
@@ -61,6 +94,21 @@ export default defineCommand({
if (flags.topK !== undefined) body.top_k = flags.topK;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.projectIds && flags.projectIds.length > 0) body.project_ids = flags.projectIds;
if (flags.minScore !== undefined) body.min_score = flags.minScore;
if (flags.enableRerank !== undefined) body.enable_rerank = flags.enableRerank;
if (flags.enableJudge !== undefined) body.enable_judge = flags.enableJudge;
if (flags.enableRewrite !== undefined) body.enable_rewrite = flags.enableRewrite;
if (flags.planVersion) {
if (flags.planVersion !== "lite" && flags.planVersion !== "pro") {
throw new UsageError("--plan-version must be lite or pro");
}
body.plan_version = flags.planVersion;
// The service ignores plan_version on its own, so mirror the intent onto
// the flag it does honor unless the caller set that one explicitly.
if (flags.enableRerank === undefined) body.enable_rerank = flags.planVersion === "pro";
}
const format = detectOutputFormat(settings.output);
@@ -1,5 +1,6 @@
import {
defineCommand,
UsageError,
memoryNodePath,
detectOutputFormat,
type MemoryNodeUpdateRequest,
@@ -34,6 +35,16 @@ export default defineCommand({
valueHint: "<id>",
description: "Memory library ID (non-default library)",
},
timestamp: {
type: "number",
valueHint: "<unix-seconds>",
description: "When the remembered event happened (default: now)",
},
metaData: {
type: "string",
valueHint: "<json>",
description: 'Custom metadata JSON object, merged incrementally: {"source":"manual"}',
},
},
exampleArgs: ['--node-id node_xxx --user-id user1 --content "updated memory content"'],
async run(ctx) {
@@ -47,6 +58,15 @@ export default defineCommand({
custom_content: content,
};
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.timestamp !== undefined) body.timestamp = flags.timestamp;
if (flags.metaData) {
try {
body.meta_data = JSON.parse(flags.metaData);
} catch {
throw new UsageError("--meta-data must be valid JSON object");
}
}
const format = detectOutputFormat(settings.output);
+5
View File
@@ -33,6 +33,10 @@ export { default as memoryUpdate } from "./commands/memory/update.ts";
export { default as memoryDelete } from "./commands/memory/delete.ts";
export { default as memoryProfileCreate } from "./commands/memory/profile-create.ts";
export { default as memoryProfileGet } from "./commands/memory/profile-get.ts";
export { default as memoryProfileList } from "./commands/memory/profile-list.ts";
export { default as memoryProfileDetail } from "./commands/memory/profile-detail.ts";
export { default as memoryProfileUpdate } from "./commands/memory/profile-update.ts";
export { default as memoryProfileDelete } from "./commands/memory/profile-delete.ts";
export { default as knowledgeRetrieve } from "./commands/knowledge/retrieve.ts";
export { default as knowledgeSearch } from "./commands/knowledge/search.ts";
export { default as knowledgeChat } from "./commands/knowledge/chat.ts";
@@ -95,6 +99,7 @@ export { default as managedAgentInit } from "./commands/managed-agent/init.ts";
export { default as managedAgentValidate } from "./commands/managed-agent/validate.ts";
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
export { default as managedAgentApply } from "./commands/managed-agent/apply.ts";
export { default as managedAgentRun } from "./commands/managed-agent/run.ts";
export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts";
export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts";
export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts";
@@ -124,7 +124,8 @@ test("inject:已带后缀且尾斜杠的 base_url 去斜杠后原样保留", ()
expect(providers.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio");
});
test("inject:workspace_id 引用且为空时 settings 填充;有字面量则保留", () => {
test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面量则保留", () => {
delete process.env.BAILIAN_WORKSPACE_ID;
const empty = { bailian: { api_key: "", workspace_id: "" } };
injectProviderCredentials(
empty,
@@ -132,6 +133,16 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则
);
expect(empty.bailian.workspace_id).toBe("ws-settings");
// 内联运行时(对象配置)不做 ${} 插值,env 变量在此补读。
process.env.BAILIAN_WORKSPACE_ID = "ws-env";
const fromEnv = { bailian: { api_key: "", workspace_id: "" } };
injectProviderCredentials(
fromEnv,
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
);
expect(fromEnv.bailian.workspace_id).toBe("ws-env");
delete process.env.BAILIAN_WORKSPACE_ID;
const literal = { bailian: { api_key: "", workspace_id: "ws-yaml" } };
injectProviderCredentials(
literal,
@@ -140,6 +151,37 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则
expect(literal.bailian.workspace_id).toBe("ws-yaml");
});
test("inject:workspace 已知时 base_url 拼工作空间主机,而非模型域 origin", () => {
// agents.yaml 字面量 workspace_id + 空 base_url。
const literal = { bailian: { api_key: "", base_url: "", workspace_id: "ws-yaml" } };
injectProviderCredentials(literal, makeHost({ apiCred: bailianCred() }));
expect(literal.bailian.base_url).toBe(
"https://ws-yaml.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
);
// 内联块:workspace_id 由 settings 填充后同样走工作空间主机。
const inline = { bailian: { api_key: "", base_url: "", workspace_id: "" } };
injectProviderCredentials(
inline,
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
);
expect(inline.bailian.workspace_id).toBe("ws-settings");
expect(inline.bailian.base_url).toBe(
"https://ws-settings.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
);
// 显式 base_url 字面量永远优先于拼装。
const explicit = {
bailian: {
api_key: "",
base_url: "https://custom.example.com/api/v1/agentstudio",
workspace_id: "ws-yaml",
},
};
injectProviderCredentials(explicit, makeHost({ apiCred: bailianCred() }));
expect(explicit.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
});
test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(providers, makeHost({}));
@@ -33,6 +33,10 @@ export const MEMORY_ROUTES: E2eRouteExports = {
"memory delete": "memoryDelete",
"memory profile create": "memoryProfileCreate",
"memory profile get": "memoryProfileGet",
"memory profile list": "memoryProfileList",
"memory profile detail": "memoryProfileDetail",
"memory profile update": "memoryProfileUpdate",
"memory profile delete": "memoryProfileDelete",
};
export const KNOWLEDGE_ROUTES: E2eRouteExports = {
@@ -173,6 +177,7 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent validate": "managedAgentValidate",
"managed-agent plan": "managedAgentPlan",
"managed-agent apply": "managedAgentApply",
"managed-agent run": "managedAgentRun",
"managed-agent destroy": "managedAgentDestroy",
"managed-agent state list": "managedAgentStateList",
"managed-agent state rm": "managedAgentStateRm",
+5 -1
View File
@@ -75,7 +75,11 @@ export function profileSchemaPath(): string {
}
export function userProfilePath(schemaId: string): string {
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/profiles`;
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/user_profile`;
}
export function profileSchemaItemPath(schemaId: string): string {
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}`;
}
// ---- Knowledge Base Retrieve (DashScope) ----
+1
View File
@@ -13,6 +13,7 @@ export {
memoryNodePath,
memorySearchPath,
mcpWebSearchPath,
profileSchemaItemPath,
profileSchemaPath,
speechRecognizePath,
speechSynthesizePath,
+82 -7
View File
@@ -305,11 +305,22 @@ export interface MemoryAddRequest {
custom_content?: string;
profile_schema?: string;
memory_library_id?: string;
project_id?: string;
meta_data?: Record<string, unknown>;
}
/** 变更的记忆片段;`event` 为 ADD / UPDATE / DELETE。 */
export interface MemoryAddNode {
memory_node_id: string;
content: string;
event?: string;
/** 仅 `event` 为 UPDATE 时有效。 */
old_content?: string;
}
export interface MemoryAddResponse {
request_id: string;
memory_ids?: string[];
memory_nodes?: MemoryAddNode[];
}
export interface MemorySearchRequest {
@@ -318,6 +329,17 @@ export interface MemorySearchRequest {
query?: string;
top_k?: number;
memory_library_id?: string;
project_ids?: string[];
min_score?: number;
/**
* **** `plan_version`,
* `enable_rerank: false` lite (pro lite 50 )
*/
enable_rerank?: boolean;
/** 文档所述的档位字段;当前服务端未按文档生效,与 `enable_rerank` 一起传。 */
plan_version?: "lite" | "pro";
enable_judge?: boolean;
enable_rewrite?: boolean;
}
export interface MemoryNode {
@@ -325,13 +347,19 @@ export interface MemoryNode {
content: string;
user_id?: string;
meta_data?: Record<string, unknown>;
created_at?: string;
updated_at?: string;
project_id?: string;
/** 秒级 Unix 时间戳。 */
created_at?: number;
/** 秒级 Unix 时间戳。 */
updated_at?: number;
timestamp?: number;
}
export interface MemorySearchResponse {
request_id: string;
memory_nodes: MemoryNode[];
/** 本次检索实际计费的档位。 */
billing_plan?: string;
}
export interface MemoryNodeListResponse {
@@ -347,13 +375,18 @@ export interface MemoryNodeUpdateRequest {
custom_content: string;
/** 非默认记忆库时必填(与控制台记忆库 ID 一致) */
memory_library_id?: string;
/** 记忆片段对应事件发生时的秒级 Unix 时间戳。 */
timestamp?: number;
/** 增量更新。 */
meta_data?: Record<string, unknown>;
}
// ---- Memory Profile (DashScope v2) ----
export interface ProfileAttribute {
name: string;
description: string;
description?: string;
default_value?: string;
value?: string;
}
@@ -361,6 +394,8 @@ export interface ProfileSchemaCreateRequest {
name: string;
description?: string;
attributes: ProfileAttribute[];
memory_library_id?: string;
plan_version?: "lite" | "pro";
}
export interface ProfileSchemaCreateResponse {
@@ -368,12 +403,52 @@ export interface ProfileSchemaCreateResponse {
profile_schema_id: string;
}
export interface ProfileSchemaSummary {
profile_schema_id: string;
name: string;
description?: string;
}
export interface ProfileSchemaListResponse {
request_id: string;
profile_schemas: ProfileSchemaSummary[];
total?: number;
}
/** 画像模板详情;`attributes[].attribute_id` 是更新/删除属性时的定位键。 */
export interface ProfileSchemaGetResponse {
request_id: string;
name: string;
description?: string;
attributes: Array<ProfileAttribute & { attribute_id: string }>;
}
export interface ProfileSchemaAttributeOperation {
op: "add" | "update" | "delete";
/** `update` / `delete` 必填。 */
attribute_id?: string;
/** `add` 必填。 */
name?: string;
description?: string;
default_value?: string | null;
}
export interface ProfileSchemaUpdateRequest {
name?: string;
description?: string;
memory_library_id?: string;
attributes_operations?: ProfileSchemaAttributeOperation[];
}
/**
* /, schema_id / user_id
*/
export interface UserProfileResponse {
request_id: string;
profile: {
schema_id: string;
user_id: string;
attributes: ProfileAttribute[];
schema_name?: string;
schema_description?: string;
attributes: Array<{ id: string; name: string; value?: string }>;
};
}
+265
View File
@@ -0,0 +1,265 @@
# bailian-cli-dsh
把阿里云百炼Model Studio的能力接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)`dsh`)的 profile bundle。
一个包提供 5 个插件行,外加对 base bundle 的 `llm-pi-ai` 行做一次配置覆盖:
| row id | 能力 | 默认 | 依赖 |
| ---------------------------- | --------------------------------------------------------------- | ---- | --------------------- |
| `llm-pi-ai`(覆盖 base 行) | 把百炼 TokenPlan 网关注册成 LLM provider`bailian-tokenplan` | 启用 | TokenPlan Key |
| `bailian-tool-vision` | `bailian_vision_describe`:图片/视频理解 | 启用 | `bl` |
| `bailian-tool-image` | `bailian_image_generate`:文生图 | 启用 | `bl` |
| `bailian-tool-managed-agent` | `bailian_run_remote_task`:按需在云端创建 agent 并跑任务 | 启用 | `bl` + 按量付费 Key |
| `bailian-web-search-rag` | 百炼知识库检索,注册为 `web_search` 的后端 | 停用 | 按量付费 Key + 知识库 |
| `bailian-memory` | 跨会话长期记忆tools + 自动检索/落库) | 停用 | 按量付费 Key |
`web-search-rag``memory` 默认停用是有意的:它们要么需要部署方特有的资源 ID要么按次计费不该在用户没配置时就生效。`tool-managed-agent` 默认启用——它加载时不建任何资源,只有模型真正调用时才在云端创建 agent。
---
## 1. 前置条件
- Node ≥ 22.19`dsh` 的要求)
- `bl`vision / image / 远程任务 三个工具通过子进程调它)
```sh
npm install -g bailian-cli
```
- 百炼 API Key。**注意有两类且不可混用**
| 类型 | 前缀 | 能访问 | 不能访问 |
| --------- | -------- | ----------------------------------------------------------- | ------------------------ |
| TokenPlan | `sk-sp-` | TokenPlan 网关LLM / vision / 文生图) | 记忆库、知识库、远程任务 |
| 按量付费 | `sk-ws-` | 记忆库、知识库、远程任务agentstudio、DashScope 全量接口 | TokenPlan 网关 |
两者互相返回 `401 InvalidApiKey`,所以本包用**两个不同的环境变量**,不会互相踩:
```sh
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx # 只给 bailian-tokenplan provider
export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG、远程任务
```
只有一类 Key 也能用,只是能力范围相应缩小。若只有 TokenPlan Key
```sh
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx
export DASHSCOPE_API_KEY=sk-sp-xxx
export DASHSCOPE_BASE_URL=https://token-plan.cn-beijing.maas.aliyuncs.com
```
这样 LLM / vision / 文生图可用(后两者经 `bl` 走 TokenPlan 网关memory 与 RAG 保持停用即可。**远程任务仍可注册**,但它的凭证解析会看出这是 TokenPlan Key / 网关,调用 `bailian_run_remote_task` 时直接给出带修复指引的报错,而不是以前的 `Bailian API 404`
**按量付费 Key 的解析顺序**memory / RAG / 远程任务三处一致):行内 `config.apiKey``$DASHSCOPE_API_KEY`
**远程任务的端点**另有讲究managed-agentagentstudioAPI **只**在工作空间前缀主机上提供——`https://{workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`(普通 dashscope 主机与 TokenPlan 网关都 404且 Key 只能访问**自己归属的工作空间**(不匹配时 403 `Endpoint.AccessDenied`)。端点解析顺序:行内 `baseUrl``$DASHSCOPE_BASE_URL` → 行内 `workspaceId``$BAILIAN_WORKSPACE_ID`后两者自动拼成工作空间主机。workspace ID 在百炼控制台右上角的工作空间下拉里看。
memory / RAG 是显式开启的插件Key 缺失或误填 `sk-sp-` 会在启动期报错;远程任务默认启用,为避免拖垮 TokenPlan-only 环境,改为调用时报错。
---
## 2. 安装到 `web` profile
`npx @deepseek-ai/dsh web``dsh --profile web` 的别名,所以要装进**名为 `web` 的 profile**,配置目录是 `~/.dsh/profiles/web/``$DSH_HOME` 可覆盖)。
本包尚未发布到 npm先在本仓库打包
```sh
pnpm -F bailian-cli-dsh build
cd packages/dsh && pnpm pack # 产出 bailian-cli-dsh-<version>.tgz
```
装入 profile`dsh plugin` 是 pnpm 的转发器,接受本地路径 / tarball / npm 包名 / git
```sh
npx @deepseek-ai/dsh plugin --profile web add /absolute/path/to/bailian-cli-dsh-1.14.2.tgz
```
因为 `package.json` 声明了 `dsh.bundle`,安装后会自动加入该 profile 的 bundle 层,无需手动改 `cordis.patch.yml`
确认 5 个插入行都在,且 TokenPlan provider 已配到 `llm-pi-ai` 上:
```sh
npx @deepseek-ai/dsh --profile web --dump-config | grep -E 'bailian|tokenplan'
```
启动:
```sh
npx @deepseek-ai/dsh web
```
Web UI 在 http://127.0.0.1:3080。
> 发布到 npm 后直接 `npx @deepseek-ai/dsh plugin --profile web add bailian-cli-dsh`,跳过打包步骤。
---
## 3. 开箱能用的部分
装完不做任何配置就生效:
**LLM provider** — 模型选择器里出现 `bailian-tokenplan`,可选模型(已逐个实测):
| 模型 | 读图 |
| ------------------------ | ------------------ |
| `qwen3.8-max` | 是 |
| `qwen3.7-plus` | 是 |
| `qwen3.6-flash` | 是 |
| `glm-5.2` | 是 |
| `qwen3.7-max` | 否(传图直接 400 |
| `deepseek-v4-pro` | 否 |
| `deepseek-v4-flash-0731` | 否 |
**三个工具** — `bailian_vision_describe``bailian_image_generate``bailian_run_remote_task`
前两个走 TokenPlanvision/image`bailian_run_remote_task` 见 [§3.1](#31-远程任务-bailian_run_remote_task)——它默认启用但用的是**按量付费 Key + dashscope 端点**,与 TokenPlan 那两个不同。
### 关于看图,有个坑值得知道
dsh 会在两处**提前**拦截图片Web UI 粘图前会查当前模型的输入模态,`read_image` 也有同样的门禁。所以主模型选 DeepSeek 时,图片根本进不到对话里。
- 主模型选 `qwen3.8-max` 等标着"是"的 → 直接粘图,原生看图,不需要任何工具
- 主模型选 DeepSeek → 让它调 `bailian_vision_describe`,工具返回**文字描述**,绕过模态门禁
DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见**(实测会回答 "None"),所以本包坚决没给它们声明 `input: [image]`——否则会从"明确拒绝"退化成"静默失明",更难排查。
`bailian_image_generate` 同理:模型能看图时返回内联图片,不能看图时降级为返回落盘路径,你可以接着用 vision 工具读它。文件不会被删除,正是为了这个衔接。
### 3.1 远程任务(`bailian_run_remote_task`
把一个任务甩到百炼云端的托管 agent 上跑,不占本地会话。**无需预先写 `agents.yaml``apply`**:工具首次被调用时,`bl managed-agent run` 会在你的账号里幂等创建一个 agent + cloud environment之后复用。
- 模型自己按用户意图填 `instructions`(远程 agent 的角色),`task` 是要它做的事。例如你说「在云端帮我审计这个依赖树,它该懂安全」→ 模型调 `bailian_run_remote_task(task="审计依赖树", instructions="你是安全专家")`
- **前提**:这条路走的是 managed-agentagentstudio服务需要**按量付费 Key**`sk-ws-`+ **工作空间端点**,且账号已开通 managed-agent。TokenPlan Key 不适用。
- **凭证解析**Key 为 `config.apiKey``$DASHSCOPE_API_KEY`;端点为 `config.baseUrl``$DASHSCOPE_BASE_URL``config.workspaceId``$BAILIAN_WORKSPACE_ID`(后两者自动拼成 `https://{workspaceId}.cn-beijing.maas.aliyuncs.com`)。凡是解析出来的,都会显式下发给 `bl`,不会落到 `bl` 活动 config profile 的端点上——这正是旧版 `Bailian API 404` 的根因agentstudio **只**在工作空间前缀主机上提供TokenPlan 网关与普通 dashscope 主机都 404。
- **两个高频报错**`404`=端点不是工作空间主机;`403 Endpoint.AccessDenied`=主机对了但这个 Key 不属于该工作空间。二者都会附带具体修复指引。Key 归属的工作空间在百炼控制台右上角下拉里看。
- 首次会创建云资源(可能计费、启动有延迟);同名 agent 后续复用。默认 agent 名 `dsh-remote-runner`,可在配置里改。
需要非默认的 agent 名 / 模型 / 凭证时:
```yaml
- id: bailian-tool-managed-agent
config:
agent: my-runner
model: qwen3.8-max
timeoutMs: 600000
# 可选凭证(省略则按上面的解析顺序找):
# apiKey: sk-ws-xxxxxxxx
# workspaceId: llm-xxxxxxxx # 推荐:自动拼成工作空间端点
# baseUrl: https://llm-xxxxxxxx.cn-beijing.maas.aliyuncs.com # 或用完整端点
```
---
## 4. 开启可选插件
用户层配置写在 `~/.dsh/profiles/web/cordis.patch.yml`,按 row `id` 覆盖 bundle 的默认值。
> **一个必须记住的语义**patch 是按 row **整体替换 `config`**,不是深合并。所以覆盖一行时要把该行完整的 config 重写一遍。
### 知识库检索RAG
注册 id 为 `bailian-kb` 的搜索后端,模型用它熟悉的 `web_search` 就能检索私域文档。
```yaml
- id: bailian-web-search-rag
disabled: false
config:
workspaceId: llm-xxxxxxxx # 百炼控制台工作空间 ID
agentId: aid-xxxxxxxx # 知识库"检索服务"ID
maxResults: 10
# apiKey 省略则读 $DASHSCOPE_API_KEY须为按量付费 sk-ws-;误填 sk-sp- 会在启动期报错)
```
一个实例对一个知识库(`WebSearchRequest` 只带 `query` / `maxResults`agentId 只能来自配置)。要多个知识库就插多行不同 `id`
**如果 profile 里还有别的搜索 provider**base bundle 默认带 `web-search-deepseek`),必须显式指定用哪个,否则 dsh 报 `WEB_PROVIDER_AMBIGUOUS`
```yaml
- id: web
config:
searchProvider: bailian-kb
```
### 长期记忆
dsh 自身没有跨会话记忆(`ctx.compaction` 只在单会话内压缩上下文)。开启后:两个工具 `bailian_memory_search` / `bailian_memory_add`,加上每个会话首轮自动检索注入、每轮结束自动落库。
```yaml
- id: bailian-memory
disabled: false
config:
userId: your-name # 省略则读 $BAILIAN_MEMORY_USER_ID再退到系统用户名
planVersion: lite
topK: 10
autoInject: true
injectEveryTurn: false # 开启会变成每轮一次检索,成本相应上升
autoPersist: true
```
**费用**:记忆库自 2026-08-20 起商业化add 与 search 按次计费pro 档约为 lite 档的 50 倍。
实测发现一个与文档不符的地方:单独传 `plan_version: lite` 会被服务端忽略、仍按 pro 计费,真正生效的开关是 `enable_rerank: false`。本插件已按此处理——`planVersion: lite`(默认)会同时下发 `enable_rerank: false`,所以默认就是便宜的那档。
不想要自动行为、只保留手动工具:
```yaml
- id: bailian-memory
disabled: false
config:
userId: your-name
autoInject: false
autoPersist: false
```
> 远程任务(`bailian_run_remote_task`)默认启用,配置见 [§3.1](#31-远程任务-bailian_run_remote_task)。
---
## 5. 验证
```sh
# 配置是否被正确合成(改完 patch 后先看这个)
npx @deepseek-ai/dsh --profile web --dump-config | grep -A5 bailian-memory
# bl 是否就绪
bl auth status
```
启动后逐项试:
- **LLM**:切到 `bailian-tokenplan / qwen3.8-max`,随便发一句
- **原生看图**:同上模型,直接粘一张图提问
- **间接看图**:切到 `deepseek-v4-pro`,让它用 `bailian_vision_describe` 读同一张图
- **文生图**:让模型生成一张图
- **RAG**:问一个只有知识库里才有答案的问题
- **记忆**:会话 A 告诉它一个事实 → 关掉 → 新开会话 B 提问,看是否命中
- **远程任务**:说「在云端帮我跑一个任务:<something>,它该擅长 <role>」→ 确认模型调用 `bailian_run_remote_task``instructions` 由模型按 role 填)→ 首次触发云端创建 → 返回远程会话结果(需按量付费 Key + 已开通 agentstudio
---
## 6. 常见问题
| 现象 | 原因 |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key |
| memory / RAG 启动期报 TokenPlan Key | `DASHSCOPE_API_KEY` / `apiKey` 误填了 `sk-sp-` 的 TokenPlan Key |
| 远程任务 `Bailian API 404` | 端点不是工作空间前缀主机TokenPlan 网关 / 普通 dashscope 主机都不提供 agentstudio给该行配 `workspaceId`(或 `baseUrl`),或导出 `BAILIAN_WORKSPACE_ID` / `DASHSCOPE_BASE_URL` |
| 远程任务 `403 Endpoint.AccessDenied` | 主机是工作空间主机,但这个 Key 不属于该工作空间;换成 Key 归属工作空间的 ID控制台右上角下拉或用属于该工作空间的 Key |
| 远程任务调用即报 TokenPlan 提示 | `$DASHSCOPE_API_KEY``sk-sp-`;换按量付费 Key 或在行内配 `apiKey` |
| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider需在 `web` 行 pin `searchProvider` |
| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 |
| 工具报找不到 `bl` | `bl` 不在 PATH`npm install -g bailian-cli` |
| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 |
| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 |
---
## 7. 卸载
```sh
npx @deepseek-ai/dsh plugin --profile web remove bailian-cli-dsh
```
移除后 bundle 层会自动从 `dsh.profile.bundles` 摘掉;`~/.dsh/profiles/web/cordis.patch.yml` 里你手写的覆盖行需要自己清理。
+112
View File
@@ -0,0 +1,112 @@
# bailian-cli-dsh — Aliyun Model Studio (Bailian) as a dsh profile bundle.
#
# Applied over whatever the earlier layers composed: one config override of the
# base bundle's dormant pi-ai adapter, then one insert of the Bailian tool rows.
# Every inserted id is `bailian-`-prefixed so a user profile can address,
# reconfigure, or disable any single capability without touching the others.
# Remember that a later patch REPLACES a row's whole `config` rather than
# merging into it, so restate the complete config when overriding.
# TokenPlan configures the base bundle's existing `llm-pi-ai` row instead of
# inserting a second @deepseek-ai/dsh-llm-pi-ai instance. That plugin declares
# pi-ai's entire built-in provider catalog to
# `ctx.llm.registerConfigurableProviders` on every apply, and that registry
# refuses an already-declared provider, so a second instance always fails the
# whole tree with DUPLICATE_DIRECTORY on `amazon-bedrock`. Only adapter routes
# are per-instance; the configurable-provider directory is global.
#
# Replacing this row's whole `config` costs nothing: the base mounts it dormant
# with no config of its own. A profile that needs to drop or re-aim TokenPlan
# restates this row's config rather than disabling an id.
- id: llm-pi-ai
config:
providers:
bailian-tokenplan:
displayName: Aliyun Bailian TokenPlan
api: openai-completions
baseURL: https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
# A dedicated name, not DASHSCOPE_API_KEY. TokenPlan keys (sk-sp-)
# and pay-as-you-go keys (sk-ws-) are not interchangeable: this
# gateway 401s a pay-as-you-go key, and the memory / knowledge-base
# endpoints 401 a TokenPlan key. Sharing one variable would make
# whichever plugin loses silently fail to authenticate.
apiKeyEnv: BAILIAN_TOKENPLAN_API_KEY
compat:
thinkingFormat: qwen
supportsReasoningEffort: true
# Verified against GET /compatible-mode/v1/models plus a per-model
# image probe on 2026-08-14. Only `id` is required; context window
# and max tokens fall back to this provider's defaults.
#
# `input: [text, image]` is a claim about the endpoint, not a
# checked fact, and it is what opens the Web UI paste path and
# `read_image`. Every entry carrying it answered a colour question
# about a test PNG correctly. The DeepSeek routes deliberately do
# NOT carry it: they accept image content without erroring and then
# answer "None", so declaring vision would turn a clean refusal
# into a silently blind reply. Use `bailian_vision_describe` there.
#
# Omitted on purpose: wan2.7-image / wan2.7-image-pro are
# generation-only (reach them through `bailian_image_generate`) and
# qwen-audio-3.0-* are audio endpoints, not chat completions.
models:
- id: qwen3.8-max
input: [text, image]
- id: qwen3.7-plus
input: [text, image]
- id: qwen3.6-flash
input: [text, image]
# Rejects image content outright with HTTP 400.
- id: qwen3.7-max
- id: glm-5.2
input: [text, image]
compat:
thinkingFormat: deepseek
- id: deepseek-v4-pro
compat:
thinkingFormat: deepseek
- id: deepseek-v4-flash-0731
compat:
thinkingFormat: deepseek
- insert:
- id: bailian-tool-vision
name: bailian-cli-dsh/tool-vision
- id: bailian-tool-image
name: bailian-cli-dsh/tool-image
# Enabled by default: the tool creates no resources at load time. It only
# provisions a cloud agent when the model actually calls it, and reuses it
# after — no deployment-specific ID to configure up front.
#
# Credentials: needs a pay-as-you-go key (sk-ws-), resolved from row config
# `apiKey`, then $DASHSCOPE_API_KEY. The agentstudio API is served only on
# the workspace-scoped host, so the endpoint resolves from row `baseUrl`,
# then $DASHSCOPE_BASE_URL, then row `workspaceId` / $BAILIAN_WORKSPACE_ID
# composed into https://{workspace}.cn-beijing.maas.aliyuncs.com — whatever
# resolves ships to bl explicitly, never leaving the endpoint to bl's
# active-profile base_url (a TokenPlan or bare model-domain origin 404s
# agentstudio). The key must belong to that workspace. A resolved TokenPlan
# key or TokenPlan endpoint rejects at call time with guidance (this row is
# enabled by default and must not break boot for TokenPlan-only setups).
- id: bailian-tool-managed-agent
name: bailian-cli-dsh/tool-managed-agent
# Disabled by default: the knowledge base to query is deployment-specific,
# and an enabled provider with no agentId would make `web_search` ambiguous
# for everyone. Set workspaceId + agentId and flip `disabled` to use it.
# Key: row config `apiKey`, then $DASHSCOPE_API_KEY (pay-as-you-go sk-ws-;
# a TokenPlan key is rejected at boot).
- id: bailian-web-search-rag
name: bailian-cli-dsh/web-search-rag
disabled: true
config: {}
# Disabled by default: memory add/search are billed per call.
# Key: row config `apiKey`, then $DASHSCOPE_API_KEY (pay-as-you-go sk-ws-;
# a missing key or a TokenPlan key fails the boot with an actionable message).
- id: bailian-memory
name: bailian-cli-dsh/memory
disabled: true
config: {}
+111
View File
@@ -0,0 +1,111 @@
{
"name": "bailian-cli-dsh",
"version": "1.14.2",
"description": "Aliyun Model Studio (Bailian) plugin bundle for DeepSeek Harness (dsh): TokenPlan LLM provider, knowledge-base RAG search, vision, image generation, long-term memory, and managed-agent subagents.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
"url": "https://github.com/modelstudioai/cli/issues"
},
"license": "Apache-2.0",
"author": "Aliyun Model Studio",
"repository": {
"type": "git",
"url": "git+https://github.com/modelstudioai/cli.git",
"directory": "packages/dsh"
},
"files": [
"README.md",
"dist",
"cordis.patch.yml"
],
"type": "module",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./tool-vision": {
"types": "./src/tool-vision/index.ts",
"default": "./src/tool-vision/index.ts"
},
"./tool-image": {
"types": "./src/tool-image/index.ts",
"default": "./src/tool-image/index.ts"
},
"./tool-managed-agent": {
"types": "./src/tool-managed-agent/index.ts",
"default": "./src/tool-managed-agent/index.ts"
},
"./web-search-rag": {
"types": "./src/web-search-rag/index.ts",
"default": "./src/web-search-rag/index.ts"
},
"./memory": {
"types": "./src/memory/index.ts",
"default": "./src/memory/index.ts"
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public",
"exports": {
".": "./dist/index.mjs",
"./tool-vision": "./dist/tool-vision/index.mjs",
"./tool-image": "./dist/tool-image/index.mjs",
"./tool-managed-agent": "./dist/tool-managed-agent/index.mjs",
"./web-search-rag": "./dist/web-search-rag/index.mjs",
"./memory": "./dist/memory/index.mjs",
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"build": "vp pack",
"dev": "vp pack --watch",
"test": "vp test",
"check": "vp check"
},
"dependencies": {
"@deepseek-ai/schemastery": "^3.18.1"
},
"devDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
"@deepseek-ai/dsh-web": "^0.1.0-rc.6",
"@types/node": "catalog:",
"typescript": "^6.0.2",
"vite-plus": "catalog:"
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
"@deepseek-ai/dsh-web": "^0.1.0-rc.6"
},
"engines": {
"node": ">=22.19.0"
},
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* bailian-cli-dsh Aliyun Model Studio capabilities as a DeepSeek Harness
* profile bundle. The package's substance is `cordis.patch.yml`, declared by
* the `dsh.bundle.patch` manifest field and resolved by the profile composer
* through that field; this module carries no runtime API.
* @module bailian-cli-dsh
*/
export {};
+379
View File
@@ -0,0 +1,379 @@
/**
* `bailian-cli-dsh/memory`: cross-session long-term memory backed by Bailian's
* hosted memory library.
*
* dsh has no memory seam `ctx.compaction` only summarizes within one
* session's context window and never writes across sessions so this plugin
* supplies the whole capability: two tools for deliberate reads and writes,
* plus automatic retrieval and persistence around each turn.
*
* Calls go straight to DashScope rather than through `bl memory`, because the
* v2 API exposes retrieval controls (`min_score`, `plan_version`,
* `enable_rerank`, `meta_data`) the CLI does not surface.
*
* BILLING: add and search are charged per call, and `pro` costs roughly fifty
* times `lite` per search. Automatic behaviour therefore defaults to `lite`,
* retrieves once per session rather than once per turn, and never requests
* profile extraction unless a schema is configured.
*
* @module bailian-cli-dsh/memory
*/
import { userInfo } from "node:os";
import type { Context } from "@deepseek-ai/cordis";
import type { Agent, PreStepDecision } from "@deepseek-ai/dsh-agent";
import type {} from "@deepseek-ai/dsh-agent";
import type { ContentBlock, Message } from "@deepseek-ai/dsh-llm";
import { createUserMessage } from "@deepseek-ai/dsh-llm";
import { defineTool } from "@deepseek-ai/dsh-tools";
import z from "@deepseek-ai/schemastery";
import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts";
import { dashScopeFetch, resolveApiKey, resolveBaseUrl } from "../shared/http.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-memory";
/** Seams this plugin registers into. */
export const inject = ["tools", "agents"];
export interface Config {
apiKey?: string;
baseUrl?: string;
/** Memory entity id. Falls back to `$BAILIAN_MEMORY_USER_ID`, then the OS user. */
userId?: string;
memoryLibraryId?: string;
projectId?: string;
/** Profile template id; omitting it skips profile extraction (and its cost). */
profileSchema?: string;
/** `lite` disables rerank and is ~50x cheaper per search. */
planVersion?: "lite" | "pro";
topK?: number;
minScore?: number;
/** Retrieve relevant memories and inject them into the conversation. */
autoInject?: boolean;
/** Retrieve on every turn instead of once per session. Costs one search per turn. */
injectEveryTurn?: boolean;
/** Persist each turn's new messages when the turn closes. */
autoPersist?: boolean;
}
export const Config: z<Config> = z.object({
apiKey: z
.string()
.role("secret")
.description(
"Pay-as-you-go DashScope key (sk-ws-); defaults to $DASHSCOPE_API_KEY. TokenPlan keys are rejected.",
),
baseUrl: z.string().description("DashScope base URL override."),
userId: z.string().description("Memory entity id owning these memories."),
memoryLibraryId: z.string().description("Memory library id; defaults to the account default."),
projectId: z.string().description("Memory extraction rule id."),
profileSchema: z.string().description("Profile template id; enables profile extraction."),
planVersion: z
.union(["lite", "pro"] as const)
.description("Search strategy; pro enables rerank at ~50x the cost."),
topK: z.natural().description("Maximum memories to recall (1-100)."),
minScore: z.number().description("Minimum similarity score, 0-1."),
autoInject: z.boolean().description("Inject recalled memories automatically."),
injectEveryTurn: z.boolean().description("Retrieve every turn instead of once per session."),
autoPersist: z.boolean().description("Persist new messages when a turn closes."),
});
const DEFAULT_TOP_K = 10;
const DEFAULT_PLAN_VERSION = "lite";
const MEMORY_PATH = "/api/v2/apps/memory";
interface MemoryNode {
memory_node_id?: string;
content?: string;
event?: string;
old_content?: string;
created_at?: number;
updated_at?: number;
}
interface MemoryResponse {
request_id?: string;
memory_nodes?: readonly MemoryNode[];
}
interface ChatTurn {
role: "user" | "assistant";
content: string;
}
/** Resolution order: explicit config, then environment, then the OS user. */
function resolveUserId(ctx: Context, config: Config): string {
if (config.userId !== undefined && config.userId.length > 0) return config.userId;
const fromEnv = ctx.get("launchEnvironment")?.get("BAILIAN_MEMORY_USER_ID")?.value;
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
return userInfo().username;
}
function textOf(content: readonly ContentBlock[]): string {
return content
.filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
.map((block) => block.text)
.join("\n")
.trim();
}
/** Plain user/assistant exchanges; tool traffic and injected context are not memories. */
function conversationTurns(messages: readonly Message[]): ChatTurn[] {
const turns: ChatTurn[] = [];
for (const message of messages) {
if (message.role !== "user" && message.role !== "assistant") continue;
if (message.role === "user" && message.source.kind !== "user") continue;
const text = textOf(message.content);
if (text.length > 0) turns.push({ role: message.role, content: text });
}
return turns;
}
class MemoryClient {
constructor(
private readonly apiKey: string,
private readonly baseUrl: string,
private readonly config: Config,
private readonly userId: string,
) {}
private shared(): Record<string, unknown> {
return {
user_id: this.userId,
...(this.config.memoryLibraryId !== undefined
? { memory_library_id: this.config.memoryLibraryId }
: {}),
};
}
async add(
messages: readonly ChatTurn[],
signal: AbortSignal | undefined,
overrides?: { customContent?: string; metaData?: Record<string, unknown> },
): Promise<MemoryResponse> {
return dashScopeFetch<MemoryResponse>({
url: `${this.baseUrl}${MEMORY_PATH}/add`,
method: "POST",
apiKey: this.apiKey,
signal,
body: {
...this.shared(),
...(overrides?.customContent !== undefined
? { custom_content: overrides.customContent }
: { messages }),
...(this.config.projectId !== undefined ? { project_id: this.config.projectId } : {}),
...(this.config.profileSchema !== undefined
? { profile_schema: this.config.profileSchema }
: {}),
...(overrides?.metaData !== undefined ? { meta_data: overrides.metaData } : {}),
},
});
}
async search(
messages: readonly ChatTurn[],
signal: AbortSignal | undefined,
overrides?: { topK?: number; minScore?: number; planVersion?: "lite" | "pro" },
): Promise<MemoryResponse> {
const planVersion = overrides?.planVersion ?? this.config.planVersion ?? DEFAULT_PLAN_VERSION;
return dashScopeFetch<MemoryResponse>({
url: `${this.baseUrl}${MEMORY_PATH}/memory_nodes/search`,
method: "POST",
apiKey: this.apiKey,
signal,
body: {
...this.shared(),
messages,
top_k: overrides?.topK ?? this.config.topK ?? DEFAULT_TOP_K,
...((overrides?.minScore ?? this.config.minScore) !== undefined
? { min_score: overrides?.minScore ?? this.config.minScore }
: {}),
// `enable_rerank` is what actually selects the billing tier. Sending
// `plan_version: lite` alone still bills `pro` (verified against the
// live API), despite the documented precedence, and pro costs ~50x
// more per search. Send both: the flag that works, plus the
// documented field in case the server-side precedence is fixed.
enable_rerank: planVersion === "pro",
plan_version: planVersion,
...(this.config.projectId !== undefined ? { project_ids: [this.config.projectId] } : {}),
},
});
}
}
function renderMemories(nodes: readonly MemoryNode[]): string {
const lines = nodes
.map((node) => node.content?.trim())
.filter((content): content is string => content !== undefined && content.length > 0)
.map((content) => `- ${content}`);
return `What you remember about this user from earlier sessions:\n${lines.join("\n")}`;
}
function registerTools(ctx: Context, client: MemoryClient): void {
ctx.tools.register(
defineTool({
name: "bailian_memory_search",
description:
"Recall facts stored about this user in earlier sessions. Use when the user refers " +
"to prior context, preferences, or decisions you have no record of in this session.",
parameters: {
query: { type: "string", required: true, description: "What to recall." },
top_k: { type: "integer", description: "Maximum memories to return (1-100)." },
min_score: { type: "number", description: "Minimum similarity score, 0-1." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
memories: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
id: { type: "string", required: true },
content: { type: "string", required: true },
},
},
},
},
},
render: (_args, value) => [
{
type: "text",
text:
value.memories.length === 0
? "No relevant memories."
: value.memories.map((memory) => `- ${memory.content}`).join("\n"),
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
const response = await client.search([{ role: "user", content: args.query }], exec.signal, {
...(args.top_k !== undefined ? { topK: args.top_k } : {}),
...(args.min_score !== undefined ? { minScore: args.min_score } : {}),
});
return {
memories: (response.memory_nodes ?? []).map((node) => ({
id: node.memory_node_id ?? "",
content: node.content ?? "",
})),
};
},
}),
);
ctx.tools.register(
defineTool({
name: "bailian_memory_add",
description:
"Store a durable fact about this user so later sessions can recall it. Use for stable " +
"preferences, decisions, and context — not for transient task state.",
parameters: {
content: { type: "string", required: true, description: "The fact to remember." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: { stored: { type: "integer", required: true } },
},
render: (_args, value) => [
{ type: "text", text: `Stored ${value.stored} memory fragment(s).` },
],
},
async execute(args, exec) {
const response = await client.add([], exec.signal, { customContent: args.content });
return { stored: (response.memory_nodes ?? []).length };
},
}),
);
}
function registerLifecycle(ctx: Context, client: MemoryClient, config: Config): void {
const injected = new WeakSet<Agent>();
const persistedUpTo = new WeakMap<Agent, number>();
if (config.autoInject !== false) {
ctx.on(
"agent/pre-step",
async ({ agent, messages, signal }, next): Promise<PreStepDecision> => {
const decision = await next();
if (decision.kind !== "enter") return decision;
if (injected.has(agent) && config.injectEveryTurn !== true) return decision;
const query = textOf(messages.flatMap((message) => message.content));
if (query.length === 0) return decision;
let nodes: readonly MemoryNode[];
try {
const response = await client.search([{ role: "user", content: query }], signal);
nodes = response.memory_nodes ?? [];
} catch {
// Recall is an enhancement; a memory-service outage must not stop the turn.
return decision;
}
injected.add(agent);
if (nodes.length === 0) return decision;
const text = renderMemories(nodes);
return {
...decision,
messages: [
...decision.messages,
createUserMessage({
content: [{ type: "text", text }],
source: {
kind: "plugin",
plugin: name,
form: "snapshot",
sections: [{ name, text }],
},
}),
],
};
},
{ prepend: true },
);
}
if (config.autoPersist !== false) {
ctx.on("agent/turn-stopping", async ({ agent, signal }): Promise<void> => {
const turns = conversationTurns(agent.session.deriveMessages());
const from = persistedUpTo.get(agent) ?? 0;
const fresh = turns.slice(from);
if (fresh.length === 0) return;
persistedUpTo.set(agent, turns.length);
try {
await client.add(fresh, signal);
} catch {
// Persistence is best-effort; never fail a turn over it.
persistedUpTo.set(agent, from);
}
});
}
}
export function apply(ctx: Context, config: Config): void {
const apiKey = resolveApiKey(ctx, config.apiKey);
if (apiKey === undefined) {
throw new Error(
"bailian-memory: no DashScope API key. Set `apiKey` in this row's config or export " +
"$DASHSCOPE_API_KEY (a pay-as-you-go sk-ws- key; the memory API 401s TokenPlan keys).",
);
}
if (isTokenPlanKey(apiKey)) {
throw new Error(tokenPlanKeyRejection(name, "the memory API"));
}
const client = new MemoryClient(
apiKey,
resolveBaseUrl(ctx, config.baseUrl),
config,
resolveUserId(ctx, config),
);
registerTools(ctx, client);
registerLifecycle(ctx, client, config);
}
+161
View File
@@ -0,0 +1,161 @@
/**
* Shared `bl` invocation for the plugins that delegate to the Bailian CLI
* rather than calling DashScope directly the ones whose CLI implementation
* carries real substance (async task polling, artifact download, SSE session
* streaming, `agents.yaml` resolution) that a plugin should not restate.
* @module bailian-cli-dsh/shared/bl
*/
import type { Context } from "@deepseek-ai/cordis";
import type { SubprocessSpawnSpec } from "@deepseek-ai/dsh-subprocess";
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
const DEFAULT_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
const DEFAULT_STDERR_MAX_BYTES = 64 * 1024;
const DEFAULT_GRACE_MS = 5_000;
/**
* Environment names `bl` reads for credentials, endpoint routing, and profile
* selection. `scrubbedParentEnv()` strips credential-shaped names from every
* harness child, so the key would never reach `bl` unless forwarded here.
*/
const FORWARDED_ENV_NAMES = [
"DASHSCOPE_API_KEY",
"DASHSCOPE_BASE_URL",
"DASHSCOPE_TIMEOUT",
"BAILIAN_WORKSPACE_ID",
"BAILIAN_CONFIG_DIR",
"ALIBABA_CLOUD_ACCESS_KEY_ID",
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
"ALIBABA_CLOUD_SECURITY_TOKEN",
] as const;
/** A `bl` invocation that exited non-zero or produced unreadable output. */
export class BlError extends Error {
constructor(
message: string,
readonly detail: { argv: readonly string[]; exitCode: number | null; stderr: string },
options?: { cause?: unknown },
) {
super(message, options);
this.name = "BlError";
}
}
export interface RunBlOptions {
/** Working directory for the child; callers pass the session cwd. */
cwd: string;
signal: AbortSignal;
/** Extra entries layered after the forwarded Bailian names. */
env?: NodeJS.ProcessEnv;
stdoutMaxBytes?: number;
graceMs?: number;
}
export interface BlOutcome {
stdout: string;
stderr: string;
exitCode: number | null;
terminatedBy: NodeJS.Signals | null;
}
function abortError(): DOMException {
return new DOMException("bl invocation aborted", "AbortError");
}
function forwardedEnv(ctx: Context, extra: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
const launchEnvironment = launchEnvironmentOf(ctx);
const env: NodeJS.ProcessEnv = {};
for (const name of FORWARDED_ENV_NAMES) {
const entry = launchEnvironment.get(name);
if (entry !== undefined) env[name] = entry.value;
}
return { ...env, ...extra };
}
/**
* Run `bl` to completion and collect its output.
* @throws {BlError} when the executable cannot be resolved.
* @throws {DOMException} `AbortError` when the caller's signal fires.
*/
export async function runBl(
ctx: Context,
argv: readonly string[],
options: RunBlOptions,
): Promise<BlOutcome> {
if (options.signal.aborted) throw abortError();
const env = forwardedEnv(ctx, options.env);
let executable: string;
try {
executable = await ctx.subprocess.resolveExecutable(
"bl",
env as Readonly<Record<string, string>>,
options.signal,
);
} catch (error) {
throw new BlError(
"the `bl` executable was not found on PATH; install it with `npm install -g bailian-cli`",
{ argv, exitCode: null, stderr: "" },
{ cause: error },
);
}
const spec: SubprocessSpawnSpec = {
argv: [executable, ...argv],
cwd: options.cwd,
stdio: {
stdin: "ignore",
stdout: { maxBytes: options.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES },
stderr: { maxBytes: DEFAULT_STDERR_MAX_BYTES },
},
graceMs: options.graceMs ?? DEFAULT_GRACE_MS,
signal: options.signal,
env,
};
const handle = ctx.subprocess.spawn(spec);
if (options.signal.aborted) throw abortError();
const outcome = await handle.done;
if (options.signal.aborted) throw abortError();
return {
stdout: handle.collected.stdout?.readFrom(0).text ?? "",
stderr: handle.collected.stderr?.readFrom(0).text ?? "",
exitCode: outcome.exitCode,
terminatedBy: outcome.signal,
};
}
/**
* Run `bl … --output json` and parse stdout.
* @throws {BlError} on non-zero exit or unparseable stdout.
*/
export async function runBlJson<T>(
ctx: Context,
argv: readonly string[],
options: RunBlOptions,
): Promise<T> {
const withJson = [...argv, "--output", "json"];
const outcome = await runBl(ctx, withJson, options);
if (outcome.exitCode !== 0) {
// bl passes service errors through verbatim; surface them unchanged.
const reason = outcome.stderr.trim() || outcome.stdout.trim() || "no diagnostics on stderr";
throw new BlError(`bl ${argv.join(" ")} failed: ${reason}`, {
argv: withJson,
exitCode: outcome.exitCode,
stderr: outcome.stderr,
});
}
try {
return JSON.parse(outcome.stdout) as T;
} catch (error) {
throw new BlError(
`bl ${argv.join(" ")} did not emit JSON on stdout`,
{ argv: withJson, exitCode: outcome.exitCode, stderr: outcome.stderr },
{ cause: error },
);
}
}
+93
View File
@@ -0,0 +1,93 @@
/**
* Pure credential classification and pairing shared by the plugins that call
* pay-as-you-go DashScope APIs directly (memory, knowledge base) or through
* `bl managed-agent` (agentstudio). No runtime imports this module is safe
* to load from tests and its rules are locked by `tests/credentials.test.ts`.
*
* TokenPlan keys (`sk-sp-`) and pay-as-you-go keys (`sk-ws-`) are not
* interchangeable: the TokenPlan gateway 401s a pay-as-you-go key, and the
* service APIs this package calls 401 or 404 a TokenPlan key. The LLM
* provider row keeps its TokenPlan key under a dedicated env name
* (`BAILIAN_TOKENPLAN_API_KEY`); every other plugin needs a pay-as-you-go key
* and rejects a TokenPlan one up front instead of failing at request time.
*
* @module bailian-cli-dsh/shared/credentials
*/
/**
* Standard DashScope model-domain endpoint. It serves the model APIs plus the
* memory v2 and knowledge indices the plugins call directly but NOT
* `/api/v1/agentstudio`, which lives on the workspace-scoped host.
*/
export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com";
/** Key prefix that marks a TokenPlan key (which service APIs reject). */
export const TOKEN_PLAN_KEY_PREFIX = "sk-sp-";
/** Whether a key is shaped like a TokenPlan key (which service APIs reject). */
export function isTokenPlanKey(apiKey: string): boolean {
return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
}
/**
* Whether a base URL points at the TokenPlan gateway. That gateway serves the
* model-inference routes only none of the service APIs this package calls,
* including `/api/v1/agentstudio`, so requests to it 404.
*/
export function isTokenPlanEndpoint(baseUrl: string): boolean {
try {
return new URL(baseUrl).hostname.startsWith("token-plan.");
} catch {
// An unparseable URL fails the request later with its own diagnostics;
// this check only classifies well-formed endpoints.
return false;
}
}
/**
* The standard error wording every plugin uses when it resolves a TokenPlan
* key, so all three surfaces fail with one recognizable, actionable message.
*/
export function tokenPlanKeyRejection(plugin: string, capability: string): string {
return (
`${plugin}: the resolved API key is a TokenPlan key (${TOKEN_PLAN_KEY_PREFIX}…), which ` +
`${capability} rejects. Use a pay-as-you-go key (sk-ws-): set \`apiKey\` in this row's ` +
"config or $DASHSCOPE_API_KEY. TokenPlan keys belong on $BAILIAN_TOKENPLAN_API_KEY, " +
"which only the `bailian-tokenplan` LLM provider reads."
);
}
/**
* Build the `--api-key` / `--base-url` flags handed to `bl managed-agent run`.
* Each resolved half ships independently:
*
* - A resolved key becomes `--api-key`, overriding bl's auth chain so an
* active TokenPlan profile cannot substitute its own key.
* - A resolved endpoint becomes `--base-url`, overriding the ACTIVE PROFILE's
* base_url the half that fixes the classic `Bailian API 404`, where a
* TokenPlan (or bare model-domain) origin does not serve
* `/api/v1/agentstudio`.
*
* There is deliberately NO fallback endpoint: agentstudio is only served on
* the workspace-scoped host (see {@link workspaceEndpoint}), and an unknown
* workspace is a configuration gap, not a defaultable value. Unresolved halves
* emit nothing and bl's own auth chain decides them.
*/
export function credentialFlags(apiKey: string | undefined, baseUrl: string | undefined): string[] {
const flags: string[] = [];
if (baseUrl !== undefined && baseUrl.length > 0) flags.push("--base-url", baseUrl);
if (apiKey !== undefined && apiKey.length > 0) flags.push("--api-key", apiKey);
return flags;
}
/**
* Compose the workspace-scoped agentstudio host for a workspace id. The
* managed-agent API is served only from
* `https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`
* (bl/the SDK append the resource path onto this origin); the plain
* dashscope origin 404s it, and a key only unlocks its own workspace's host
* (a mismatched one 403s `Endpoint.AccessDenied`).
*/
export function workspaceEndpoint(workspaceId: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com`;
}
+101
View File
@@ -0,0 +1,101 @@
/**
* Direct DashScope HTTP for the plugins whose CLI counterpart does not expose
* the full parameter surface (long-term memory, knowledge-base retrieval).
* Service errors pass through verbatim this layer classifies nothing.
* @module bailian-cli-dsh/shared/http
*/
import type { Context } from "@deepseek-ai/cordis";
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
import { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts";
export { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts";
/** A non-2xx DashScope response, carrying the server's own wording. */
export class DashScopeError extends Error {
constructor(
message: string,
readonly detail: { status: number; code?: string; requestId?: string },
options?: { cause?: unknown },
) {
super(message, options);
this.name = "DashScopeError";
}
}
/**
* Resolve the DashScope key: explicit row config first, then the launch
* environment (process env, project `.env`, harness-home `.env`). Callers
* that get `undefined` decide their own failure mode opt-in plugins reject
* at boot, the managed-agent tool falls through to bl's own auth chain.
*/
export function resolveApiKey(ctx: Context, explicit?: string): string | undefined {
if (explicit !== undefined && explicit.length > 0) return explicit;
const entry = launchEnvironmentOf(ctx).get("DASHSCOPE_API_KEY");
return entry !== undefined && entry.value.length > 0 ? entry.value : undefined;
}
export function resolveBaseUrl(ctx: Context, explicit?: string): string {
if (explicit !== undefined && explicit.length > 0) return explicit;
const entry = launchEnvironmentOf(ctx).get("DASHSCOPE_BASE_URL");
return entry !== undefined && entry.value.length > 0 ? entry.value : DASHSCOPE_DEFAULT_BASE_URL;
}
export interface DashScopeRequest {
url: string;
method: "GET" | "POST" | "PATCH" | "DELETE";
apiKey: string;
body?: unknown;
signal?: AbortSignal | undefined;
}
interface DashScopeErrorBody {
code?: string;
message?: string;
request_id?: string;
error?: { code?: string; message?: string };
}
/**
* Issue one DashScope request and parse its JSON body.
* @throws {DashScopeError} on a non-2xx response or an unreadable body.
*/
export async function dashScopeFetch<T>(request: DashScopeRequest): Promise<T> {
const response = await fetch(request.url, {
method: request.method,
headers: {
Authorization: `Bearer ${request.apiKey}`,
"Content-Type": "application/json",
},
...(request.body !== undefined ? { body: JSON.stringify(request.body) } : {}),
...(request.signal !== undefined ? { signal: request.signal } : {}),
redirect: "error",
});
const text = await response.text();
if (!response.ok) {
let parsed: DashScopeErrorBody = {};
try {
parsed = JSON.parse(text) as DashScopeErrorBody;
} catch {
// A non-JSON error body is still worth surfacing as-is.
}
const code = parsed.code ?? parsed.error?.code;
const message = parsed.message ?? parsed.error?.message ?? text.trim();
throw new DashScopeError(message.length > 0 ? message : `HTTP ${response.status}`, {
status: response.status,
...(code !== undefined ? { code } : {}),
...(parsed.request_id !== undefined ? { requestId: parsed.request_id } : {}),
});
}
try {
return JSON.parse(text) as T;
} catch (error) {
throw new DashScopeError(
"DashScope returned a non-JSON success body",
{ status: response.status },
{ cause: error },
);
}
}
+239
View File
@@ -0,0 +1,239 @@
/**
* `bailian-cli-dsh/tool-image`: image generation through `bl image generate`.
*
* Delegating to the CLI keeps the async-task polling, model-to-endpoint
* routing, and artifact download in one place rather than restating them here.
*
* Generated files are committed to `ctx.attachments` and returned as
* `ImageBlock`s when the calling route declares image input. When it does not
* DeepSeek routes never do the tool degrades to reporting the saved paths
* instead of failing, so the model can hand one to `bailian_vision_describe`.
* For that fallback to work the files must survive the call, so this tool
* deliberately does not delete what the CLI wrote.
*
* @module bailian-cli-dsh/tool-image
*/
import type { Context } from "@deepseek-ai/cordis";
import type { ImageAttachmentRef, ImageMediaType } from "@deepseek-ai/dsh-attachment";
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
import type {} from "@deepseek-ai/dsh-fs";
import { defineTool } from "@deepseek-ai/dsh-tools";
import type { ToolExecution } from "@deepseek-ai/dsh-tools";
import z from "@deepseek-ai/schemastery";
import { runBlJson } from "../shared/bl.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-tool-image";
/** Seams this plugin registers into. */
export const inject = ["tools", "subprocess", "fs"];
export interface Config {
/** Image model passed to `bl image generate --model`. */
model?: string;
/** Directory for generated files; defaults to the CLI's own output dir. */
outDir?: string;
/** Cooperative budget; async models poll until the task succeeds. */
timeoutMs?: number;
}
export const Config: z<Config> = z.object({
model: z.string().description("Image model; defaults to the CLI's own default."),
outDir: z.string().description("Directory for generated files."),
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
});
const DEFAULT_TIMEOUT_MS = 300_000;
const MAX_IMAGES = 6;
const MEDIA_TYPE_BY_EXTENSION: Readonly<Record<string, ImageMediaType>> = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
webp: "image/webp",
gif: "image/gif",
};
interface ImageGenerateResponse {
urls?: readonly string[];
saved?: readonly string[];
total?: number;
}
/** One committed image, stored as plain JSON so `render` stays pure. */
interface CommittedImage {
attachmentId: string;
mediaType: ImageMediaType;
bytes: number;
width: number;
height: number;
path: string;
}
function mediaTypeOf(path: string): ImageMediaType | undefined {
const extension = path.split(".").pop()?.toLowerCase();
return extension === undefined ? undefined : MEDIA_TYPE_BY_EXTENSION[extension];
}
function attachmentRefOf(image: CommittedImage): ImageAttachmentRef {
return {
attachmentId: AttachmentId(image.attachmentId),
mediaType: image.mediaType,
bytes: image.bytes,
width: image.width,
height: image.height,
};
}
/**
* Whether the calling route declares image input. Unlike `read_image`'s hard
* gate this only reports, because an unroutable or text-only model is a reason
* to fall back to paths rather than to refuse generating anything.
*/
async function routeAcceptsImages(ctx: Context, exec: ToolExecution): Promise<boolean> {
const routed = exec.agent?.session.requestHeader()?.config;
const provider = routed?.provider ?? exec.agent?.options.provider;
const model = routed?.model ?? exec.agent?.options.model;
const llm = ctx.get("llm");
if (provider === undefined || model === undefined || llm === undefined) return false;
try {
const active = await llm.resolveModelInfo(provider, model, exec.signal);
return active.inputModalities?.includes("image") === true;
} catch {
return false;
}
}
export function apply(ctx: Context, config: Config): void {
ctx.tools.register(
defineTool({
name: "bailian_image_generate",
description:
"Generate images from a text prompt using Aliyun Bailian (Qwen-Image / Wan). " +
"Files are written to disk and returned inline when the active model can view " +
"images; otherwise the saved paths are reported and you can inspect one with " +
"`bailian_vision_describe`.",
parameters: {
prompt: {
type: "string",
required: true,
description: "What to depict. Be specific about subject, style, and composition.",
},
model: { type: "string", description: "Override the configured image model." },
size: {
type: "string",
description: 'Aspect ratio such as "1:1" / "16:9", or explicit pixels as "1024*1024".',
},
n: {
type: "integer",
description: `How many images to generate (1-${MAX_IMAGES}).`,
},
negative_prompt: { type: "string", description: "What to avoid depicting." },
seed: { type: "integer", description: "Seed for reproducible generation." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
images: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
attachmentId: { type: "string", required: true },
mediaType: { type: "string", required: true },
bytes: { type: "integer", required: true },
width: { type: "integer", required: true },
height: { type: "integer", required: true },
path: { type: "string", required: true },
},
},
},
paths: { type: "array", required: true, items: { type: "string" } },
urls: { type: "array", required: true, items: { type: "string" } },
},
},
render: (_args, value) => {
const paths = value.paths.join("\n");
if (value.images.length === 0) {
return [
{
type: "text",
text:
`Generated ${value.paths.length} image(s); the active model cannot view ` +
`images, so they are on disk only. Use bailian_vision_describe to inspect ` +
`one.\n${paths}`,
},
];
}
const blocks: ContentBlock[] = [
{ type: "text", text: `Generated ${value.images.length} image(s):\n${paths}` },
];
for (const image of value.images) {
blocks.push({ type: "image", attachment: attachmentRefOf(image as CommittedImage) });
}
return blocks;
},
},
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
async execute(args, exec) {
if (args.n !== undefined && (args.n < 1 || args.n > MAX_IMAGES)) {
throw new Error(`bailian_image_generate accepts n between 1 and ${MAX_IMAGES}.`);
}
const cwd = exec.agent?.session.header.cwd ?? process.cwd();
const argv = ["image", "generate", "--prompt", args.prompt];
const model = args.model ?? config.model;
if (model !== undefined) argv.push("--model", model);
if (args.size !== undefined) argv.push("--size", args.size);
if (args.n !== undefined) argv.push("--n", String(args.n));
if (args.negative_prompt !== undefined)
argv.push("--negative-prompt", args.negative_prompt);
if (args.seed !== undefined) argv.push("--seed", String(args.seed));
if (config.outDir !== undefined) argv.push("--out-dir", config.outDir);
const response = await runBlJson<ImageGenerateResponse>(ctx, argv, {
cwd,
signal: exec.signal,
});
const paths = [...(response.saved ?? [])];
const urls = [...(response.urls ?? [])];
if (paths.length === 0) {
throw new Error("bl image generate reported no saved files.");
}
const attachments = ctx.get("attachments");
const images: CommittedImage[] = [];
if (attachments !== undefined && (await routeAcceptsImages(ctx, exec))) {
const byteCap = Math.min(
attachments.imageLimits.maxImageBytes,
attachments.imageLimits.maxMessageImageBytes,
);
for (const path of paths) {
const mediaType = mediaTypeOf(path);
if (mediaType === undefined || !attachments.imageLimits.mediaTypes.includes(mediaType))
continue;
const target = await ctx.fs.resolve(path, { cwd, signal: exec.signal });
const data = await ctx.fs.readBytes(target, exec.signal, byteCap);
const ref = await attachments.saveImage({ data, mediaType, name: target.displayPath });
images.push({
attachmentId: ref.attachmentId,
mediaType: ref.mediaType,
bytes: ref.bytes,
width: ref.width,
height: ref.height,
path,
});
}
}
return { images, paths, urls };
},
}),
);
}
@@ -0,0 +1,279 @@
/**
* `bailian-cli-dsh/tool-managed-agent`: run a task on a Bailian-hosted managed
* agent, provisioned on demand, through `bl managed-agent run`.
*
* A plain tool rather than a `SubagentProvider`: in dsh's `web` profile every
* `tool-subagent` row is disabled in the host plane (delegation tools live in
* agent presets), and a subagent provider fixes one agent identity in config
* neither fits "the model describes an intent and a remote agent is created for
* it". As a tool the model calls it directly and fills `instructions` from the
* user's intent, so the remote agent's role is defined per task.
*
* The CLI does ensure+run in one step: it materializes (idempotently) a cloud
* agent + environment under the given `agent` name on first use and reuses them
* after, so no `agents.yaml` or prior `apply` is required. First use provisions
* cloud resources it may incur cost and take longer to start.
*
* Credentials: agentstudio is a pay-as-you-go DashScope API served ONLY on the
* workspace-scoped host `https://{workspace}.cn-beijing.maas.aliyuncs.com`
* (the plain dashscope origin and the TokenPlan gateway both 404 it, and a key
* only unlocks its own workspace's host). `bl` resolves the key as
* `--api-key` > `$DASHSCOPE_API_KEY` > the active config profile, but a
* profile's `base_url` is NOT paired with an env-resolved key an active
* TokenPlan profile therefore aims agentstudio at the TokenPlan gateway. So
* whenever this plugin resolves a key or an endpoint (row config, then launch
* env), it passes them explicitly; see {@link credentialFlags}. Endpoint
* resolution is `baseUrl`, then `$DASHSCOPE_BASE_URL`, then `workspaceId`
* composed into the workspace host (same for `$BAILIAN_WORKSPACE_ID`). With
* nothing resolvable here both halves are left to bl's own auth chain.
*
* @module bailian-cli-dsh/tool-managed-agent
*/
import type { Context } from "@deepseek-ai/cordis";
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
import { defineTool } from "@deepseek-ai/dsh-tools";
import type {} from "@deepseek-ai/dsh-tools";
import z from "@deepseek-ai/schemastery";
import { runBlJson } from "../shared/bl.ts";
import {
credentialFlags,
isTokenPlanEndpoint,
isTokenPlanKey,
tokenPlanKeyRejection,
workspaceEndpoint,
} from "../shared/credentials.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-tool-managed-agent";
/** Seams this plugin registers into. */
export const inject = ["tools", "subprocess"];
/** Default agent identity provisioned and reused across calls. */
const DEFAULT_AGENT = "dsh-remote-runner";
export interface Config {
/** Agent identity to create/reuse; distinct names get distinct remote agents. */
agent?: string;
/** Model for the remote agent. */
model?: string;
/** Pay-as-you-go DashScope key; defaults to `$DASHSCOPE_API_KEY`. */
apiKey?: string;
/**
* Workspace id the key belongs to; composed into the agentstudio host.
* Read from the console's top-right workspace switcher.
*/
workspaceId?: string;
/** Full agentstudio origin; wins over `workspaceId`. */
baseUrl?: string;
/** Cooperative budget; first-run provisioning of a cloud environment is slow. */
timeoutMs?: number;
}
export const Config: z<Config> = z.object({
agent: z.string().description("Remote agent identity to create/reuse."),
model: z.string().description("Model for the remote agent."),
apiKey: z
.string()
.role("secret")
.description(
"Pay-as-you-go DashScope key (sk-ws-); defaults to $DASHSCOPE_API_KEY. TokenPlan keys are rejected.",
),
workspaceId: z
.string()
.description(
"Workspace the key belongs to (console top-right switcher); defaults to $BAILIAN_WORKSPACE_ID. " +
"Composed into https://{workspaceId}.cn-beijing.maas.aliyuncs.com.",
),
baseUrl: z
.string()
.description(
"Full agentstudio origin; overrides workspaceId. Defaults to $DASHSCOPE_BASE_URL.",
),
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
});
const DEFAULT_TIMEOUT_MS = 600_000;
/**
* Resolve the managed-agent credentials: row config first, then the launch
* environment. Endpoint resolution: `baseUrl` (explicit origin) beats
* `workspaceId` (composed into the workspace-scoped host); env names mirror
* the same split. Agentstudio is only served on the workspace-scoped host, so
* an unresolved endpoint is left unset for bl to resolve (and the failure
* hints below explain the gap when bl cannot either).
*/
function resolveCredentials(ctx: Context, config: Config): { apiKey?: string; baseUrl?: string } {
const launchEnvironment = launchEnvironmentOf(ctx);
const env = (varName: string): string | undefined => {
const value = launchEnvironment.get(varName)?.value;
return value !== undefined && value.length > 0 ? value : undefined;
};
const apiKey = config.apiKey ?? env("DASHSCOPE_API_KEY");
const workspaceId = config.workspaceId ?? env("BAILIAN_WORKSPACE_ID");
const baseUrl =
config.baseUrl ??
env("DASHSCOPE_BASE_URL") ??
(workspaceId !== undefined ? workspaceEndpoint(workspaceId) : undefined);
return {
...(apiKey !== undefined ? { apiKey } : {}),
...(baseUrl !== undefined ? { baseUrl } : {}),
};
}
/** The `bl managed-agent run --output json` envelope: a session-event list. */
interface SessionRunResponse {
session_id?: string;
agent?: string;
events?: readonly { type?: string; content?: unknown; role?: string }[];
}
/** Assistant-visible text of a finished remote session. */
function assistantText(response: SessionRunResponse): string {
return (response.events ?? [])
.filter((event) => event.type === "message" && typeof event.content === "string")
.map((event) => event.content as string)
.join("\n")
.trim();
}
export function apply(ctx: Context, config: Config): void {
// Resolve credentials at boot so misconfigurations surface as one clear
// message instead of a cryptic 401/404 mid-task. This row is ENABLED BY
// DEFAULT, though, and TokenPlan-only setups legitimately keep
// $DASHSCOPE_API_KEY / $DASHSCOPE_BASE_URL aimed at the TokenPlan gateway
// for the vision/image tools — so a TokenPlan key or endpoint is not a boot
// error here: it becomes a per-call rejection with guidance, and everything
// else keeps working. (Opt-in plugins like bailian-memory reject at boot.)
const credentials = resolveCredentials(ctx, config);
const rejection =
credentials.apiKey !== undefined && isTokenPlanKey(credentials.apiKey)
? tokenPlanKeyRejection(name, "the managed-agent (agentstudio) API")
: credentials.baseUrl !== undefined && isTokenPlanEndpoint(credentials.baseUrl)
? `${name}: the resolved endpoint ${credentials.baseUrl} is the TokenPlan gateway, ` +
"which does not serve /api/v1/agentstudio (requests 404). Agentstudio lives on the " +
"workspace-scoped host: set `workspaceId` (the workspace your key belongs to, from " +
"the console's top-right switcher) or `baseUrl` in this row's config, or export " +
"BAILIAN_WORKSPACE_ID / DASHSCOPE_BASE_URL."
: undefined;
const credentialArgv = credentialFlags(credentials.apiKey, credentials.baseUrl);
ctx.tools.register(
defineTool({
name: "bailian_run_remote_task",
description:
"Run a task on a Bailian-hosted cloud agent. Use for long-running or isolated work you " +
"want executed remotely rather than in this session. A remote agent is created on demand " +
"(and reused) — describe the role it should play through `instructions`, and the concrete " +
"task through `task`. Returns the remote agent's final answer.",
parameters: {
task: {
type: "string",
required: true,
description: "The concrete task for the remote agent to carry out.",
},
instructions: {
type: "string",
description:
"Role/system instructions defining what the remote agent is good at. " +
"Defaults to a generic assistant.",
},
model: {
type: "string",
description: "Override the configured model for this task.",
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
answer: { type: "string", required: true },
sessionId: { type: "string", required: true },
},
},
render: (_args, value) => [{ type: "text", text: value.answer }],
},
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
async execute(args, exec) {
if (rejection !== undefined) throw new Error(rejection);
const argv = [
"managed-agent",
"run",
"--prompt",
args.task,
"--agent",
config.agent ?? DEFAULT_AGENT,
];
if (args.instructions !== undefined) argv.push("--instructions", args.instructions);
const model = args.model ?? config.model;
if (model !== undefined) argv.push("--model", model);
// Atomic credential pair: never let bl pair a key with its active
// profile's base_url (a TokenPlan profile 404s agentstudio).
argv.push(...credentialArgv);
let response: SessionRunResponse;
try {
response = await runBlJson<SessionRunResponse>(ctx, argv, {
cwd: exec.agent?.session.header.cwd ?? process.cwd(),
signal: exec.signal,
});
} catch (error) {
throw enrichProvisioningError(error, {
fellThroughToBlChain: credentials.apiKey === undefined,
endpointResolved: credentials.baseUrl !== undefined,
});
}
const answer = assistantText(response);
if (answer.length === 0) {
throw new Error("the remote agent produced no assistant output.");
}
return { answer, sessionId: response.session_id ?? "" };
},
}),
);
}
/**
* Attach an actionable hint to the classic misconfiguration signatures.
* Agentstudio is only served on the workspace-scoped host, and a key only
* unlocks its own workspace, so the three failure modes each get targeted
* guidance: 404 = endpoint is not a workspace host; 403 `Endpoint.
* AccessDenied` = right shape of host but the wrong workspace for this key;
* 401 = TokenPlan key on a pay-as-you-go API. Anything else passes through.
*/
function enrichProvisioningError(
error: unknown,
context: { fellThroughToBlChain: boolean; endpointResolved: boolean },
): unknown {
if (!(error instanceof Error)) return error;
const message = error.message;
const workspaceHint =
"Agentstudio is served only on the workspace-scoped host " +
"https://{workspaceId}.cn-beijing.maas.aliyuncs.com, and a key only unlocks its own " +
"workspace. Set `workspaceId` (the workspace your key belongs to, from the console's " +
"top-right switcher) or `baseUrl` on the bailian-tool-managed-agent row, or export " +
"BAILIAN_WORKSPACE_ID / DASHSCOPE_BASE_URL.";
let hint: string | undefined;
if (message.includes("Endpoint.AccessDenied") || message.includes("403")) {
hint = `The host is workspace-scoped but this key belongs to a different workspace. ${workspaceHint}`;
} else if (message.includes("404")) {
hint = context.endpointResolved
? `The endpoint rejected /api/v1/agentstudio. ${workspaceHint}`
: context.fellThroughToBlChain
? "No key/endpoint resolved from this row's config or the environment, so bl used its " +
"own auth chain — its active profile endpoint is not the workspace host agentstudio " +
`needs. ${workspaceHint}`
: `The endpoint rejected /api/v1/agentstudio. ${workspaceHint}`;
} else if (message.includes("401")) {
hint =
"The managed-agent API rejected the key. It needs a pay-as-you-go key (sk-ws-); " +
"TokenPlan keys (sk-sp-) only serve the TokenPlan LLM gateway.";
}
if (hint === undefined) return error;
error.message = `${error.message}\n${hint}`;
return error;
}
+135
View File
@@ -0,0 +1,135 @@
/**
* `bailian-cli-dsh/tool-vision`: image and video understanding through
* `bl vision describe` (Qwen-VL).
*
* The tool returns TEXT, never an `ImageBlock` that is deliberate. dsh gates
* image content on the active route's declared input modalities in two places
* before a plugin ever sees it (the Web UI paste pre-check and `read_image`),
* so a text-only main model such as DeepSeek cannot receive pictures at all.
* Handing back a description instead gives those routes vision indirectly.
* A genuinely multimodal route does not need this tool and should paste images
* directly.
*
* @module bailian-cli-dsh/tool-vision
*/
import type { Context } from "@deepseek-ai/cordis";
import { defineTool } from "@deepseek-ai/dsh-tools";
import type {} from "@deepseek-ai/dsh-tools";
import z from "@deepseek-ai/schemastery";
import { runBlJson } from "../shared/bl.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-tool-vision";
/** Seams this plugin registers into. */
export const inject = ["tools", "subprocess"];
export interface Config {
/** Vision model passed to `bl vision describe --model`. */
model?: string;
/** Cooperative budget; video understanding uploads and is slow. */
timeoutMs?: number;
}
export const Config: z<Config> = z.object({
model: z.string().description("Vision model; defaults to the CLI's own default."),
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
});
const DEFAULT_TIMEOUT_MS = 180_000;
/** The OpenAI-shaped body `bl vision describe --output json` passes through. */
interface VisionResponse {
model?: string;
request_id?: string;
choices?: readonly {
message?: { content?: unknown };
}[];
}
/** Chat content is a string or an array of typed parts; keep only the text. */
function readContent(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.map((part) =>
typeof part === "object" && part !== null && "text" in part
? String((part as { text: unknown }).text)
: "",
)
.join("")
.trim();
}
export function apply(ctx: Context, config: Config): void {
ctx.tools.register(
defineTool({
name: "bailian_vision_describe",
description:
"Understand an image or video using Aliyun Bailian's Qwen-VL models. " +
"Accepts a local file path or a URL and returns a text description, so it works " +
"even when the active model cannot take image input. Ask a specific question " +
"through `prompt` (for example OCR, chart reading, or object identification) " +
"instead of relying on the generic default.",
parameters: {
image: {
type: "string",
description: "Local image path or http(s)/oss URL. Provide this or `video`.",
},
video: {
type: "array",
items: { type: "string" },
description:
"Video file paths or URLs (mp4/mov/avi/mkv/webm). Local files are uploaded first.",
},
prompt: {
type: "string",
description: "Question about the content. Defaults to a plain description request.",
},
model: {
type: "string",
description: "Override the configured vision model.",
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
description: { type: "string", required: true },
model: { type: "string", required: true },
},
},
render: (_args, value) => [{ type: "text", text: value.description }],
},
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
isConcurrencySafe: () => true,
async execute(args, exec) {
const videos = args.video ?? [];
if (args.image === undefined && videos.length === 0) {
throw new Error("bailian_vision_describe requires `image` or `video`.");
}
const argv = ["vision", "describe"];
if (args.image !== undefined) argv.push("--image", args.image);
for (const video of videos) argv.push("--video", video);
if (args.prompt !== undefined) argv.push("--prompt", args.prompt);
const model = args.model ?? config.model;
if (model !== undefined) argv.push("--model", model);
const response = await runBlJson<VisionResponse>(ctx, argv, {
cwd: exec.agent?.session.header.cwd ?? process.cwd(),
signal: exec.signal,
});
const description = readContent(response.choices?.[0]?.message?.content);
if (description.length === 0) {
throw new Error("Qwen-VL returned an empty description.");
}
return { description, model: response.model ?? model ?? "" };
},
}),
);
}
+171
View File
@@ -0,0 +1,171 @@
/**
* `bailian-cli-dsh/web-search-rag`: registers a Bailian knowledge-base
* `WebSearchProvider` with `ctx.web`.
*
* Retrieval is modelled as a search provider rather than a bespoke tool so the
* model reaches private corpora through the `web_search` it already knows
* no new tool, no new prompting. Calls go straight to DashScope because the
* seam needs per-call control the CLI does not surface.
*
* One instance serves one knowledge base: `WebSearchRequest` carries only
* `query` and `maxResults`, so the agent id has to come from config. Insert
* additional rows with distinct ids to expose more than one.
*
* @module bailian-cli-dsh/web-search-rag
*/
import type { Context } from "@deepseek-ai/cordis";
import type {
WebSearchProvider,
WebSearchRequest,
WebSearchResult,
WebSearchSource,
} from "@deepseek-ai/dsh-web";
import { WebError } from "@deepseek-ai/dsh-web";
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
import z from "@deepseek-ai/schemastery";
import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts";
import { dashScopeFetch, resolveApiKey } from "../shared/http.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-web-search-rag";
/** The web seam this provider registers into. */
export const inject = ["web"];
/** Stable provider id; pin it as `searchProvider` to disambiguate. */
export const BAILIAN_KB_PROVIDER_ID = "bailian-kb";
export interface Config {
/** DashScope key; falls back to `DASHSCOPE_API_KEY`. */
apiKey?: string;
/** Workspace id; also the retrieval host prefix. Falls back to `BAILIAN_WORKSPACE_ID`. */
workspaceId?: string;
/** Retrieval service id from the console's knowledge retrieval page. */
agentId?: string;
/** Default upper bound when the caller sets none. */
maxResults?: number;
}
export const Config: z<Config> = z.object({
apiKey: z
.string()
.role("secret")
.description(
"Pay-as-you-go DashScope key (sk-ws-); defaults to $DASHSCOPE_API_KEY. TokenPlan keys are rejected.",
),
workspaceId: z.string().description("Bailian workspace id; defaults to $BAILIAN_WORKSPACE_ID."),
agentId: z.string().description("Retrieval service (agent) id identifying the knowledge base."),
maxResults: z.natural().description("Default source cap when the caller sets none."),
});
const DEFAULT_MAX_RESULTS = 10;
interface KnowledgeSearchNode {
score?: number;
text?: string;
metadata?: {
title?: string;
doc_id?: string;
doc_name?: string;
doc_url?: string;
page_number?: number;
};
}
interface KnowledgeSearchResponse {
data?: { total?: number; nodes?: readonly KnowledgeSearchNode[] };
}
export interface BailianKbProviderOptions {
apiKey: string;
workspaceId: string;
agentId: string;
maxResults: number;
}
function isAbort(error: unknown): boolean {
return error instanceof DOMException && error.name === "AbortError";
}
/** The seam requires a URL; documents without one still deserve a stable identity. */
function sourceUrl(node: KnowledgeSearchNode, index: number): string {
const url = node.metadata?.doc_url;
if (url !== undefined && url.length > 0) return url;
return `bailian-kb://${node.metadata?.doc_id ?? `node-${index}`}`;
}
export class BailianKbSearchProvider implements WebSearchProvider {
readonly id = BAILIAN_KB_PROVIDER_ID;
constructor(private readonly options: BailianKbProviderOptions) {}
available(): boolean {
return (
this.options.apiKey.length > 0 &&
this.options.workspaceId.length > 0 &&
this.options.agentId.length > 0
);
}
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
const limit = request.maxResults ?? this.options.maxResults;
let response: KnowledgeSearchResponse;
try {
response = await dashScopeFetch<KnowledgeSearchResponse>({
url: `https://${this.options.workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`,
method: "POST",
apiKey: this.options.apiKey,
body: { query: request.query, agent_id: this.options.agentId },
signal,
});
} catch (error) {
if (isAbort(error))
throw new WebError("knowledge base search aborted", "WEB_ABORTED", { cause: error });
const reason = error instanceof Error ? error.message : String(error);
throw new WebError(`knowledge base search failed: ${reason}`, "WEB_PROVIDER_ERROR", {
cause: error,
});
}
const nodes = (response.data?.nodes ?? []).slice(0, limit);
const sources: WebSearchSource[] = nodes.map((node, index) => {
const metadata = node.metadata ?? {};
const title = metadata.doc_name ?? metadata.title;
const text = node.text ?? "";
return {
url: sourceUrl(node, index),
...(title !== undefined ? { title } : {}),
...(text.length > 0 ? { snippet: text } : {}),
};
});
const content = nodes
.map((node) => node.text ?? "")
.filter((text) => text.length > 0)
.join("\n\n");
// Truncation is the seam's job; report what this provider returned.
return { ...(content.length > 0 ? { content } : {}), sources, truncated: false };
}
}
export function apply(ctx: Context, config: Config): void {
const apiKey = resolveApiKey(ctx, config.apiKey);
// A TokenPlan key would register a provider that looks available and then 401s
// on every search; reject it at boot instead. An absent key stays soft:
// `available()` returns false and dsh falls back to another provider.
if (apiKey !== undefined && isTokenPlanKey(apiKey)) {
throw new Error(tokenPlanKeyRejection(name, "the knowledge-base API"));
}
const workspaceId =
config.workspaceId ?? launchEnvironmentOf(ctx).get("BAILIAN_WORKSPACE_ID")?.value ?? "";
ctx.web.registerSearchProvider(
new BailianKbSearchProvider({
apiKey: apiKey ?? "",
workspaceId,
agentId: config.agentId ?? "",
maxResults: config.maxResults ?? DEFAULT_MAX_RESULTS,
}),
);
}
+57
View File
@@ -0,0 +1,57 @@
import { expect, test } from "vite-plus/test";
import {
credentialFlags,
DASHSCOPE_DEFAULT_BASE_URL,
isTokenPlanEndpoint,
isTokenPlanKey,
workspaceEndpoint,
} from "../src/shared/credentials.ts";
// 行为锁定:两类 Key(sk-sp- TokenPlan / sk-ws- 按量付费)不可混用,三个直连服务
// 模块(memory / RAG / managed-agent)都会拦下 TokenPlan Key,而不是等请求时
// 拿到难懂的 401/404。managed-agent 的凭证两半独立下发:解析出 key 就显式
// --api-key(不让 bl 用活动 profile 的 key),解析出端点就显式 --base-url
// (不让 bl 用活动 profile 的端点)。agentstudio 只在工作空间前缀主机上提供,
// 因此绝不存在"默认端点"——工作空间未知就是配置缺口,该报错而不是猜。
test("isTokenPlanKey classifies by prefix", () => {
expect(isTokenPlanKey("sk-sp-abc123")).toBe(true);
expect(isTokenPlanKey("sk-ws-abc123")).toBe(false);
expect(isTokenPlanKey("")).toBe(false);
});
test("isTokenPlanEndpoint classifies the gateway host", () => {
expect(isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com")).toBe(true);
expect(
isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"),
).toBe(true);
expect(isTokenPlanEndpoint(DASHSCOPE_DEFAULT_BASE_URL)).toBe(false);
expect(isTokenPlanEndpoint(workspaceEndpoint("llm-x"))).toBe(false);
// 不可解析的 URL 交给后续请求自己报错,这里只做形状分类。
expect(isTokenPlanEndpoint("not a url")).toBe(false);
});
test("workspaceEndpoint composes the workspace-scoped agentstudio host", () => {
expect(workspaceEndpoint("llm-kpgesh4vqzf5gzv9")).toBe(
"https://llm-kpgesh4vqzf5gzv9.cn-beijing.maas.aliyuncs.com",
);
expect(workspaceEndpoint("ws_abc")).toBe("https://ws_abc.cn-beijing.maas.aliyuncs.com");
});
test("credentialFlags: each resolved half ships independently, no defaults", () => {
expect(credentialFlags(undefined, undefined)).toEqual([]);
expect(credentialFlags("", "")).toEqual([]);
// 只有 key:端点留给 bl 解析,绝不塞一个会 404 的默认主机。
expect(credentialFlags("sk-ws-abc", undefined)).toEqual(["--api-key", "sk-ws-abc"]);
// 只有端点:也下发,key 留给 bl 的 auth chain。
expect(credentialFlags(undefined, "https://ws.example.com")).toEqual([
"--base-url",
"https://ws.example.com",
]);
expect(credentialFlags("sk-ws-abc", "https://ws.example.com")).toEqual([
"--base-url",
"https://ws.example.com",
"--api-key",
"sk-ws-abc",
]);
});
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["es2023"],
"moduleDetection": "force",
"module": "nodenext",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"noUnusedLocals": true,
"declaration": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
}
}
+27
View File
@@ -0,0 +1,27 @@
import { defineConfig } from "vite-plus";
export default defineConfig({
pack: {
// One entry per `exports` subpath: each dsh plugin row imports its own
// module specifier, so they cannot share a bundle.
entry: [
"src/index.ts",
"src/tool-vision/index.ts",
"src/tool-image/index.ts",
"src/tool-managed-agent/index.ts",
"src/web-search-rag/index.ts",
"src/memory/index.ts",
],
minify: true,
dts: {
tsgo: true,
},
},
lint: {
options: {
typeAware: true,
typeCheck: true,
},
},
fmt: {},
});
+1879 -340
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -24,6 +24,10 @@ catalogMode: prefer
overrides:
vite: "catalog:"
vitest: "catalog:"
# The @deepseek-ai/dsh-* rc line (used only by bailian-cli-dsh) peers on
# packages that were never published: dsh-type-meta, dsh-environment,
# dsh-tasks. Auto-installing peers therefore 404s the whole workspace.
autoInstallPeers: false
peerDependencyRules:
allowAny:
- vite
+26 -22
View File
@@ -36,7 +36,11 @@ Use this index for the skill-scoped quick index and global flags.
| `bl memory delete` | Delete a memory node | [memory.md](memory.md) |
| `bl memory list` | List memory nodes for a user | [memory.md](memory.md) |
| `bl memory profile create` | Create a user profile schema for memory profiling | [memory.md](memory.md) |
| `bl memory profile delete` | Delete a profile schema | [memory.md](memory.md) |
| `bl memory profile detail` | Show a profile schema and its attribute IDs | [memory.md](memory.md) |
| `bl memory profile get` | Get user profile by schema ID and user ID | [memory.md](memory.md) |
| `bl memory profile list` | List profile schemas | [memory.md](memory.md) |
| `bl memory profile update` | Update a profile schema's name, description, or attributes | [memory.md](memory.md) |
| `bl memory search` | Search memory nodes by query or messages | [memory.md](memory.md) |
| `bl memory update` | Update a memory node content | [memory.md](memory.md) |
| `bl model list` | Browse model families or show detailed model info in the Bailian model marketplace | [model.md](model.md) |
@@ -71,28 +75,28 @@ Use this index for the skill-scoped quick index and global flags.
## By group
| Group | Commands | Reference |
| ------------ | ---------------------------------------------------------------------------- | ------------------------------ |
| `advisor` | `recommend` | [advisor.md](advisor.md) |
| `app` | `call`, `list` | [app.md](app.md) |
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
| `console` | `call` | [console.md](console.md) |
| `file` | `upload` | [file.md](file.md) |
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
| `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) |
| `model` | `list` | [model.md](model.md) |
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
| `search` | `web` | [search.md](search.md) |
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `text` | `chat` | [text.md](text.md) |
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
| `update` | `(root)` | [update.md](update.md) |
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
| Group | Commands | Reference |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `advisor` | `recommend` | [advisor.md](advisor.md) |
| `app` | `call`, `list` | [app.md](app.md) |
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
| `console` | `call` | [console.md](console.md) |
| `file` | `upload` | [file.md](file.md) |
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
| `memory` | `add`, `delete`, `list`, `profile create`, `profile delete`, `profile detail`, `profile get`, `profile list`, `profile update`, `search`, `update` | [memory.md](memory.md) |
| `model` | `list` | [model.md](model.md) |
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
| `search` | `web` | [search.md](search.md) |
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `text` | `chat` | [text.md](text.md) |
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
| `update` | `(root)` | [update.md](update.md) |
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
## Global flags
+178 -43
View File
@@ -7,15 +7,19 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| -------------------------- | ------------------------------------------------- |
| `bl memory add` | Add memory from messages or custom content |
| `bl memory delete` | Delete a memory node |
| `bl memory list` | List memory nodes for a user |
| `bl memory profile create` | Create a user profile schema for memory profiling |
| `bl memory profile get` | Get user profile by schema ID and user ID |
| `bl memory search` | Search memory nodes by query or messages |
| `bl memory update` | Update a memory node content |
| Command | Description |
| -------------------------- | ---------------------------------------------------------- |
| `bl memory add` | Add memory from messages or custom content |
| `bl memory delete` | Delete a memory node |
| `bl memory list` | List memory nodes for a user |
| `bl memory profile create` | Create a user profile schema for memory profiling |
| `bl memory profile delete` | Delete a profile schema |
| `bl memory profile detail` | Show a profile schema and its attribute IDs |
| `bl memory profile get` | Get user profile by schema ID and user ID |
| `bl memory profile list` | List profile schemas |
| `bl memory profile update` | Update a profile schema's name, description, or attributes |
| `bl memory search` | Search memory nodes by query or messages |
| `bl memory update` | Update a memory node content |
## Command details
@@ -29,15 +33,17 @@ Index: [index.md](index.md)
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------------------------------------- |
| `--user-id <id>` | string | yes | User ID (required) |
| `--messages <json>` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] |
| `--content <text>` | string | no | Custom content text to memorize |
| `--profile-schema <id>` | string | no | Profile schema ID for user profiling |
| `--memory-library-id <id>` | string | no | Memory library ID (isolate memory space) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------------------------------ |
| `--user-id <id>` | string | yes | User ID (required) |
| `--messages <json>` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] |
| `--content <text>` | string | no | Custom content text to memorize |
| `--profile-schema <id>` | string | no | Profile schema ID for user profiling |
| `--memory-library-id <id>` | string | no | Memory library ID (isolate memory space) |
| `--project-id <id>` | string | no | Memory extraction rule ID (defaults to the library's default rule) |
| `--meta-data <json>` | string | no | Custom metadata JSON object: {"location":"Beijing"} |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -53,6 +59,10 @@ bl memory add --user-id user1 --messages '[{"role":"user","content":"I like trav
bl memory add --user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx
```
```bash
bl memory add --user-id user1 --content "Lives in Beijing" --meta-data '{"source":"onboarding"}'
```
### `bl memory delete`
| Field | Value |
@@ -87,14 +97,15 @@ bl memory delete --node-id node_xxx --user-id user1
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------ |
| `--user-id <id>` | string | yes | User ID (required) |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------------------------------ |
| `--user-id <id>` | string | yes | User ID (required) |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--project-id <id>` | string | no | Memory extraction rule ID (defaults to the library's default rule) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -130,6 +141,52 @@ bl memory list --user-id user1 --page-size 20 --page 2
bl memory profile create --name "user_basic" --attributes '[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]'
```
### `bl memory profile delete`
| Field | Value |
| --------------- | --------------------------------------------------- |
| **Name** | `memory profile delete` |
| **Description** | Delete a profile schema |
| **Usage** | `bl memory profile delete --schema-id <id> [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
```bash
bl memory profile delete --schema-id schema_xxx
```
### `bl memory profile detail`
| Field | Value |
| --------------- | --------------------------------------------------- |
| **Name** | `memory profile detail` |
| **Description** | Show a profile schema and its attribute IDs |
| **Usage** | `bl memory profile detail --schema-id <id> [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
```bash
bl memory profile detail --schema-id schema_xxx
```
### `bl memory profile get`
| Field | Value |
@@ -153,6 +210,72 @@ bl memory profile create --name "user_basic" --attributes '[{"name":"age","descr
bl memory profile get --schema-id schema_xxx --user-id user1
```
### `bl memory profile list`
| Field | Value |
| --------------- | -------------------------------- |
| **Name** | `memory profile list` |
| **Description** | List profile schemas |
| **Usage** | `bl memory profile list [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------ |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
```bash
bl memory profile list
```
```bash
bl memory profile list --page-size 20 --page 2
```
### `bl memory profile update`
| Field | Value |
| --------------- | -------------------------------------------------------------------------------------------- |
| **Name** | `memory profile update` |
| **Description** | Update a profile schema's name, description, or attributes |
| **Usage** | `bl memory profile update --schema-id <id> [--name <name>] [--attribute-ops <json>] [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--name <name>` | string | no | New schema name |
| `--description <text>` | string | no | New schema description |
| `--attribute-ops <json>` | string | no | Attribute operations JSON array: [{"op":"add","name":"plan"},{"op":"delete","attribute_id":"attr_1"}] |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Attribute IDs for update/delete operations come from `memory profile detail`.
#### Examples
```bash
bl memory profile update --schema-id schema_xxx --name "user_basic_v2"
```
```bash
bl memory profile update --schema-id schema_xxx --attribute-ops '[{"op":"add","name":"plan","description":"subscription plan"}]'
```
```bash
bl memory profile update --schema-id schema_xxx --attribute-ops '[{"op":"delete","attribute_id":"attr_1"}]'
```
### `bl memory search`
| Field | Value |
@@ -163,15 +286,21 @@ bl memory profile get --schema-id schema_xxx --user-id user1
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | -------------------------------------------- |
| `--user-id <id>` | string | yes | User ID (required) |
| `--query <text>` | string | no | Search query text |
| `--messages <json>` | string | no | Messages JSON array for context-based search |
| `--top-k <n>` | number | no | Number of results to return (default: 10) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ---------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `--user-id <id>` | string | yes | User ID (required) |
| `--query <text>` | string | no | Search query text |
| `--messages <json>` | string | no | Messages JSON array for context-based search |
| `--top-k <n>` | number | no | Number of results to return (default: 10) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--project-ids <id>` | array | no | Memory extraction rule ID for hybrid retrieval (repeatable) |
| `--min-score <n>` | number | no | Minimum similarity score, 0-1 (default: 0.3) |
| `--enable-rerank <bool>` | boolean | no | Rerank results. Also selects the billing tier: false bills lite, true bills pro (~50x). (default: true) |
| `--plan-version <lite\|pro>` | string | no | Documented billing tier. The service currently honors --enable-rerank instead, so prefer that flag |
| `--enable-judge <bool>` | boolean | no | Enable the intent-discrimination callback (default: false) |
| `--enable-rewrite <bool>` | boolean | no | Enable query rewriting (default: false) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -183,6 +312,10 @@ bl memory search --user-id user1 --query "programming preferences"
bl memory search --user-id user1 --messages '[{"role":"user","content":"recommend a book"}]' --top-k 5
```
```bash
bl memory search --user-id user1 --query "preferences" --enable-rerank false --min-score 0.5
```
### `bl memory update`
| Field | Value |
@@ -193,14 +326,16 @@ bl memory search --user-id user1 --messages '[{"role":"user","content":"recommen
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------ |
| `--node-id <id>` | string | yes | Memory node ID (required) |
| `--user-id <id>` | string | yes | User ID (required) |
| `--content <text>` | string | yes | New content for the memory node (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (non-default library) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ---------------------------- | ------ | -------- | ---------------------------------------------------------------------- |
| `--node-id <id>` | string | yes | Memory node ID (required) |
| `--user-id <id>` | string | yes | User ID (required) |
| `--content <text>` | string | yes | New content for the memory node (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (non-default library) |
| `--timestamp <unix-seconds>` | number | no | When the remembered event happened (default: now) |
| `--meta-data <json>` | string | no | Custom metadata JSON object, merged incrementally: {"source":"manual"} |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
+23 -22
View File
@@ -9,31 +9,32 @@ Use this index for the skill-scoped quick index and global flags.
## Quick index
| Command | Description | Detail |
| --------------------------------- | ------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
| Command | Description | Detail |
| --------------------------------- | -------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent run` | Provision (if needed) a cloud agent and run a task in one step | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
## By group
| Group | Commands | Reference |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) |
| Group | Commands | Reference |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `run`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) |
## Global flags
@@ -7,25 +7,26 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| --------------------------------- | ------------------------------------------------------------- |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | Create a new agents.yaml template |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure |
| `bl managed-agent session create` | Create a new session for an agent |
| `bl managed-agent session delete` | Delete a session |
| `bl managed-agent session events` | List event history for a session |
| `bl managed-agent session get` | Get details of a session |
| `bl managed-agent session list` | List sessions from the provider |
| `bl managed-agent session run` | Create a session, send a message, and stream the response |
| `bl managed-agent session send` | Send a message to an existing session and stream the response |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog |
| `bl managed-agent state import` | Import an existing remote resource into agents state |
| `bl managed-agent state list` | List resources tracked in agents state |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | Show details of a resource in agents state |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) |
| Command | Description |
| --------------------------------- | -------------------------------------------------------------- |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | Create a new agents.yaml template |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure |
| `bl managed-agent run` | Provision (if needed) a cloud agent and run a task in one step |
| `bl managed-agent session create` | Create a new session for an agent |
| `bl managed-agent session delete` | Delete a session |
| `bl managed-agent session events` | List event history for a session |
| `bl managed-agent session get` | Get details of a session |
| `bl managed-agent session list` | List sessions from the provider |
| `bl managed-agent session run` | Create a session, send a message, and stream the response |
| `bl managed-agent session send` | Send a message to an existing session and stream the response |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog |
| `bl managed-agent state import` | Import an existing remote resource into agents state |
| `bl managed-agent state list` | List resources tracked in agents state |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | Show details of a resource in agents state |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) |
## Command details
@@ -52,6 +53,7 @@ Index: [index.md](index.md)
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -86,6 +88,7 @@ bl managed-agent apply --provider bailian --yes
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -152,6 +155,7 @@ bl managed-agent init --provider all
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- --no-refresh and --dry-run plan offline from local config and state: no remote requests, no state writes, provider keys are not checked.
@@ -170,6 +174,44 @@ bl managed-agent plan --provider bailian
bl managed-agent plan --no-refresh
```
### `bl managed-agent run`
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent run` |
| **Description** | Provision (if needed) a cloud agent and run a task in one step |
| **Usage** | `bl managed-agent run --prompt <text> [--instructions <text>] [--model <id>] [--agent <name>]` |
#### Flags
| Flag | Type | Required | Description |
| ----------------------- | ------ | -------- | -------------------------------------------------------------------------- |
| `--prompt <text>` | string | yes | Task to run (required) |
| `--instructions <text>` | string | no | Role/system instructions for the remote agent (default: generic assistant) |
| `--model <id>` | string | no | Model for the remote agent (default: qwen3.8-max) |
| `--agent <name>` | string | no | Agent identity to create/reuse (default: dsh-remote-runner) |
| `--no-stream` | switch | no | Use polling instead of SSE streaming |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.
#### Examples
```bash
bl managed-agent run --prompt "Summarize the latest AI news"
```
```bash
bl managed-agent run --prompt "Audit this dependency tree" --instructions "You are a security expert" --model qwen3.8-max
```
### `bl managed-agent session create`
| Field | Value |
@@ -195,6 +237,7 @@ bl managed-agent plan --no-refresh
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -233,6 +276,7 @@ bl managed-agent session create --agent assistant --title 'debug run'
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -265,6 +309,7 @@ bl managed-agent session delete --session-id sess_abc123
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -299,6 +344,7 @@ bl managed-agent session events --session-id sess_abc123 --all
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -330,6 +376,7 @@ bl managed-agent session get --session-id sess_abc123
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -374,6 +421,7 @@ bl managed-agent session list --all
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- --output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.
@@ -411,6 +459,7 @@ bl managed-agent session run --agent assistant --prompt "summarize this repo"
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -441,6 +490,7 @@ bl managed-agent session send --session-id sess_abc123 --message "continue"
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Providers without a skill listing API (e.g. ark) return an empty list.
@@ -487,6 +537,7 @@ bl managed-agent skill-list --source custom --provider bailian
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.