diff --git a/.gitignore b/.gitignore index 97b43669..0afb45a4 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ packages/cli/scene/**/outputs/ # Local scratch / plan drafts (never commit) .scratch/ + +# pnpm pack output +*.tgz diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 652da452..74d8b632 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -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 = { "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 = { "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 = { "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, diff --git a/packages/commands/src/commands/managed-agent/_engine/credentials.ts b/packages/commands/src/commands/managed-agent/_engine/credentials.ts index cf0212cc..c3ba746a 100644 --- a/packages/commands/src/commands/managed-agent/_engine/credentials.ts +++ b/packages/commands/src/commands/managed-agent/_engine/credentials.ts @@ -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, @@ -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}`; + } } } diff --git a/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts b/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts new file mode 100644 index 00000000..8515a5e2 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts @@ -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 { + 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 { + 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, + }; +} diff --git a/packages/commands/src/commands/managed-agent/run.ts b/packages/commands/src/commands/managed-agent/run.ts new file mode 100644 index 00000000..5cdfc587 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/run.ts @@ -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: "", + description: "Task to run (required)", + required: true, + }, + instructions: { + type: "string", + valueHint: "", + description: "Role/system instructions for the remote agent (default: generic assistant)", + }, + model: { + type: "string", + valueHint: "", + description: `Model for the remote agent (default: ${DEFAULT_INLINE_MODEL})`, + }, + agent: { + type: "string", + valueHint: "", + 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 [--instructions ] [--model ] [--agent ]", + 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, + }); + } + }); + }), + ); + }, +}); diff --git a/packages/commands/src/commands/memory/add.ts b/packages/commands/src/commands/memory/add.ts index 160999fc..8b1b0537 100644 --- a/packages/commands/src/commands/memory/add.ts +++ b/packages/commands/src/commands/memory/add.ts @@ -28,6 +28,16 @@ const ADD_FLAGS = { valueHint: "", description: "Memory library ID (isolate memory space)", }, + projectId: { + type: "string", + valueHint: "", + description: "Memory extraction rule ID (defaults to the library's default rule)", + }, + metaData: { + type: "string", + valueHint: "", + description: 'Custom metadata JSON object: {"location":"Beijing"}', + }, } satisfies FlagsDef; type AddFlags = ParsedFlags; @@ -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); } diff --git a/packages/commands/src/commands/memory/list.ts b/packages/commands/src/commands/memory/list.ts index 6757fe6a..79e08d74 100644 --- a/packages/commands/src/commands/memory/list.ts +++ b/packages/commands/src/commands/memory/list.ts @@ -24,6 +24,11 @@ export default defineCommand({ }, page: { type: "number", valueHint: "", description: "Page number (default: 1)" }, memoryLibraryId: { type: "string", valueHint: "", description: "Memory library ID" }, + projectId: { + type: "string", + valueHint: "", + 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()}`; diff --git a/packages/commands/src/commands/memory/profile-delete.ts b/packages/commands/src/commands/memory/profile-delete.ts new file mode 100644 index 00000000..4cc78c7b --- /dev/null +++ b/packages/commands/src/commands/memory/profile-delete.ts @@ -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 [flags]", + flags: { + schemaId: { + type: "string", + valueHint: "", + description: "Profile schema ID (required)", + required: true, + }, + memoryLibraryId: { type: "string", valueHint: "", 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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/profile-detail.ts b/packages/commands/src/commands/memory/profile-detail.ts new file mode 100644 index 00000000..0eefc4bb --- /dev/null +++ b/packages/commands/src/commands/memory/profile-detail.ts @@ -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 [flags]", + flags: { + schemaId: { + type: "string", + valueHint: "", + description: "Profile schema ID (required)", + required: true, + }, + memoryLibraryId: { type: "string", valueHint: "", 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({ + 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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/profile-list.ts b/packages/commands/src/commands/memory/profile-list.ts new file mode 100644 index 00000000..990503b7 --- /dev/null +++ b/packages/commands/src/commands/memory/profile-list.ts @@ -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: "", description: "Memory library ID" }, + pageSize: { type: "number", valueHint: "", description: "Results per page (default: 10)" }, + page: { type: "number", valueHint: "", 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({ + 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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/profile-update.ts b/packages/commands/src/commands/memory/profile-update.ts new file mode 100644 index 00000000..7283fd22 --- /dev/null +++ b/packages/commands/src/commands/memory/profile-update.ts @@ -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: "", + description: "Profile schema ID (required)", + required: true, + }, + name: { type: "string", valueHint: "", description: "New schema name" }, + description: { type: "string", valueHint: "", description: "New schema description" }, + attributeOps: { + type: "string", + valueHint: "", + description: + 'Attribute operations JSON array: [{"op":"add","name":"plan"},{"op":"delete","attribute_id":"attr_1"}]', + }, + memoryLibraryId: { type: "string", valueHint: "", description: "Memory library ID" }, +} satisfies FlagsDef; +type UpdateFlags = ParsedFlags; + +export default defineCommand({ + description: "Update a profile schema's name, description, or attributes", + auth: "apiKey", + usageArgs: "--schema-id [--name ] [--attribute-ops ] [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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/search.ts b/packages/commands/src/commands/memory/search.ts index 6abccb4e..d29815be 100644 --- a/packages/commands/src/commands/memory/search.ts +++ b/packages/commands/src/commands/memory/search.ts @@ -24,6 +24,38 @@ const SEARCH_FLAGS = { description: "Number of results to return (default: 10)", }, memoryLibraryId: { type: "string", valueHint: "", description: "Memory library ID" }, + projectIds: { + type: "array", + valueHint: "", + description: "Memory extraction rule ID for hybrid retrieval (repeatable)", + }, + minScore: { + type: "number", + valueHint: "", + description: "Minimum similarity score, 0-1 (default: 0.3)", + }, + enableRerank: { + type: "boolean", + valueHint: "", + description: + "Rerank results. Also selects the billing tier: false bills lite, true bills pro (~50x). (default: true)", + }, + planVersion: { + type: "string", + valueHint: "", + description: + "Documented billing tier. The service currently honors --enable-rerank instead, so prefer that flag", + }, + enableJudge: { + type: "boolean", + valueHint: "", + description: "Enable the intent-discrimination callback (default: false)", + }, + enableRewrite: { + type: "boolean", + valueHint: "", + description: "Enable query rewriting (default: false)", + }, } satisfies FlagsDef; type SearchFlags = ParsedFlags; @@ -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); diff --git a/packages/commands/src/commands/memory/update.ts b/packages/commands/src/commands/memory/update.ts index 14cd3e99..a9ad5de9 100644 --- a/packages/commands/src/commands/memory/update.ts +++ b/packages/commands/src/commands/memory/update.ts @@ -1,5 +1,6 @@ import { defineCommand, + UsageError, memoryNodePath, detectOutputFormat, type MemoryNodeUpdateRequest, @@ -34,6 +35,16 @@ export default defineCommand({ valueHint: "", description: "Memory library ID (non-default library)", }, + timestamp: { + type: "number", + valueHint: "", + description: "When the remembered event happened (default: now)", + }, + metaData: { + type: "string", + valueHint: "", + 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); diff --git a/packages/commands/src/commands/token-plan/personal-key.ts b/packages/commands/src/commands/token-plan/personal-key.ts new file mode 100644 index 00000000..f52a2fed --- /dev/null +++ b/packages/commands/src/commands/token-plan/personal-key.ts @@ -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); + }, +}); diff --git a/packages/commands/src/commands/token-plan/personal-usage.ts b/packages/commands/src/commands/token-plan/personal-usage.ts new file mode 100644 index 00000000..5c7857e7 --- /dev/null +++ b/packages/commands/src/commands/token-plan/personal-usage.ts @@ -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, key: string): Record | undefined { + const val = obj[key]; + return val && typeof val === "object" && !Array.isArray(val) + ? (val as Record) + : undefined; +} + +/** Unwrap the console gateway `data.DataV2.data.data` envelope to the business payload. */ +function extract(result: Record): Record { + 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), + subscription: extract(subscription as Record), + addonSummary: extract(addon as Record), + }, + format, + ); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index a5af82d1..c63373af 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -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"; diff --git a/packages/commands/tests/credentials-bridge.test.ts b/packages/commands/tests/credentials-bridge.test.ts index 3a1fb982..f2bb81d0 100644 --- a/packages/commands/tests/credentials-bridge.test.ts +++ b/packages/commands/tests/credentials-bridge.test.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({})); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 84761f4a..1bcac903 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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", diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 119c7bda..a9be6f15 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -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) ---- diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 31bd04a7..6b66adc2 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -13,6 +13,7 @@ export { memoryNodePath, memorySearchPath, mcpWebSearchPath, + profileSchemaItemPath, profileSchemaPath, speechRecognizePath, speechSynthesizePath, diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index ef149255..0837cbc7 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -199,7 +199,15 @@ export function buildSources(flags: Partial): 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)), diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index c6c00ec0..cf98552b 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -305,11 +305,22 @@ export interface MemoryAddRequest { custom_content?: string; profile_schema?: string; memory_library_id?: string; + project_id?: string; + meta_data?: Record; +} + +/** 变更的记忆片段;`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; - 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; } // ---- 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; +} + +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 }>; }; } diff --git a/packages/dsh/.gitignore b/packages/dsh/.gitignore new file mode 100644 index 00000000..9d43549a --- /dev/null +++ b/packages/dsh/.gitignore @@ -0,0 +1,4 @@ +# build artifacts (regenerated by `pnpm build`) +client.bundle.js +dist/ +*.tgz diff --git a/packages/dsh/README.md b/packages/dsh/README.md new file mode 100644 index 00000000..ba6423d2 --- /dev/null +++ b/packages/dsh/README.md @@ -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-.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("/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 工具),已移除工具实现但保留共享逻辑作为参考。 diff --git a/packages/dsh/cordis.patch.yml b/packages/dsh/cordis.patch.yml new file mode 100644 index 00000000..65167e23 --- /dev/null +++ b/packages/dsh/cordis.patch.yml @@ -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 "/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 diff --git a/packages/dsh/package.json b/packages/dsh/package.json new file mode 100644 index 00000000..0f49125e --- /dev/null +++ b/packages/dsh/package.json @@ -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" + } + } +} diff --git a/packages/dsh/scripts/build-client.mjs b/packages/dsh/scripts/build-client.mjs new file mode 100644 index 00000000..8d210a56 --- /dev/null +++ b/packages/dsh/scripts/build-client.mjs @@ -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"); diff --git a/packages/dsh/src/client.ts b/packages/dsh/src/client.ts new file mode 100644 index 00000000..54158dbb --- /dev/null +++ b/packages/dsh/src/client.ts @@ -0,0 +1,1007 @@ +/** + * `bailian-cli-dsh` (Client half): renders the general "Bailian" webui — + * a settings.section page (凭证配置 / TokenPlan 用量 / 记忆库) plus a + * new-session welcome page. Built by `scripts/build-client.mjs` (esbuild) + * into the DSH ModuleLoader format (`client.bundle.js`). + * + * Runs in the browser; `React` is external (resolved by the ModuleLoader), + * styles are injected via the DOM, and data comes from the Host's + * `/bailian/*` webServer routes via `fetch`. + * + * @module bailian-cli-dsh/client + */ + +// @ts-nocheck — built by esbuild into the ModuleLoader client bundle; React is +// external (resolved by the browser ModuleLoader), styles injected via DOM. +import React from "react"; +import { featureByTitle } from "./features.ts"; + +/** Inject a