mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 074fb58329 | |||
| 98acbd7341 | |||
| 125be085e5 | |||
| 186c500ca4 | |||
| 9ba4a9d1a3 | |||
| 50ed680ade | |||
| f919ebae3c | |||
| 9e9911aa86 |
@@ -52,3 +52,6 @@ packages/cli/scene/**/outputs/
|
||||
|
||||
# Local scratch / plan drafts (never commit)
|
||||
.scratch/
|
||||
|
||||
# pnpm pack output
|
||||
*.tgz
|
||||
|
||||
@@ -30,6 +30,10 @@ import {
|
||||
memoryDelete,
|
||||
memoryProfileCreate,
|
||||
memoryProfileGet,
|
||||
memoryProfileList,
|
||||
memoryProfileDetail,
|
||||
memoryProfileUpdate,
|
||||
memoryProfileDelete,
|
||||
knowledgeRetrieve,
|
||||
knowledgeSearch,
|
||||
knowledgeChat,
|
||||
@@ -84,6 +88,8 @@ import {
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
tokenPlanAddMember,
|
||||
tokenPlanPersonalUsage,
|
||||
tokenPlanPersonalKey,
|
||||
workspaceInit,
|
||||
pluginInstall,
|
||||
pluginLink,
|
||||
@@ -98,6 +104,7 @@ import {
|
||||
managedAgentValidate,
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentRun,
|
||||
managedAgentDestroy,
|
||||
managedAgentStateList,
|
||||
managedAgentStateShow,
|
||||
@@ -149,6 +156,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,
|
||||
@@ -203,6 +214,8 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"token-plan create-key": tokenPlanCreateKey,
|
||||
"token-plan assign-seats": tokenPlanAssignSeats,
|
||||
"token-plan add-member": tokenPlanAddMember,
|
||||
"token-plan personal-usage": tokenPlanPersonalUsage,
|
||||
"token-plan personal-key": tokenPlanPersonalKey,
|
||||
"workspace init": workspaceInit,
|
||||
"plugin install": pluginInstall,
|
||||
"plugin link": pluginLink,
|
||||
@@ -217,6 +230,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,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);
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const GET_KEY_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/api-keys/getKeyByUid";
|
||||
|
||||
export default defineCommand({
|
||||
description: "Get the personal-edition TokenPlan API key (masked) for the current account",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: {},
|
||||
exampleArgs: [""],
|
||||
async run(ctx) {
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const result = await ctx.client.console(GET_KEY_API, {});
|
||||
emitResult(result, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
||||
const SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription";
|
||||
const ADDON_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/addon/summary";
|
||||
|
||||
const COMMODITY_CN = "sfm_tokenplansolo_public_cn";
|
||||
const COMMODITY_INTL = "sfm_tokenplansolo_public_intl";
|
||||
const ADDON_CN = "sfm_tokenplansoloaddon_public_cn";
|
||||
const ADDON_INTL = "sfm_tokenplansoloaddon_public_intl";
|
||||
|
||||
function nested(obj: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
|
||||
const val = obj[key];
|
||||
return val && typeof val === "object" && !Array.isArray(val)
|
||||
? (val as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Unwrap the console gateway `data.DataV2.data.data` envelope to the business payload. */
|
||||
function extract(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const data = nested(result, "data");
|
||||
if (!data) return result;
|
||||
const dataV2 = nested(data, "DataV2");
|
||||
if (dataV2) {
|
||||
const inner = nested(dataV2, "data");
|
||||
const innerData = inner ? nested(inner, "data") : undefined;
|
||||
return innerData ?? inner ?? dataV2;
|
||||
}
|
||||
return nested(data, "data") ?? data;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description:
|
||||
"Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits)",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: {},
|
||||
exampleArgs: [""],
|
||||
async run(ctx) {
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const intl = settings.consoleSite === "international";
|
||||
|
||||
const [usage, subscription, addon] = await Promise.all([
|
||||
ctx.client.console(USAGE_API, {}),
|
||||
ctx.client.console(SUBSCRIPTION_API, {
|
||||
queryInstanceInfoRequest: { commodityCode: intl ? COMMODITY_INTL : COMMODITY_CN },
|
||||
}),
|
||||
ctx.client.console(ADDON_API, { commodityCode: intl ? ADDON_INTL : ADDON_CN }),
|
||||
]);
|
||||
|
||||
emitResult(
|
||||
{
|
||||
usage: extract(usage as Record<string, unknown>),
|
||||
subscription: extract(subscription as Record<string, unknown>),
|
||||
addonSummary: extract(addon as Record<string, unknown>),
|
||||
},
|
||||
format,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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";
|
||||
@@ -91,10 +95,13 @@ export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats.
|
||||
export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts";
|
||||
export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts";
|
||||
export { default as tokenPlanAddMember } from "./commands/token-plan/add-member.ts";
|
||||
export { default as tokenPlanPersonalUsage } from "./commands/token-plan/personal-usage.ts";
|
||||
export { default as tokenPlanPersonalKey } from "./commands/token-plan/personal-key.ts";
|
||||
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 = {
|
||||
@@ -158,6 +162,8 @@ export const TOKEN_PLAN_ROUTES: E2eRouteExports = {
|
||||
"token-plan create-key": "tokenPlanCreateKey",
|
||||
"token-plan assign-seats": "tokenPlanAssignSeats",
|
||||
"token-plan add-member": "tokenPlanAddMember",
|
||||
"token-plan personal-usage": "tokenPlanPersonalUsage",
|
||||
"token-plan personal-key": "tokenPlanPersonalKey",
|
||||
};
|
||||
|
||||
export const SKILL_ROUTES: E2eRouteExports = {
|
||||
@@ -173,6 +179,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",
|
||||
|
||||
@@ -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) ----
|
||||
|
||||
@@ -13,6 +13,7 @@ export {
|
||||
memoryNodePath,
|
||||
memorySearchPath,
|
||||
mcpWebSearchPath,
|
||||
profileSchemaItemPath,
|
||||
profileSchemaPath,
|
||||
speechRecognizePath,
|
||||
speechSynthesizePath,
|
||||
|
||||
@@ -199,7 +199,15 @@ export function buildSources(flags: Partial<SourceFlags>): ResolutionSources {
|
||||
const raw = readRawConfigObject();
|
||||
const configExplicit = flags.config !== undefined;
|
||||
const activeConfigName = readStoredActiveConfigName(raw, !configExplicit);
|
||||
const configName = configExplicit ? normalizeConfigName(flags.config) : activeConfigName;
|
||||
// Config selection: --config flag > BAILIAN_CONFIG env > persisted active_config.
|
||||
// The env lets a host (e.g. dsh) pin a named profile for all child `bl`
|
||||
// calls without rewriting --config or the user's active_config.
|
||||
const envConfig = process.env.BAILIAN_CONFIG;
|
||||
const configName = configExplicit
|
||||
? normalizeConfigName(flags.config)
|
||||
: envConfig
|
||||
? normalizeConfigName(envConfig)
|
||||
: activeConfigName;
|
||||
return {
|
||||
flags,
|
||||
file: parseConfigFile(readRawConfigBlock(raw, configName)),
|
||||
|
||||
@@ -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 }>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# build artifacts (regenerated by `pnpm build`)
|
||||
client.bundle.js
|
||||
dist/
|
||||
*.tgz
|
||||
@@ -0,0 +1,232 @@
|
||||
# bailian-cli-dsh
|
||||
|
||||
把阿里云百炼(Model Studio)的能力接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的 profile bundle。
|
||||
|
||||
本包提供两项能力:
|
||||
|
||||
| 能力 | 说明 |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Bailian 设置页** | 通用的百炼凭证配置(AK/SK 存入 `dsh` bl profile + DashScope API Key)+ TokenPlan 用量展示 + 记忆库配置 + 新会话欢迎页 |
|
||||
| **跨会话长期记忆** | 自动检索注入 + 自动落库,模型可主动 search/add/list。按量计费,默认停用 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- Node ≥ 22.19(`dsh` 的要求)
|
||||
- `bl`(用量展示通过子进程调用 `bl console call`)
|
||||
|
||||
```sh
|
||||
npm install -g bailian-cli
|
||||
```
|
||||
|
||||
- **阿里云 AK/SK**(AccessKey ID + AccessKey Secret)—— 用于控制台鉴权,查询用量信息。在 webui 设置页填入即可,无需环境变量。
|
||||
- **DashScope API Key**(`sk-` 前缀,按量付费)—— 用于记忆库等 DashScope API 调用。在设置页「凭证配置」填入,与 AK/SK 并列为通用凭证。
|
||||
|
||||
获取方式:[阿里云控制台 → AccessKey 管理](https://ram.console.aliyun.com/manage/ak)
|
||||
|
||||
---
|
||||
|
||||
## 2. 安装到 `web` profile
|
||||
|
||||
`npx @deepseek-ai/dsh web` 是 `dsh --profile web` 的别名,配置目录是 `~/.dsh/profiles/web/`。
|
||||
|
||||
```sh
|
||||
pnpm -F bailian-cli-dsh build # vp pack(host)+ esbuild(client.bundle.js)
|
||||
cd packages/dsh && pnpm pack
|
||||
|
||||
npx @deepseek-ai/dsh plugin --profile web add /absolute/path/to/bailian-cli-dsh-<version>.tgz
|
||||
```
|
||||
|
||||
确认 bailian 行都在:
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep -E 'bailian'
|
||||
```
|
||||
|
||||
启动:
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh web
|
||||
```
|
||||
|
||||
Web UI 在 http://127.0.0.1:3080。
|
||||
|
||||
---
|
||||
|
||||
## 3. Bailian 设置页 + 欢迎页
|
||||
|
||||
安装并重启后:
|
||||
|
||||
- **Settings → Bailian**:通用设置页(凭证配置 / TokenPlan 用量 / 记忆库)。
|
||||
- **新会话欢迎页**:每个新会话(blank)在输入框上方显示「百炼 Agent」欢迎页(Tab + 功能卡片),发出第一条消息后自动隐藏。
|
||||
|
||||
### 凭证配置(通用)
|
||||
|
||||
1. 在「凭证配置」区填入 **AccessKey ID** 和 **AccessKey Secret**
|
||||
2. 点击 **「保存凭证」**
|
||||
|
||||
Host 会执行 `bl auth login --open-api --config dsh`,将 AK/SK 和新生成的 access_token 存入 bl 的 `dsh` 专属 profile。**所有后续百炼插件共用此凭证**,无需重复配置。
|
||||
|
||||
### TokenPlan 用量
|
||||
|
||||
1. 选择区域和站点
|
||||
2. 点击 **「查询用量」**
|
||||
|
||||
Host 执行 `bl console call --config dsh` 调用 3 个个人版控制台接口,返回:
|
||||
|
||||
- **用量百分比** —— 5 小时窗口 / 1 周窗口的用量百分比和重置时间
|
||||
- **套餐信息** —— 套餐类型(基础版/标准版/高级版)、状态、剩余天数、到期时间、自动续费
|
||||
- **额外用量包** —— Credits 总量、剩余量、生效中数量
|
||||
|
||||
### 凭证解析优先级
|
||||
|
||||
凭证保存到 bl 的 `dsh` profile 后,所有百炼插件通过 `--config dsh` 读取。行内 config 的 `accessKeyId`/`accessKeySecret` 作为兜底(未通过 UI 保存时自动使用)。
|
||||
|
||||
### 行内配置(可选)
|
||||
|
||||
如果不想在 UI 里每次输入,可以在 profile 的 `cordis.patch.yml` 里固化凭证:
|
||||
|
||||
```yaml
|
||||
- id: bailian-tokenplan-usage
|
||||
config:
|
||||
# accessKeyId / accessKeySecret: 兜底凭证(未通过 UI 保存时使用)
|
||||
# consoleRegion: cn-beijing
|
||||
# consoleSite: domestic
|
||||
# profile: dsh # 默认用 dsh 专属 profile
|
||||
```
|
||||
|
||||
配置后 UI 表单会留空,但点击「查询用量」会使用行内凭证。
|
||||
|
||||
---
|
||||
|
||||
## 4. 跨会话长期记忆
|
||||
|
||||
默认停用(按量计费)。在 `cordis.patch.yml` 中设 `disabled: false` 启用,然后在设置页配置 API Key 和参数。
|
||||
|
||||
### 功能
|
||||
|
||||
- **自动检索注入**:新会话首轮,用用户消息搜索记忆,将结果注入上下文(`autoInject`,默认开启)
|
||||
- **自动落库**:每轮结束,将该轮新消息发送到记忆库 add API(`autoPersist`,默认开启)
|
||||
- **模型工具**:`bailian_memory_search`(检索)、`bailian_memory_add`(存储)、`bailian_memory_list`(浏览)
|
||||
|
||||
### 触发机制
|
||||
|
||||
| 时机 | 触发方式 |
|
||||
| ---------- | --------------------------------------------------------- |
|
||||
| 新会话首轮 | 自动检索记忆注入上下文(`agent/pre-step` 事件) |
|
||||
| 对话中 | 模型主动调用 `bailian_memory_search`/`bailian_memory_add` |
|
||||
| 轮次结束 | 自动落库新消息(`agent/turn-stopping` 事件) |
|
||||
|
||||
### 凭证与配置
|
||||
|
||||
- **API Key**:DashScope 按量付费 Key(`sk-`),在设置页「凭证配置」填入
|
||||
- **Base URL**:默认 `https://dashscope.aliyuncs.com/api/v2/apps/memory/`
|
||||
- **User ID**:记忆归属 ID,默认读系统用户名
|
||||
- **Plan Version**:`lite`(便宜,关闭 rerank)或 `pro`(开启 rerank,约 50 倍成本)。注意:实际计费由 `enable_rerank` 控制
|
||||
- **Top K**:检索返回数量(1-100,默认 10)
|
||||
- **Memory Library ID**:记忆库 ID,留空用默认
|
||||
|
||||
### 计费
|
||||
|
||||
- Add:120 QPM
|
||||
- Search:300 QPM(Lite ¥0.00002/次,Pro ¥0.001/次)
|
||||
- 总计不超过 3000 QPM
|
||||
|
||||
### 启用
|
||||
|
||||
```yaml
|
||||
- id: bailian-memory
|
||||
disabled: false
|
||||
config:
|
||||
baseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/memory/"
|
||||
planVersion: "lite"
|
||||
topK: 10
|
||||
autoInject: true
|
||||
autoPersist: true
|
||||
```
|
||||
|
||||
启用后在设置页「记忆库」section 配置 API Key 和参数。
|
||||
|
||||
> 记忆库调用 DashScope memory v2 API(非 `bl memory`),因为 v2 API 暴露了 `min_score`、`enable_rerank`、`plan_version`、`memory_library_id` 等参数 `bl memory` 不支持。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
```sh
|
||||
# 配置合成
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep bailian
|
||||
|
||||
# bl 就绪
|
||||
bl auth status
|
||||
```
|
||||
|
||||
启动后验证:
|
||||
|
||||
- **欢迎页**:新开一个会话,输入框上方出现「百炼 Agent」欢迎页
|
||||
- **凭证配置**:打开 Settings → Bailian → 填入 AK/SK → 保存凭证
|
||||
- **用量展示**:同页面选择区域 → 查询用量
|
||||
- **记忆库**:启用 `bailian-memory` 后,同页面配置 API Key
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
| 现象 | 原因 |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 用量查询报 `bl auth login failed` | AK/SK 无效或无权限;确认 AK 有百炼控制台访问权限 |
|
||||
| 用量查询报 `NotLogined` 或 token 过期 | bl 的 access token 已过期;Host 会自动通过 AK/SK 刷新,确认 AK/SK 正确 |
|
||||
| 用量查询报 `bl console call failed` | 控制台接口调用失败;检查 region/site 是否匹配你的账号 |
|
||||
| 用量查询报 `Workspace.NotAuthorised` | bl 用了其他 profile 的旧 access_token;Host 默认用 `--config dsh` 专属 profile 隔离,首次 login 会生成新 token |
|
||||
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
|
||||
| 设置页/欢迎页看不到 Bailian | 需**重启 `dsh web`**(bundle 在启动时加载);确认 `dump-config` 有 `bailian-client` 行,且 `client.bundle.js` 为 ModuleLoader 格式 |
|
||||
| 启动报 `invalid plugin ... apply` | 包根 `dist/index.mjs` 必须导出 `apply`(no-op 插件);重新 `pnpm build` 再装 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 卸载
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh plugin --profile web remove bailian-cli-dsh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
### Host 半
|
||||
|
||||
- `src/tokenplan-usage/index.ts` —— 凭证 + TokenPlan 用量。`inject: ['subprocess']`,所有 bl 命令带 `--config dsh` 隔离凭证。两个 webServer 路由:
|
||||
- `POST /bailian/credentials` — 保存 AK/SK(`bl auth login --open-api --config dsh`,生成新 token)
|
||||
- `POST /bailian/tokenplan/usage` — 查询用量(`bl console call --config dsh`,3 个个人版接口)
|
||||
- `src/memory/index.ts` —— 记忆库(默认停用)。直接调 DashScope memory v2 API,注册 tools + auto-inject/persist。路由 `/bailian/memory/config`、`/bailian/memory/status`。
|
||||
- `src/index.ts` —— 包根 no-op 插件,供 `bailian-client` 行加载(该行只为了让 client-modules 服务浏览器 bundle)。
|
||||
|
||||
> 路由用 `/bailian/*` 而非 `/api/*`:`/api` 前缀被 dsh 的 RPC 网关(apiProxy)占用,自定义路由会被遮蔽。
|
||||
|
||||
调用链路:**AK/SK → `bl auth login --open-api --config dsh`(存入 dsh profile)→ `bl console call --config dsh`(读 dsh profile token → 控制台网关)→ 个人版 TokenPlan 接口**
|
||||
|
||||
### Client 半(`src/client.ts`)
|
||||
|
||||
- 唯一的浏览器源码,构建为 DSH ModuleLoader 格式(见下)。
|
||||
- 注册 `settings.section`(id: `bailian`,label: `Bailian`),渲染通用百炼设置页(凭证配置 / TokenPlan 用量 / 记忆库)。
|
||||
- 注册 `conversation.input.dock`(id: `bailian-welcome`):当 `session.blank === true`(新会话)渲染「百炼 Agent」欢迎页(Tab + 功能卡片),开始对话后自动隐藏。
|
||||
- 通过 `fetch('/bailian/*')` 调 Host 路由。
|
||||
|
||||
### Client 构建(ModuleLoader 格式)
|
||||
|
||||
DSH 浏览器只加载 `window.__ModuleLoader__.load({ id, factory })` 格式的 bundle(`require('react')` 由浏览器 ModuleLoader 提供)。vite-plus 产出裸 ES module,格式不对,所以 client 单独用 esbuild 构建:
|
||||
|
||||
- `scripts/build-client.mjs` —— 把 `src/client.ts` 构建为 CJS + browser + `react` external,包上 ModuleLoader banner/footer,输出 `client.bundle.js`。
|
||||
- `package.json` 的 `build` = `vp pack && node scripts/build-client.mjs`。
|
||||
- `package.json` 的 `exports["./client"]` 与 `dsh.client: { platform: "web" }` 指向 `client.bundle.js`,被 client-modules 扫描并服务。
|
||||
- `cordis.patch.yml` 的 `bailian-client` 行 `name` 必须是**包根**(`bailian-cli-dsh`,无子路径),client-modules 才能 `require.resolve("<name>/package.json")` 识别 `dsh.client`。
|
||||
|
||||
改 client UI 只需编辑 `src/client.ts`,`pnpm build` 自动重新生成 `client.bundle.js`。
|
||||
|
||||
### 共享模块(`src/shared/`)
|
||||
|
||||
- `bl.ts` —— `bl` 子进程调用封装(env 转发、stdout/stderr 收集、JSON 解析)
|
||||
- `credentials.ts` —— TokenPlan / 按量付费 Key 分类工具
|
||||
- `http.ts` —— DashScope HTTP 客户端
|
||||
|
||||
这些模块来自早期版本(vision / image / managed-agent / RAG / memory 工具),已移除工具实现但保留共享逻辑作为参考。
|
||||
@@ -0,0 +1,53 @@
|
||||
# bailian-cli-dsh — Aliyun Model Studio (Bailian) as a dsh profile bundle.
|
||||
#
|
||||
# Inserts Bailian plugin rows: TokenPlan usage display + cross-session memory.
|
||||
# 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.
|
||||
|
||||
- insert:
|
||||
# Client-only row: name is the package ROOT (no subpath) so client-modules
|
||||
# can resolve "<name>/package.json" and detect the dsh.client declaration.
|
||||
# Its node half (dist/index.mjs) is a no-op; the row exists to serve the
|
||||
# browser bundle (client.bundle.js) that renders the Bailian settings page
|
||||
# and the new-session welcome page.
|
||||
- id: bailian-client
|
||||
name: bailian-cli-dsh
|
||||
|
||||
# TokenPlan usage display (dual-face: Host provides two webServer routes,
|
||||
# Client renders a general "Bailian" settings.section page). All bl commands
|
||||
# use `--config dsh` to isolate credentials in a dedicated bl profile.
|
||||
#
|
||||
# Two routes:
|
||||
# POST /api/bailian/credentials — saves AK/SK to dsh profile
|
||||
# (bl auth login --open-api --config dsh). Generates fresh access_token.
|
||||
# POST /api/bailian/tokenplan/usage — fetches personal-edition usage
|
||||
# using the dsh profile (no AK/SK in body; credentials already saved).
|
||||
#
|
||||
# Users configure AK/SK once on the settings page; all future Bailian
|
||||
# plugins reuse the same dsh profile credentials.
|
||||
#
|
||||
# Config fields:
|
||||
# accessKeyId / accessKeySecret: fallback when not provided via UI.
|
||||
# consoleRegion: default region (cn-beijing).
|
||||
# consoleSite: domestic | international (default: domestic).
|
||||
# profile: bl config profile name (default: dsh).
|
||||
- id: bailian-tokenplan-usage
|
||||
name: bailian-cli-dsh/tokenplan-usage
|
||||
config: {}
|
||||
|
||||
# Disabled by default: memory add/search are billed per call. Enable in
|
||||
# the profile patch and configure API Key + parameters on the Bailian
|
||||
# settings page. Calls DashScope memory v2 API directly (not bl memory)
|
||||
# for full parameter control (min_score, enable_rerank, plan_version,
|
||||
# memory_library_id, enable_judge, enable_rewrite).
|
||||
- id: bailian-memory
|
||||
name: bailian-cli-dsh/memory
|
||||
disabled: true
|
||||
config:
|
||||
baseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/memory/"
|
||||
planVersion: "lite"
|
||||
topK: 10
|
||||
autoInject: true
|
||||
autoPersist: true
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"name": "bailian-cli-dsh",
|
||||
"version": "1.14.2",
|
||||
"description": "Aliyun Model Studio (Bailian) plugin bundle for DeepSeek Harness (dsh): TokenPlan LLM provider and personal-edition TokenPlan usage display in the webui.",
|
||||
"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",
|
||||
"client.bundle.js",
|
||||
"cordis.patch.yml"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./tokenplan-usage": {
|
||||
"types": "./src/tokenplan-usage/index.ts",
|
||||
"default": "./dist/tokenplan-usage/index.mjs"
|
||||
},
|
||||
"./memory": {
|
||||
"types": "./src/memory/index.ts",
|
||||
"default": "./dist/memory/index.mjs"
|
||||
},
|
||||
"./client": "./client.bundle.js",
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": "./dist/index.mjs",
|
||||
"./tokenplan-usage": "./dist/tokenplan-usage/index.mjs",
|
||||
"./memory": "./dist/memory/index.mjs",
|
||||
"./client": "./client.bundle.js",
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vp pack && node scripts/build-client.mjs",
|
||||
"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"
|
||||
},
|
||||
"client": {
|
||||
"platform": "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Build the browser client bundle in the DSH ModuleLoader closure format.
|
||||
*
|
||||
* The DSH web shell only loads client plugins that call
|
||||
* `window.__ModuleLoader__.load({ id, factory })`, resolving externals (react)
|
||||
* through the injected `require`. vite-plus emits plain ESM (wrong format), so
|
||||
* the client is built separately with esbuild: CJS + browser platform + react
|
||||
* external, wrapped in the ModuleLoader banner/footer.
|
||||
*
|
||||
* Run after `vp pack` (see package.json "build").
|
||||
*/
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const esbuild = join(pkgDir, "node_modules", ".bin", "esbuild");
|
||||
|
||||
const banner =
|
||||
'window.__ModuleLoader__.load({ id: "bailian-cli-dsh", factory: (require) => { ' +
|
||||
"var module = { exports: {} }; var exports = module.exports;";
|
||||
const footer = "return module.exports; } });";
|
||||
|
||||
const result = spawnSync(
|
||||
esbuild,
|
||||
[
|
||||
"src/client.ts",
|
||||
"--bundle",
|
||||
"--format=cjs",
|
||||
"--platform=browser",
|
||||
"--external:react",
|
||||
`--banner:js=${banner}`,
|
||||
`--footer:js=${footer}`,
|
||||
"--outfile=client.bundle.js",
|
||||
],
|
||||
{ cwd: pkgDir, stdio: "inherit" },
|
||||
);
|
||||
|
||||
if (result.status !== 0) {
|
||||
// Throw rather than process.exit: an uncaught top-level error still yields a
|
||||
// non-zero exit (so `pnpm build` fails), and it carries esbuild's own status.
|
||||
throw new Error(`build-client: esbuild failed with status ${result.status ?? "unknown"}`);
|
||||
}
|
||||
console.log("build-client: client.bundle.js (ModuleLoader format) written");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Bailian feature registry — single source of truth mapping a welcome-page
|
||||
* card to a **bailian-cli command**. The console-API knowledge lives in
|
||||
* bailian-cli (packages/commands); this bundle only shells out to `bl`, so a
|
||||
* feature added there is reusable here for free.
|
||||
*
|
||||
* Each entry is exposed two ways by the Host:
|
||||
* 1. a **model tool** `bailian_<id>` (natural-language entry: the LLM reads
|
||||
* `intent` and calls the tool when the user asks in plain language);
|
||||
* 2. the **generic route** `POST /bailian/console { featureId }` (card-click
|
||||
* entry: the client renders `summarize`/`data`).
|
||||
*
|
||||
* Adding a feature = (a) add a `bl` command in bailian-cli, (b) add one record
|
||||
* here. Tool + card come for free.
|
||||
*
|
||||
* Browser-safe (no node imports) so both the vite host build and the esbuild
|
||||
* client bundle can import it.
|
||||
*
|
||||
* @module bailian-cli-dsh/features
|
||||
*/
|
||||
|
||||
export interface BailianFeature {
|
||||
/** Stable id; tool name is `bailian_<id>`. */
|
||||
id: string;
|
||||
/** Card title (matched against welcome cards). */
|
||||
title: string;
|
||||
/** Card description. */
|
||||
desc: string;
|
||||
/** Tool description: tells the LLM which user utterances should use it. */
|
||||
intent: string;
|
||||
/** Natural-language query sent into the conversation when the card is clicked. */
|
||||
query: string;
|
||||
/** `bl` command args (without `--output`); the Host appends `--output json`. */
|
||||
argv: string[];
|
||||
/** Args appended when the user supplies no params (e.g. ["--all"]). */
|
||||
defaultArgs?: string[];
|
||||
/** Optional params the LLM (or UI) may supply; mapped to bl flags. */
|
||||
paramFlags?: FeatureParam[];
|
||||
/** Human/LLM summary of the command's JSON output. */
|
||||
summarize: (data: any) => string;
|
||||
}
|
||||
|
||||
export interface FeatureParam {
|
||||
/** Tool parameter name (LLM fills it). */
|
||||
name: string;
|
||||
/** bl flag it maps to (e.g. --model). */
|
||||
flag: string;
|
||||
type: "string" | "number" | "boolean";
|
||||
description: string;
|
||||
}
|
||||
|
||||
function pick(obj: any, ...keys: string[]): any {
|
||||
for (const k of keys) if (obj && obj[k] !== undefined && obj[k] !== null) return obj[k];
|
||||
return undefined;
|
||||
}
|
||||
function pct(v: any): string {
|
||||
if (v === undefined || v === null) return "—";
|
||||
const n = (typeof v === "number" ? v : Number(v)) * 100;
|
||||
return (isNaN(n) ? 0 : n).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
export const FEATURES: BailianFeature[] = [
|
||||
{
|
||||
id: "free-tier",
|
||||
title: "免费额度一键防护",
|
||||
desc: "查询免费额度用量,一键开启「用完即停」,额度耗尽自动停止调用,不再产生意外扣费",
|
||||
intent:
|
||||
"查询百炼免费额度用量与『用完即停』防护状态。当用户提到免费额度、额度耗尽、意外扣费、用完即停、额度防护时使用。",
|
||||
query: "帮我查看百炼免费额度用量,并告诉我怎么开启「用完即停」防护",
|
||||
argv: ["usage", "freetier"],
|
||||
defaultArgs: ["--all"],
|
||||
paramFlags: [
|
||||
{
|
||||
name: "models",
|
||||
flag: "--model",
|
||||
type: "string",
|
||||
description:
|
||||
"逗号分隔的模型列表;不填则查询全部(--all)。若用户只关心特定模型且未说明,可先用 AskUserQuestion 询问。",
|
||||
},
|
||||
],
|
||||
summarize: (d) => {
|
||||
if (!d || typeof d !== "object") return "未获取到免费额度数据。";
|
||||
const list = pick(d, "quotas", "quotaList", "models", "list");
|
||||
if (Array.isArray(list)) {
|
||||
const lines = list.slice(0, 8).map((m: any) => {
|
||||
const model = pick(m, "model", "modelName", "modelId") ?? "?";
|
||||
const total = pick(m, "quotaTotal", "totalQuota", "total");
|
||||
const used = pick(m, "quotaUsed", "usedQuota", "used");
|
||||
const on = pick(m, "freeTierOnly");
|
||||
return `- ${model}: 已用 ${used ?? "?"} / 共 ${total ?? "?"}${on !== undefined ? `,用完即停 ${on ? "开" : "关"}` : ""}`;
|
||||
});
|
||||
return lines.length
|
||||
? `免费额度:\n${lines.join("\n")}`
|
||||
: "免费额度: " + JSON.stringify(d).slice(0, 300);
|
||||
}
|
||||
return "免费额度: " + JSON.stringify(d).slice(0, 300);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "usage",
|
||||
title: "模型用量统计",
|
||||
desc: "各模型/TokenPlan 的用量与百分比一次查清,自动生成用量分析",
|
||||
intent:
|
||||
"查询百炼 TokenPlan 个人版用量(5 小时/1 周窗口百分比、重置时间、套餐、用量包)。当用户问用量、用了多少、额度百分比、TokenPlan 使用情况时使用。",
|
||||
query: "帮我查询百炼 TokenPlan 个人版用量(5 小时/1 周窗口、套餐与用量包)",
|
||||
argv: ["token-plan", "personal-usage"],
|
||||
summarize: (d) => {
|
||||
if (!d || typeof d !== "object") return "未获取到用量数据。";
|
||||
const u = d.usage ?? d;
|
||||
const parts: string[] = [];
|
||||
if (u.per5HourPercentage !== undefined)
|
||||
parts.push(`5 小时窗口已用 ${pct(u.per5HourPercentage)}`);
|
||||
if (u.per1WeekPercentage !== undefined)
|
||||
parts.push(`1 周窗口已用 ${pct(u.per1WeekPercentage)}`);
|
||||
const sub = d.subscription;
|
||||
if (sub && sub.remainingDays !== undefined) parts.push(`套餐剩余 ${sub.remainingDays} 天`);
|
||||
const add = d.addonSummary;
|
||||
if (add && add.remainingCredits !== undefined)
|
||||
parts.push(`用量包剩余 ${add.remainingCredits}/${add.totalCredits}`);
|
||||
return parts.length
|
||||
? `TokenPlan 用量: ${parts.join(";")}`
|
||||
: "用量: " + JSON.stringify(d).slice(0, 300);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function featureById(id: string): BailianFeature | undefined {
|
||||
return FEATURES.find((f) => f.id === id);
|
||||
}
|
||||
export function featureByTitle(title: string): BailianFeature | undefined {
|
||||
return FEATURES.find((f) => f.title === title);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* This root module is the no-op node half loaded by the `bailian-client` row
|
||||
* (whose purpose is to make `client-modules` serve the browser bundle
|
||||
* `client.bundle.js`). Cordis requires every row to resolve to a plugin with
|
||||
* an `apply` method, so this exports a minimal one. The real Host logic lives
|
||||
* in `./tokenplan-usage` and `./memory`; the browser UI lives in
|
||||
* `client.bundle.js`.
|
||||
*
|
||||
* @module bailian-cli-dsh
|
||||
*/
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-cli-dsh";
|
||||
|
||||
/** No-op: this row exists only to serve the client bundle. */
|
||||
export function apply(): void {}
|
||||
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/memory` (Host half): cross-session long-term memory backed
|
||||
* by Bailian's hosted memory library (DashScope memory v2 API).
|
||||
*
|
||||
* Provides:
|
||||
* - Two model tools: `bailian_memory_search` (recall) + `bailian_memory_add`
|
||||
* (store), plus `bailian_memory_list` (browse).
|
||||
* - Auto-inject: on the first turn of each session (or every turn if
|
||||
* configured), search memory and inject relevant facts into context.
|
||||
* - Auto-persist: when a turn closes, send new user/assistant messages to
|
||||
* the add API so future sessions can recall them.
|
||||
* - webServer routes for the Client settings page to configure memory
|
||||
* parameters (apiKey, baseUrl, userId, planVersion, etc.).
|
||||
*
|
||||
* Calls go straight to DashScope rather than through `bl memory`, because
|
||||
* the v2 API exposes retrieval controls (`min_score`, `plan_version`,
|
||||
* `enable_rerank`, `memory_library_id`, `enable_judge`, `enable_rewrite`)
|
||||
* the CLI does not surface.
|
||||
*
|
||||
* BILLING: add and search are charged per call. `pro` costs ~50x `lite` per
|
||||
* search. The `enable_rerank` flag is what actually selects the billing tier
|
||||
* (verified: sending `plan_version: lite` alone still bills `pro`).
|
||||
*
|
||||
* @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 type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts";
|
||||
import { dashScopeFetch } 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", "webServer"];
|
||||
|
||||
export interface Config {
|
||||
/** DashScope API key (pay-as-you-go sk-ws-). Falls back to $DASHSCOPE_API_KEY. */
|
||||
apiKey?: string;
|
||||
/** Memory API base URL (default: https://dashscope.aliyuncs.com/api/v2/apps/memory/). */
|
||||
baseUrl?: string;
|
||||
/** Memory entity id. Falls back to $BAILIAN_MEMORY_USER_ID, then OS user. */
|
||||
userId?: string;
|
||||
/** Memory library id; defaults to the account default. */
|
||||
memoryLibraryId?: string;
|
||||
/** Memory extraction rule id. */
|
||||
projectId?: string;
|
||||
/** Profile template id; omitting skips profile extraction (and its cost). */
|
||||
profileSchema?: string;
|
||||
/** Search strategy; pro enables rerank at ~50x the cost. */
|
||||
planVersion?: "lite" | "pro";
|
||||
topK?: number;
|
||||
minScore?: number;
|
||||
/** Retrieve relevant memories and inject into the conversation. */
|
||||
autoInject?: boolean;
|
||||
/** Retrieve every turn instead of once per session. */
|
||||
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 API key (sk-)."),
|
||||
baseUrl: z.string().description("Memory API base URL."),
|
||||
userId: z.string().description("Memory entity id owning these memories."),
|
||||
memoryLibraryId: z.string().description("Memory library id."),
|
||||
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 ~50x 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_BASE_URL = "https://dashscope.aliyuncs.com/api/v2/apps/memory/";
|
||||
const DEFAULT_TOP_K = 10;
|
||||
const DEFAULT_PLAN_VERSION = "lite";
|
||||
|
||||
const CONFIG_ROUTE = "/bailian/memory/config";
|
||||
const STATUS_ROUTE = "/bailian/memory/status";
|
||||
|
||||
interface MemoryNode {
|
||||
memory_node_id?: string;
|
||||
content?: string;
|
||||
event?: string;
|
||||
old_content?: string;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
meta_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface MemoryResponse {
|
||||
request_id?: string;
|
||||
memory_nodes?: readonly MemoryNode[];
|
||||
total?: number;
|
||||
page_num?: number;
|
||||
page_size?: number;
|
||||
billing_plan?: string;
|
||||
}
|
||||
|
||||
interface ChatTurn {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Mutable runtime config — updated via webServer route, initialized from Cordis config. */
|
||||
interface MemoryRuntimeConfig {
|
||||
apiKey: string | undefined;
|
||||
baseUrl: string;
|
||||
userId: string;
|
||||
memoryLibraryId: string | undefined;
|
||||
projectId: string | undefined;
|
||||
profileSchema: string | undefined;
|
||||
planVersion: "lite" | "pro";
|
||||
topK: number;
|
||||
minScore: number | undefined;
|
||||
autoInject: boolean;
|
||||
injectEveryTurn: boolean;
|
||||
autoPersist: boolean;
|
||||
}
|
||||
|
||||
/** Resolution order: explicit config, then env, then 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;
|
||||
}
|
||||
|
||||
/** Read a UTF-8 POST body up to a size limit. */
|
||||
function readJsonBody(req: IncomingMessage, maxBytes: number = 16384): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let total = 0;
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
total += chunk.length;
|
||||
if (total > maxBytes) {
|
||||
req.destroy();
|
||||
reject(new Error("body too large"));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
if (text.length === 0) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch {
|
||||
reject(new Error("invalid JSON"));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendJson(res: ServerResponse, status: number, data: unknown): void {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
class MemoryClient {
|
||||
constructor(
|
||||
private readonly apiKey: string,
|
||||
private readonly baseUrl: string,
|
||||
private readonly cfg: MemoryRuntimeConfig,
|
||||
private readonly userId: string,
|
||||
) {}
|
||||
|
||||
private shared(): Record<string, unknown> {
|
||||
return {
|
||||
user_id: this.userId,
|
||||
...(this.cfg.memoryLibraryId !== undefined
|
||||
? { memory_library_id: this.cfg.memoryLibraryId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
async add(
|
||||
messages: readonly ChatTurn[],
|
||||
signal: AbortSignal | undefined,
|
||||
overrides?: { customContent?: string; metaData?: Record<string, unknown> },
|
||||
): Promise<MemoryResponse> {
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}add`,
|
||||
method: "POST",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
body: {
|
||||
...this.shared(),
|
||||
...(overrides?.customContent !== undefined
|
||||
? { custom_content: overrides.customContent }
|
||||
: { messages }),
|
||||
...(this.cfg.projectId !== undefined ? { project_id: this.cfg.projectId } : {}),
|
||||
...(this.cfg.profileSchema !== undefined ? { profile_schema: this.cfg.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.cfg.planVersion ?? DEFAULT_PLAN_VERSION;
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}memory_nodes/search`,
|
||||
method: "POST",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
body: {
|
||||
...this.shared(),
|
||||
messages,
|
||||
top_k: overrides?.topK ?? this.cfg.topK ?? DEFAULT_TOP_K,
|
||||
...((overrides?.minScore ?? this.cfg.minScore) !== undefined
|
||||
? { min_score: overrides?.minScore ?? this.cfg.minScore }
|
||||
: {}),
|
||||
// enable_rerank is what actually selects the billing tier (verified:
|
||||
// plan_version alone still bills pro). Send both for safety.
|
||||
enable_rerank: planVersion === "pro",
|
||||
plan_version: planVersion,
|
||||
...(this.cfg.projectId !== undefined ? { project_ids: [this.cfg.projectId] } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async list(
|
||||
signal: AbortSignal | undefined,
|
||||
overrides?: { pageNum?: number; pageSize?: number },
|
||||
): Promise<MemoryResponse> {
|
||||
const params = new URLSearchParams({
|
||||
user_id: this.userId,
|
||||
page_num: String(overrides?.pageNum ?? 1),
|
||||
page_size: String(overrides?.pageSize ?? 10),
|
||||
...(this.cfg.memoryLibraryId !== undefined
|
||||
? { memory_library_id: this.cfg.memoryLibraryId }
|
||||
: {}),
|
||||
});
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}memory_nodes?${params.toString()}`,
|
||||
method: "GET",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatMemories(nodes: readonly MemoryNode[]): string {
|
||||
const items = nodes
|
||||
.map((node) => node.content?.trim())
|
||||
.filter((content): content is string => content !== undefined && content.length > 0);
|
||||
if (items.length === 0) return "";
|
||||
return `What you remember about this user from earlier sessions:\n${items.map((item) => `- ${item}`).join("\n")}`;
|
||||
}
|
||||
|
||||
/** Register model tools for deliberate memory operations. */
|
||||
function registerTools(ctx: Context, client: () => MemoryClient | undefined): 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((m: any) => `- ${m.content}`).join("\n"),
|
||||
},
|
||||
],
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const mem = client();
|
||||
if (mem === undefined)
|
||||
throw new Error(
|
||||
"bailian-memory: not configured. Set apiKey in the Bailian settings page or config.",
|
||||
);
|
||||
const result = await mem.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: (result.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 mem = client();
|
||||
if (mem === undefined)
|
||||
throw new Error(
|
||||
"bailian-memory: not configured. Set apiKey in the Bailian settings page or config.",
|
||||
);
|
||||
const result = await mem.add([], exec.signal, { customContent: args.content });
|
||||
return { stored: (result.memory_nodes ?? []).length };
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_memory_list",
|
||||
description:
|
||||
"List all stored memory fragments for this user. Use to review what the system already knows.",
|
||||
parameters: {
|
||||
page_size: { type: "integer", description: "Results per page (default 10)." },
|
||||
page_num: { type: "integer", description: "Page number, starting from 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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: "integer", required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [
|
||||
{
|
||||
type: "text",
|
||||
text: `${value.total} memory fragment(s):\n${value.memories.map((m: any) => `- ${m.content}`).join("\n")}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const mem = client();
|
||||
if (mem === undefined) throw new Error("bailian-memory: not configured.");
|
||||
const result = await mem.list(exec.signal, {
|
||||
...(args.page_size !== undefined ? { pageSize: args.page_size } : {}),
|
||||
...(args.page_num !== undefined ? { pageNum: args.page_num } : {}),
|
||||
});
|
||||
return {
|
||||
memories: (result.memory_nodes ?? []).map((node) => ({
|
||||
id: node.memory_node_id ?? "",
|
||||
content: node.content ?? "",
|
||||
})),
|
||||
total: result.total ?? 0,
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Auto-inject (search on first turn) + auto-persist (add on turn end). */
|
||||
function registerAutoBehavior(
|
||||
ctx: Context,
|
||||
client: () => MemoryClient | undefined,
|
||||
cfg: () => MemoryRuntimeConfig,
|
||||
): void {
|
||||
const injectedSessions = new WeakSet<Agent>();
|
||||
const persistedCursor = new WeakMap<Agent, number>();
|
||||
|
||||
const currentCfg = cfg();
|
||||
|
||||
if (currentCfg.autoInject !== false) {
|
||||
ctx.on(
|
||||
"agent/pre-step",
|
||||
async (
|
||||
{
|
||||
agent,
|
||||
messages,
|
||||
signal,
|
||||
}: { agent: Agent; messages: readonly Message[]; signal: AbortSignal },
|
||||
next: () => Promise<PreStepDecision>,
|
||||
) => {
|
||||
const decision = await next();
|
||||
if (decision.kind !== "enter") return decision;
|
||||
if (injectedSessions.has(agent) && currentCfg.injectEveryTurn !== true) return decision;
|
||||
|
||||
const query = textOf(messages.flatMap((m) => m.content));
|
||||
if (query.length === 0) return decision;
|
||||
|
||||
const mem = client();
|
||||
if (mem === undefined) return decision;
|
||||
|
||||
let nodes: readonly MemoryNode[] = [];
|
||||
try {
|
||||
const result = await mem.search([{ role: "user", content: query }], signal);
|
||||
nodes = result.memory_nodes ?? [];
|
||||
} catch {
|
||||
return decision;
|
||||
}
|
||||
|
||||
injectedSessions.add(agent);
|
||||
if (nodes.length === 0) return decision;
|
||||
|
||||
const text = formatMemories(nodes);
|
||||
return {
|
||||
...decision,
|
||||
messages: [
|
||||
...decision.messages,
|
||||
createUserMessage({
|
||||
content: [{ type: "text", text }],
|
||||
source: {
|
||||
kind: "plugin",
|
||||
plugin: name,
|
||||
form: "snapshot",
|
||||
sections: [{ name, text }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
{ prepend: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCfg.autoPersist !== false) {
|
||||
ctx.on(
|
||||
"agent/turn-stopping",
|
||||
async ({ agent, signal }: { agent: Agent; signal: AbortSignal }) => {
|
||||
const mem = client();
|
||||
if (mem === undefined) return;
|
||||
|
||||
const turns = conversationTurns(agent.session.deriveMessages());
|
||||
const cursor = persistedCursor.get(agent) ?? 0;
|
||||
const newTurns = turns.slice(cursor);
|
||||
if (newTurns.length === 0) return;
|
||||
|
||||
persistedCursor.set(agent, turns.length);
|
||||
try {
|
||||
await mem.add(newTurns, signal);
|
||||
} catch {
|
||||
persistedCursor.set(agent, cursor);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const webServer = ctx.get("webServer");
|
||||
|
||||
// Mutable runtime config — initialized from Cordis config, updatable via webServer route.
|
||||
let runtime: MemoryRuntimeConfig = {
|
||||
apiKey: config.apiKey,
|
||||
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
||||
userId: resolveUserId(ctx, config),
|
||||
memoryLibraryId: config.memoryLibraryId,
|
||||
projectId: config.projectId,
|
||||
profileSchema: config.profileSchema,
|
||||
planVersion: config.planVersion ?? DEFAULT_PLAN_VERSION,
|
||||
topK: config.topK ?? DEFAULT_TOP_K,
|
||||
minScore: config.minScore,
|
||||
autoInject: config.autoInject ?? true,
|
||||
injectEveryTurn: config.injectEveryTurn ?? false,
|
||||
autoPersist: config.autoPersist ?? true,
|
||||
};
|
||||
|
||||
/** Build a MemoryClient from the current runtime config, or undefined if no API key. */
|
||||
function buildClient(): MemoryClient | undefined {
|
||||
if (runtime.apiKey === undefined || runtime.apiKey.length === 0) return undefined;
|
||||
if (isTokenPlanKey(runtime.apiKey)) {
|
||||
throw new Error(tokenPlanKeyRejection(name, "the memory API"));
|
||||
}
|
||||
return new MemoryClient(runtime.apiKey, runtime.baseUrl, runtime, runtime.userId);
|
||||
}
|
||||
|
||||
// Register tools + auto behavior.
|
||||
registerTools(ctx, buildClient);
|
||||
registerAutoBehavior(ctx, buildClient, () => runtime);
|
||||
|
||||
// webServer routes for the Client settings page.
|
||||
if (webServer !== undefined) {
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: STATUS_ROUTE,
|
||||
handler: async (_req: IncomingMessage, res: ServerResponse) => {
|
||||
sendJson(res, 200, {
|
||||
configured: runtime.apiKey !== undefined && runtime.apiKey.length > 0,
|
||||
userId: runtime.userId,
|
||||
baseUrl: runtime.baseUrl,
|
||||
planVersion: runtime.planVersion,
|
||||
topK: runtime.topK,
|
||||
autoInject: runtime.autoInject,
|
||||
injectEveryTurn: runtime.injectEveryTurn,
|
||||
autoPersist: runtime.autoPersist,
|
||||
memoryLibraryId: runtime.memoryLibraryId,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: CONFIG_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update mutable fields from the request body.
|
||||
if (typeof body.apiKey === "string") runtime.apiKey = body.apiKey || undefined;
|
||||
if (typeof body.baseUrl === "string" && body.baseUrl.length > 0)
|
||||
runtime.baseUrl = body.baseUrl;
|
||||
if (typeof body.userId === "string" && body.userId.length > 0)
|
||||
runtime.userId = body.userId;
|
||||
if (typeof body.memoryLibraryId === "string")
|
||||
runtime.memoryLibraryId = body.memoryLibraryId || undefined;
|
||||
if (typeof body.projectId === "string") runtime.projectId = body.projectId || undefined;
|
||||
if (typeof body.profileSchema === "string")
|
||||
runtime.profileSchema = body.profileSchema || undefined;
|
||||
if (body.planVersion === "lite" || body.planVersion === "pro")
|
||||
runtime.planVersion = body.planVersion;
|
||||
if (typeof body.topK === "number") runtime.topK = body.topK;
|
||||
if (typeof body.minScore === "number") runtime.minScore = body.minScore;
|
||||
if (typeof body.autoInject === "boolean") runtime.autoInject = body.autoInject;
|
||||
if (typeof body.injectEveryTurn === "boolean")
|
||||
runtime.injectEveryTurn = body.injectEveryTurn;
|
||||
if (typeof body.autoPersist === "boolean") runtime.autoPersist = body.autoPersist;
|
||||
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
configured: runtime.apiKey !== undefined && runtime.apiKey.length > 0,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tokenplan-usage` (Host half): provides two webServer
|
||||
* routes for the Client's "Bailian" settings page:
|
||||
*
|
||||
* 1. `POST /api/bailian/credentials` — saves AK/SK to the dedicated `dsh`
|
||||
* bl profile via `bl auth login --open-api --config dsh`. This generates
|
||||
* a fresh access_token and stores AK/SK + token in the profile. All
|
||||
* subsequent console calls read this profile.
|
||||
*
|
||||
* 2. `POST /api/bailian/tokenplan/usage` — fetches personal-edition
|
||||
* TokenPlan usage (3 console APIs) using the `dsh` profile credentials.
|
||||
* Takes only `{ region, site }`; AK/SK are already saved in the profile.
|
||||
*
|
||||
* Configuration UX: users save AK/SK once on the settings page. All future
|
||||
* Bailian plugins reuse the same `dsh` profile credentials.
|
||||
*
|
||||
* @module bailian-cli-dsh/tokenplan-usage
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { runBl } from "../shared/bl.ts";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type { JsonValue } from "@deepseek-ai/dsh-session";
|
||||
import { FEATURES, featureById, type FeatureParam } from "../features.ts";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tokenplan-usage";
|
||||
|
||||
/** Hard deps: bl via subprocess; routes need webServer; feature tools need tools. */
|
||||
export const inject = ["subprocess", "webServer", "tools"];
|
||||
|
||||
export interface Config {
|
||||
/** Alibaba Cloud Access Key ID. Fallback when not provided via UI. */
|
||||
accessKeyId?: string;
|
||||
/** Alibaba Cloud Access Key Secret. Fallback when not provided via UI. */
|
||||
accessKeySecret?: string;
|
||||
/** Console gateway region (default: cn-beijing). */
|
||||
consoleRegion?: string;
|
||||
/** Console site: domestic or international (default: domestic). */
|
||||
consoleSite?: "domestic" | "international";
|
||||
/** Dedicated bl config profile name (default: dsh). */
|
||||
profile?: string;
|
||||
}
|
||||
|
||||
export const Config = z.object({
|
||||
accessKeyId: z.string().description("Alibaba Cloud Access Key ID (fallback)."),
|
||||
accessKeySecret: z.string().description("Alibaba Cloud Access Key Secret (fallback)."),
|
||||
consoleRegion: z.string().description("Console gateway region (default: cn-beijing)."),
|
||||
consoleSite: z.string().description("Console site: domestic or international."),
|
||||
profile: z.string().description("Dedicated bl config profile name (default: dsh)."),
|
||||
});
|
||||
|
||||
/** Personal-edition console API names (from bailian-tokenplan frontend). */
|
||||
const PERSONAL_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
||||
const PERSONAL_SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription";
|
||||
const PERSONAL_ADDON_SUMMARY_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/addon/summary";
|
||||
|
||||
const PERSONAL_SUB_COMMODITY_CN = "sfm_tokenplansolo_public_cn";
|
||||
const PERSONAL_SUB_COMMODITY_INTL = "sfm_tokenplansolo_public_intl";
|
||||
const PERSONAL_ADDON_COMMODITY_CN = "sfm_tokenplansoloaddon_public_cn";
|
||||
const PERSONAL_ADDON_COMMODITY_INTL = "sfm_tokenplansoloaddon_public_intl";
|
||||
|
||||
const CREDENTIALS_ROUTE = "/bailian/credentials";
|
||||
const USAGE_ROUTE = "/bailian/tokenplan/usage";
|
||||
const CONSOLE_ROUTE = "/bailian/console";
|
||||
const BL_LOGIN_TIMEOUT_MS = 30_000;
|
||||
const BL_CALL_TIMEOUT_MS = 90_000;
|
||||
const BL_LOGIN_GRACE_MS = 20_000;
|
||||
const BL_CALL_GRACE_MS = 60_000;
|
||||
const DEFAULT_PROFILE = "dsh";
|
||||
|
||||
interface FetchResult {
|
||||
usage: unknown;
|
||||
subscription: unknown;
|
||||
addonSummary: unknown;
|
||||
errors: Array<{ api: string; message: string }>;
|
||||
}
|
||||
|
||||
/** Extract the business payload from a console gateway response. */
|
||||
function extractData(response: unknown): unknown {
|
||||
if (response === null || typeof response !== "object") return response;
|
||||
const outer = (response as Record<string, unknown>).data;
|
||||
if (outer !== null && typeof outer === "object") {
|
||||
const dataV2 = (outer as Record<string, unknown>).DataV2;
|
||||
if (dataV2 !== null && typeof dataV2 === "object") {
|
||||
const inner = (dataV2 as Record<string, unknown>).data;
|
||||
if (inner !== null && typeof inner === "object") {
|
||||
const payload = (inner as Record<string, unknown>).data;
|
||||
if (payload !== undefined) return payload;
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
const fallback = (outer as Record<string, unknown>).data;
|
||||
if (fallback !== undefined) return fallback;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Read a UTF-8 POST body up to a size limit. */
|
||||
function readJsonBody(req: IncomingMessage, maxBytes: number = 8192): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let total = 0;
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
total += chunk.length;
|
||||
if (total > maxBytes) {
|
||||
req.destroy();
|
||||
reject(new Error("request body too large"));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
if (text.length === 0) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch {
|
||||
reject(new Error("invalid JSON body"));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** Send a JSON response with a status code. */
|
||||
function sendJson(res: ServerResponse, status: number, data: unknown): void {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const webServer = ctx.get("webServer");
|
||||
if (webServer === undefined) return;
|
||||
|
||||
const profile = config.profile || DEFAULT_PROFILE;
|
||||
|
||||
/** Save AK/SK to the dsh profile (bl auth login --open-api --config dsh). */
|
||||
async function saveCredentials(accessKeyId: string, accessKeySecret: string): Promise<void> {
|
||||
const loginArgs = [
|
||||
"auth",
|
||||
"login",
|
||||
"--open-api",
|
||||
"--config",
|
||||
profile,
|
||||
"--access-key-id",
|
||||
accessKeyId,
|
||||
"--access-key-secret",
|
||||
accessKeySecret,
|
||||
];
|
||||
const loginOutcome = await runBl(ctx, loginArgs, {
|
||||
cwd: process.cwd(),
|
||||
signal: AbortSignal.timeout(BL_LOGIN_TIMEOUT_MS),
|
||||
graceMs: BL_LOGIN_GRACE_MS,
|
||||
});
|
||||
if (loginOutcome.exitCode !== 0) {
|
||||
const reason =
|
||||
loginOutcome.stderr.trim() || loginOutcome.stdout.trim() || `exit ${loginOutcome.exitCode}`;
|
||||
throw new Error(`bl auth login failed: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Call a console API using the dsh profile (credentials already saved). */
|
||||
async function consoleCall(
|
||||
region: string,
|
||||
site: string,
|
||||
api: string,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const callArgs = [
|
||||
"console",
|
||||
"call",
|
||||
"--config",
|
||||
profile,
|
||||
"--api",
|
||||
api,
|
||||
"--data",
|
||||
JSON.stringify(data),
|
||||
"--console-region",
|
||||
region,
|
||||
"--console-site",
|
||||
site,
|
||||
"--output",
|
||||
"json",
|
||||
];
|
||||
const callOutcome = await runBl(ctx, callArgs, {
|
||||
cwd: process.cwd(),
|
||||
signal: AbortSignal.timeout(BL_CALL_TIMEOUT_MS),
|
||||
graceMs: BL_CALL_GRACE_MS,
|
||||
});
|
||||
if (callOutcome.exitCode !== 0) {
|
||||
const reason =
|
||||
callOutcome.stderr.trim() || callOutcome.stdout.trim() || `exit ${callOutcome.exitCode}`;
|
||||
throw new Error(`bl console call failed (${api}): ${reason}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(callOutcome.stdout);
|
||||
} catch {
|
||||
return { raw: callOutcome.stdout };
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch all personal-edition TokenPlan usage (3 console calls). */
|
||||
async function fetchUsage(region: string, site: string): Promise<FetchResult> {
|
||||
const isIntl = site === "international";
|
||||
const subCommodity = isIntl ? PERSONAL_SUB_COMMODITY_INTL : PERSONAL_SUB_COMMODITY_CN;
|
||||
const addonCommodity = isIntl ? PERSONAL_ADDON_COMMODITY_INTL : PERSONAL_ADDON_COMMODITY_CN;
|
||||
|
||||
const errors: Array<{ api: string; message: string }> = [];
|
||||
let usage = null;
|
||||
let subscription = null;
|
||||
let addonSummary = null;
|
||||
|
||||
try {
|
||||
usage = extractData(await consoleCall(region, site, PERSONAL_USAGE_API, {}));
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
api: "usage",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
try {
|
||||
subscription = extractData(
|
||||
await consoleCall(region, site, PERSONAL_SUBSCRIPTION_API, {
|
||||
queryInstanceInfoRequest: { commodityCode: subCommodity },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
api: "subscription",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
try {
|
||||
addonSummary = extractData(
|
||||
await consoleCall(region, site, PERSONAL_ADDON_SUMMARY_API, {
|
||||
commodityCode: addonCommodity,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
api: "addonSummary",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
return { usage, subscription, addonSummary, errors };
|
||||
}
|
||||
|
||||
// Route 1: Save credentials to the dsh bl profile.
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: CREDENTIALS_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "method not allowed, use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
const accessKeyId = (body.accessKeyId as string) || config.accessKeyId;
|
||||
const accessKeySecret = (body.accessKeySecret as string) || config.accessKeySecret;
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
sendJson(res, 400, { error: "accessKeyId and accessKeySecret are required." });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveCredentials(accessKeyId, accessKeySecret);
|
||||
sendJson(res, 200, { ok: true, profile });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Route 2: Fetch TokenPlan usage using the dsh profile credentials.
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: USAGE_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "method not allowed, use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
// If AK/SK are provided in the body, save them first (auto-provision).
|
||||
const bodyKeyId = (body.accessKeyId as string) || undefined;
|
||||
const bodyKeySecret = (body.accessKeySecret as string) || undefined;
|
||||
if (bodyKeyId && bodyKeySecret) {
|
||||
try {
|
||||
await saveCredentials(bodyKeyId, bodyKeySecret);
|
||||
} catch (error) {
|
||||
sendJson(res, 500, {
|
||||
error: error instanceof Error ? error.message : "credential save failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const region = (body.region as string) || config.consoleRegion || "cn-beijing";
|
||||
const site = (body.site as string) || config.consoleSite || "domestic";
|
||||
try {
|
||||
const result = await fetchUsage(region, site);
|
||||
sendJson(res, 200, result);
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// ── Feature layer: reuse bailian-cli commands as model tools + a generic route ──
|
||||
|
||||
/** Run a feature's `bl` command with the dsh profile; returns parsed JSON. */
|
||||
async function invokeFeature(
|
||||
feature: (typeof FEATURES)[number],
|
||||
params?: Record<string, unknown>,
|
||||
): Promise<JsonValue> {
|
||||
const extra: string[] = [];
|
||||
for (const pf of feature.paramFlags ?? []) {
|
||||
const val = params?.[pf.name];
|
||||
if (val !== undefined && val !== null && val !== "") extra.push(pf.flag, String(val));
|
||||
}
|
||||
if (extra.length === 0 && feature.defaultArgs) extra.push(...feature.defaultArgs);
|
||||
const args = [...feature.argv, ...extra, "--config", profile, "--output", "json"];
|
||||
const outcome = await runBl(ctx, args, {
|
||||
cwd: process.cwd(),
|
||||
signal: AbortSignal.timeout(BL_CALL_TIMEOUT_MS),
|
||||
graceMs: BL_CALL_GRACE_MS,
|
||||
});
|
||||
if (outcome.exitCode !== 0) {
|
||||
const reason = outcome.stderr.trim() || outcome.stdout.trim() || `exit ${outcome.exitCode}`;
|
||||
throw new Error(`bl ${feature.argv.join(" ")} failed: ${reason}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(outcome.stdout);
|
||||
} catch {
|
||||
return { raw: outcome.stdout };
|
||||
}
|
||||
}
|
||||
|
||||
// Natural-language entry: one model tool per feature.
|
||||
const tools = ctx.get("tools");
|
||||
if (tools !== undefined) {
|
||||
for (const feature of FEATURES) {
|
||||
// Keep FeatureParam's literal `type` union: widening it to `string`
|
||||
// makes the map unassignable to ParameterSchemaSpec.
|
||||
const parameters: Record<string, { type: FeatureParam["type"]; description: string }> = {};
|
||||
for (const pf of feature.paramFlags ?? []) {
|
||||
parameters[pf.name] = { type: pf.type, description: pf.description };
|
||||
}
|
||||
ctx.effect(() =>
|
||||
tools.register(
|
||||
defineTool({
|
||||
name: `bailian_${feature.id}`,
|
||||
description: `${feature.title}。${feature.intent}`,
|
||||
parameters,
|
||||
output: {
|
||||
schema: { type: "object", additionalProperties: true },
|
||||
render: (_a, value) => [
|
||||
{ type: "text", text: String((value as any).summary ?? JSON.stringify(value)) },
|
||||
],
|
||||
},
|
||||
async execute(args) {
|
||||
const data = await invokeFeature(feature, args as Record<string, unknown>);
|
||||
return { summary: feature.summarize(data), data };
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Card-click entry: generic route dispatching to a feature by id.
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: CONSOLE_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "method not allowed, use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
const feature = featureById(String(body.featureId ?? ""));
|
||||
if (feature === undefined) {
|
||||
sendJson(res, 400, { error: `unknown featureId: ${String(body.featureId)}` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await invokeFeature(
|
||||
feature,
|
||||
body.params as Record<string, unknown> | undefined,
|
||||
);
|
||||
sendJson(res, 200, { summary: feature.summarize(data), data });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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- 按量付费)不可混用。
|
||||
// TokenPlan 网关 401 按量付费 Key,TokenPlan 网关只提供模型推理,不提供
|
||||
// 服务 API。managed-agent 的凭证两半独立下发:解析出 key 就显式
|
||||
// --api-key(不让 bl 用活动 profile 的 key),解析出端点就显式 --base-url
|
||||
// (不让 bl 用活动 profile 的端点)。agentstudio 只在工作空间前缀主机上提供,
|
||||
// 因此绝不存在"默认端点"——工作空间未知就是配置缺口,该报错而不是猜。
|
||||
// 这些共享函数来自早期版本(vision / image / managed-agent 等工具),
|
||||
// 工具已移除但凭证分类逻辑保留作为参考。
|
||||
|
||||
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",
|
||||
]);
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
entry: ["src/index.ts", "src/tokenplan-usage/index.ts", "src/memory/index.ts"],
|
||||
minify: true,
|
||||
dts: {
|
||||
tsgo: true,
|
||||
},
|
||||
},
|
||||
lint: {
|
||||
options: {
|
||||
typeAware: true,
|
||||
typeCheck: true,
|
||||
},
|
||||
},
|
||||
fmt: {},
|
||||
});
|
||||
Generated
+1879
-340
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
@@ -61,6 +65,8 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan personal-key` | Get the personal-edition TokenPlan API key (masked) for the current account | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan personal-usage` | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) | [token-plan.md](token-plan.md) |
|
||||
| `bl update` | Update the CLI to the latest or a specified version | [update.md](update.md) |
|
||||
| `bl usage free` | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) |
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
|
||||
@@ -71,28 +77,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`, `personal-key`, `personal-usage` | [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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | ----------------------------------------- |
|
||||
| `bl token-plan add-member` | Add a member to a Token Plan organization |
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details |
|
||||
| Command | Description |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------- |
|
||||
| `bl token-plan add-member` | Add a member to a Token Plan organization |
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details |
|
||||
| `bl token-plan personal-key` | Get the personal-edition TokenPlan API key (masked) for the current account |
|
||||
| `bl token-plan personal-usage` | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) |
|
||||
|
||||
## Command details
|
||||
|
||||
@@ -153,3 +155,49 @@ bl token-plan list-seats --page-size 20 --status NORMAL
|
||||
```bash
|
||||
bl token-plan list-seats --query-assigned true --seat-type standard
|
||||
```
|
||||
|
||||
### `bl token-plan personal-key`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan personal-key` |
|
||||
| **Description** | Get the personal-edition TokenPlan API key (masked) for the current account |
|
||||
| **Usage** | `bl token-plan personal-key [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl token-plan personal-key
|
||||
```
|
||||
|
||||
### `bl token-plan personal-usage`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan personal-usage` |
|
||||
| **Description** | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) |
|
||||
| **Usage** | `bl token-plan personal-usage [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl token-plan personal-usage
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user