mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(dsh): add bailian-cli-dsh plugin bundle for DeepSeek Harness
Expose Bailian capabilities to dsh through its service seams as one package with six subpath plugin entries and a `dsh.bundle` patch: - TokenPlan as an LLM provider - bailian_vision_describe / bailian_image_generate tools over `bl` - knowledge-base retrieval as a WebSearchProvider (`bailian-kb`) - cross-session memory: tools, pre-step recall, turn-close persist - managed-agent as a SubagentProvider TokenPlan configures the base bundle's existing pi-ai row rather than mounting a second `dsh-llm-pi-ai` instance. A second instance cannot work: pi-ai re-declares its entire built-in provider catalog to `registerConfigurableProviders`, and that directory is global, so boot fails with a duplicate on `amazon-bedrock`. Routes and vision support were probed against the live gateway. qwen3.8-max, qwen3.7-plus, qwen3.6-flash and glm-5.2 read images; qwen3.7-max rejects them with HTTP 400; the DeepSeek routes accept image content without erroring yet stay blind. The DeepSeek entries therefore do not declare image input — claiming it would turn a clean refusal into a silently wrong answer — and `bailian_vision_describe` serves them by returning text instead. Also fix `bl memory` against the v2 API, each verified live: - `profile get` used /profiles, which returns HTTP 500. The documented and working endpoint is /user_profile. - `add` read `response.memory_ids`, which the service never returns. It returns `memory_nodes`, so text output always printed "IDs: none". - `MemoryNode.created_at`/`updated_at` are unix seconds, not strings, and `UserProfileResponse.profile` did not match the wire shape. - Add the missing request parameters: --meta-data, --project-id, --project-ids, --min-score, --enable-rerank, --plan-version, --enable-judge, --enable-rewrite, --timestamp. - Add `memory profile list|detail|update|delete`, covering the four v2 profile-schema operations the CLI was missing. `plan_version: lite` is ignored by the service and still bills pro; `enable_rerank: false` is what actually selects lite, which is ~50x cheaper per search. The CLI flag and the memory plugin both send the parameter that works. Disable pnpm's autoInstallPeers: the @deepseek-ai/dsh-* rc line peers on three packages that were never published to npm, which 404s the whole workspace install. Verified the existing packages still build. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
@@ -149,6 +153,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,253 @@
|
||||
# bailian-cli-dsh
|
||||
|
||||
把阿里云百炼(Model Studio)的能力接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的 profile bundle。
|
||||
|
||||
一个包提供 5 个插件行,外加对 base bundle 的 `llm-pi-ai` 行做一次配置覆盖:
|
||||
|
||||
| row id | 能力 | 默认 | 依赖 |
|
||||
| -------------------------------- | --------------------------------------------------------------- | ---- | --------------------- |
|
||||
| `llm-pi-ai`(覆盖 base 行) | 把百炼 TokenPlan 网关注册成 LLM provider(`bailian-tokenplan`) | 启用 | TokenPlan Key |
|
||||
| `bailian-tool-vision` | `bailian_vision_describe`:图片/视频理解 | 启用 | `bl` |
|
||||
| `bailian-tool-image` | `bailian_image_generate`:文生图 | 启用 | `bl` |
|
||||
| `bailian-web-search-rag` | 百炼知识库检索,注册为 `web_search` 的后端 | 停用 | 按量付费 Key + 知识库 |
|
||||
| `bailian-memory` | 跨会话长期记忆(tools + 自动检索/落库) | 停用 | 按量付费 Key |
|
||||
| `bailian-subagent-managed-agent` | 子 agent 跑在百炼托管运行时 | 停用 | `bl` + `agents.yaml` |
|
||||
|
||||
后三个默认停用是有意的:它们要么需要部署方特有的资源 ID,要么按次计费,不该在用户没配置时就生效。
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- Node ≥ 22.19(`dsh` 的要求)
|
||||
- `bl`(vision / image / subagent 三个插件通过子进程调它)
|
||||
|
||||
```sh
|
||||
npm install -g bailian-cli
|
||||
```
|
||||
|
||||
- 百炼 API Key。**注意有两类且不可混用**:
|
||||
|
||||
| 类型 | 前缀 | 能访问 | 不能访问 |
|
||||
| --------- | -------- | --------------------------------------- | -------------- |
|
||||
| TokenPlan | `sk-sp-` | TokenPlan 网关(LLM / vision / 文生图) | 记忆库、知识库 |
|
||||
| 按量付费 | `sk-ws-` | 记忆库、知识库、DashScope 全量接口 | TokenPlan 网关 |
|
||||
|
||||
两者互相返回 `401 InvalidApiKey`,所以本包用**两个不同的环境变量**,不会互相踩:
|
||||
|
||||
```sh
|
||||
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx # 只给 bailian-tokenplan provider
|
||||
export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG
|
||||
```
|
||||
|
||||
只有一类 Key 也能用,只是能力范围相应缩小。若只有 TokenPlan Key:
|
||||
|
||||
```sh
|
||||
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx
|
||||
export DASHSCOPE_API_KEY=sk-sp-xxx
|
||||
export DASHSCOPE_BASE_URL=https://token-plan.cn-beijing.maas.aliyuncs.com
|
||||
```
|
||||
|
||||
这样 LLM / vision / 文生图可用,memory 与 RAG 不可用(保持停用即可)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 安装到 `web` profile
|
||||
|
||||
`npx @deepseek-ai/dsh web` 是 `dsh --profile web` 的别名,所以要装进**名为 `web` 的 profile**,配置目录是 `~/.dsh/profiles/web/`(`$DSH_HOME` 可覆盖)。
|
||||
|
||||
本包尚未发布到 npm,先在本仓库打包:
|
||||
|
||||
```sh
|
||||
pnpm -F bailian-cli-dsh build
|
||||
cd packages/dsh && pnpm pack # 产出 bailian-cli-dsh-<version>.tgz
|
||||
```
|
||||
|
||||
装入 profile(`dsh plugin` 是 pnpm 的转发器,接受本地路径 / tarball / npm 包名 / git):
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh plugin --profile web add /absolute/path/to/bailian-cli-dsh-1.14.2.tgz
|
||||
```
|
||||
|
||||
因为 `package.json` 声明了 `dsh.bundle`,安装后会自动加入该 profile 的 bundle 层,无需手动改 `cordis.patch.yml`。
|
||||
|
||||
确认 5 个插入行都在,且 TokenPlan provider 已配到 `llm-pi-ai` 上:
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep -E 'bailian|tokenplan'
|
||||
```
|
||||
|
||||
启动:
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh web
|
||||
```
|
||||
|
||||
Web UI 在 http://127.0.0.1:3080。
|
||||
|
||||
> 发布到 npm 后直接 `npx @deepseek-ai/dsh plugin --profile web add bailian-cli-dsh`,跳过打包步骤。
|
||||
|
||||
---
|
||||
|
||||
## 3. 开箱能用的部分
|
||||
|
||||
装完不做任何配置就生效:
|
||||
|
||||
**LLM provider** — 模型选择器里出现 `bailian-tokenplan`,可选模型(已逐个实测):
|
||||
|
||||
| 模型 | 读图 |
|
||||
| ------------------------ | ------------------ |
|
||||
| `qwen3.8-max` | 是 |
|
||||
| `qwen3.7-plus` | 是 |
|
||||
| `qwen3.6-flash` | 是 |
|
||||
| `glm-5.2` | 是 |
|
||||
| `qwen3.7-max` | 否(传图直接 400) |
|
||||
| `deepseek-v4-pro` | 否 |
|
||||
| `deepseek-v4-flash-0731` | 否 |
|
||||
|
||||
**两个工具** — `bailian_vision_describe`、`bailian_image_generate`。
|
||||
|
||||
### 关于看图,有个坑值得知道
|
||||
|
||||
dsh 会在两处**提前**拦截图片:Web UI 粘图前会查当前模型的输入模态,`read_image` 也有同样的门禁。所以主模型选 DeepSeek 时,图片根本进不到对话里。
|
||||
|
||||
- 主模型选 `qwen3.8-max` 等标着"是"的 → 直接粘图,原生看图,不需要任何工具
|
||||
- 主模型选 DeepSeek → 让它调 `bailian_vision_describe`,工具返回**文字描述**,绕过模态门禁
|
||||
|
||||
DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见**(实测会回答 "None"),所以本包坚决没给它们声明 `input: [image]`——否则会从"明确拒绝"退化成"静默失明",更难排查。
|
||||
|
||||
`bailian_image_generate` 同理:模型能看图时返回内联图片,不能看图时降级为返回落盘路径,你可以接着用 vision 工具读它。文件不会被删除,正是为了这个衔接。
|
||||
|
||||
---
|
||||
|
||||
## 4. 开启可选插件
|
||||
|
||||
用户层配置写在 `~/.dsh/profiles/web/cordis.patch.yml`,按 row `id` 覆盖 bundle 的默认值。
|
||||
|
||||
> **一个必须记住的语义**:patch 是按 row **整体替换 `config`**,不是深合并。所以覆盖一行时要把该行完整的 config 重写一遍。
|
||||
|
||||
### 知识库检索(RAG)
|
||||
|
||||
注册 id 为 `bailian-kb` 的搜索后端,模型用它熟悉的 `web_search` 就能检索私域文档。
|
||||
|
||||
```yaml
|
||||
- id: bailian-web-search-rag
|
||||
disabled: false
|
||||
config:
|
||||
workspaceId: llm-xxxxxxxx # 百炼控制台工作空间 ID
|
||||
agentId: aid-xxxxxxxx # 知识库"检索服务"ID
|
||||
maxResults: 10
|
||||
# apiKey 省略则读 $DASHSCOPE_API_KEY
|
||||
```
|
||||
|
||||
一个实例对一个知识库(`WebSearchRequest` 只带 `query` / `maxResults`,agentId 只能来自配置)。要多个知识库就插多行不同 `id`。
|
||||
|
||||
**如果 profile 里还有别的搜索 provider**(base bundle 默认带 `web-search-deepseek`),必须显式指定用哪个,否则 dsh 报 `WEB_PROVIDER_AMBIGUOUS`:
|
||||
|
||||
```yaml
|
||||
- id: web
|
||||
config:
|
||||
searchProvider: bailian-kb
|
||||
```
|
||||
|
||||
### 长期记忆
|
||||
|
||||
dsh 自身没有跨会话记忆(`ctx.compaction` 只在单会话内压缩上下文)。开启后:两个工具 `bailian_memory_search` / `bailian_memory_add`,加上每个会话首轮自动检索注入、每轮结束自动落库。
|
||||
|
||||
```yaml
|
||||
- id: bailian-memory
|
||||
disabled: false
|
||||
config:
|
||||
userId: your-name # 省略则读 $BAILIAN_MEMORY_USER_ID,再退到系统用户名
|
||||
planVersion: lite
|
||||
topK: 10
|
||||
autoInject: true
|
||||
injectEveryTurn: false # 开启会变成每轮一次检索,成本相应上升
|
||||
autoPersist: true
|
||||
```
|
||||
|
||||
**费用**:记忆库自 2026-08-20 起商业化,add 与 search 按次计费,pro 档约为 lite 档的 50 倍。
|
||||
|
||||
实测发现一个与文档不符的地方:单独传 `plan_version: lite` 会被服务端忽略、仍按 pro 计费,真正生效的开关是 `enable_rerank: false`。本插件已按此处理——`planVersion: lite`(默认)会同时下发 `enable_rerank: false`,所以默认就是便宜的那档。
|
||||
|
||||
不想要自动行为、只保留手动工具:
|
||||
|
||||
```yaml
|
||||
- id: bailian-memory
|
||||
disabled: false
|
||||
config:
|
||||
userId: your-name
|
||||
autoInject: false
|
||||
autoPersist: false
|
||||
```
|
||||
|
||||
### 托管子 agent
|
||||
|
||||
把 dsh 的子 agent 派发到百炼云端运行,不占本地资源。
|
||||
|
||||
先在工作目录准备好清单并应用:
|
||||
|
||||
```sh
|
||||
bl managed-agent init
|
||||
bl managed-agent apply --yes
|
||||
```
|
||||
|
||||
再开启:
|
||||
|
||||
```yaml
|
||||
- id: bailian-subagent-managed-agent
|
||||
disabled: false
|
||||
config:
|
||||
file: agents.yaml
|
||||
agent: assistant
|
||||
provider: bailian
|
||||
timeoutMs: 600000
|
||||
```
|
||||
|
||||
两个已知限制:CLI 在会话结束时才一次性输出 JSON,所以**中途没有增量进度**,被取消时也拿不到部分输出;`prepareContinuable` 未实现,即只支持一次性委派,不支持多轮续聊。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证
|
||||
|
||||
```sh
|
||||
# 配置是否被正确合成(改完 patch 后先看这个)
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep -A5 bailian-memory
|
||||
|
||||
# bl 是否就绪
|
||||
bl auth status
|
||||
```
|
||||
|
||||
启动后逐项试:
|
||||
|
||||
- **LLM**:切到 `bailian-tokenplan / qwen3.8-max`,随便发一句
|
||||
- **原生看图**:同上模型,直接粘一张图提问
|
||||
- **间接看图**:切到 `deepseek-v4-pro`,让它用 `bailian_vision_describe` 读同一张图
|
||||
- **文生图**:让模型生成一张图
|
||||
- **RAG**:问一个只有知识库里才有答案的问题
|
||||
- **记忆**:会话 A 告诉它一个事实 → 关掉 → 新开会话 B 提问,看是否命中
|
||||
- **子 agent**:派发一个子任务
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
| 现象 | 原因 |
|
||||
| -------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key |
|
||||
| memory / RAG `401 InvalidApiKey` | `DASHSCOPE_API_KEY` 误填了 `sk-sp-` 的 TokenPlan Key |
|
||||
| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider,需在 `web` 行 pin `searchProvider` |
|
||||
| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 |
|
||||
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
|
||||
| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 |
|
||||
| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 卸载
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh plugin --profile web remove bailian-cli-dsh
|
||||
```
|
||||
|
||||
移除后 bundle 层会自动从 `dsh.profile.bundles` 摘掉;`~/.dsh/profiles/web/cordis.patch.yml` 里你手写的覆盖行需要自己清理。
|
||||
@@ -0,0 +1,97 @@
|
||||
# bailian-cli-dsh — Aliyun Model Studio (Bailian) as a dsh profile bundle.
|
||||
#
|
||||
# Applied over whatever the earlier layers composed: one config override of the
|
||||
# base bundle's dormant pi-ai adapter, then one insert of the Bailian tool rows.
|
||||
# Every inserted id is `bailian-`-prefixed so a user profile can address,
|
||||
# reconfigure, or disable any single capability without touching the others.
|
||||
# Remember that a later patch REPLACES a row's whole `config` rather than
|
||||
# merging into it, so restate the complete config when overriding.
|
||||
|
||||
# TokenPlan configures the base bundle's existing `llm-pi-ai` row instead of
|
||||
# inserting a second @deepseek-ai/dsh-llm-pi-ai instance. That plugin declares
|
||||
# pi-ai's entire built-in provider catalog to
|
||||
# `ctx.llm.registerConfigurableProviders` on every apply, and that registry
|
||||
# refuses an already-declared provider, so a second instance always fails the
|
||||
# whole tree with DUPLICATE_DIRECTORY on `amazon-bedrock`. Only adapter routes
|
||||
# are per-instance; the configurable-provider directory is global.
|
||||
#
|
||||
# Replacing this row's whole `config` costs nothing: the base mounts it dormant
|
||||
# with no config of its own. A profile that needs to drop or re-aim TokenPlan
|
||||
# restates this row's config rather than disabling an id.
|
||||
- id: llm-pi-ai
|
||||
config:
|
||||
providers:
|
||||
bailian-tokenplan:
|
||||
displayName: Aliyun Bailian TokenPlan
|
||||
api: openai-completions
|
||||
baseURL: https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
|
||||
# A dedicated name, not DASHSCOPE_API_KEY. TokenPlan keys (sk-sp-)
|
||||
# and pay-as-you-go keys (sk-ws-) are not interchangeable: this
|
||||
# gateway 401s a pay-as-you-go key, and the memory / knowledge-base
|
||||
# endpoints 401 a TokenPlan key. Sharing one variable would make
|
||||
# whichever plugin loses silently fail to authenticate.
|
||||
apiKeyEnv: BAILIAN_TOKENPLAN_API_KEY
|
||||
compat:
|
||||
thinkingFormat: qwen
|
||||
supportsReasoningEffort: true
|
||||
# Verified against GET /compatible-mode/v1/models plus a per-model
|
||||
# image probe on 2026-08-14. Only `id` is required; context window
|
||||
# and max tokens fall back to this provider's defaults.
|
||||
#
|
||||
# `input: [text, image]` is a claim about the endpoint, not a
|
||||
# checked fact, and it is what opens the Web UI paste path and
|
||||
# `read_image`. Every entry carrying it answered a colour question
|
||||
# about a test PNG correctly. The DeepSeek routes deliberately do
|
||||
# NOT carry it: they accept image content without erroring and then
|
||||
# answer "None", so declaring vision would turn a clean refusal
|
||||
# into a silently blind reply. Use `bailian_vision_describe` there.
|
||||
#
|
||||
# Omitted on purpose: wan2.7-image / wan2.7-image-pro are
|
||||
# generation-only (reach them through `bailian_image_generate`) and
|
||||
# qwen-audio-3.0-* are audio endpoints, not chat completions.
|
||||
models:
|
||||
- id: qwen3.8-max
|
||||
input: [text, image]
|
||||
- id: qwen3.7-plus
|
||||
input: [text, image]
|
||||
- id: qwen3.6-flash
|
||||
input: [text, image]
|
||||
# Rejects image content outright with HTTP 400.
|
||||
- id: qwen3.7-max
|
||||
- id: glm-5.2
|
||||
input: [text, image]
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
- id: deepseek-v4-pro
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
- id: deepseek-v4-flash-0731
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
|
||||
- insert:
|
||||
- id: bailian-tool-vision
|
||||
name: bailian-cli-dsh/tool-vision
|
||||
|
||||
- id: bailian-tool-image
|
||||
name: bailian-cli-dsh/tool-image
|
||||
|
||||
# Disabled by default: the knowledge base to query is deployment-specific,
|
||||
# and an enabled provider with no agentId would make `web_search` ambiguous
|
||||
# for everyone. Set workspaceId + agentId and flip `disabled` to use it.
|
||||
- id: bailian-web-search-rag
|
||||
name: bailian-cli-dsh/web-search-rag
|
||||
disabled: true
|
||||
config: {}
|
||||
|
||||
# Disabled by default: memory add/search are billed per call.
|
||||
- id: bailian-memory
|
||||
name: bailian-cli-dsh/memory
|
||||
disabled: true
|
||||
config: {}
|
||||
|
||||
# Disabled by default: requires an applied `agents.yaml` in the workspace.
|
||||
- id: bailian-subagent-managed-agent
|
||||
name: bailian-cli-dsh/subagent-managed-agent
|
||||
disabled: true
|
||||
config: {}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "bailian-cli-dsh",
|
||||
"version": "1.14.2",
|
||||
"description": "Aliyun Model Studio (Bailian) plugin bundle for DeepSeek Harness (dsh): TokenPlan LLM provider, knowledge-base RAG search, vision, image generation, long-term memory, and managed-agent subagents.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
"url": "https://github.com/modelstudioai/cli/issues"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Aliyun Model Studio",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/modelstudioai/cli.git",
|
||||
"directory": "packages/dsh"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"dist",
|
||||
"cordis.patch.yml"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./tool-vision": {
|
||||
"types": "./src/tool-vision/index.ts",
|
||||
"default": "./src/tool-vision/index.ts"
|
||||
},
|
||||
"./tool-image": {
|
||||
"types": "./src/tool-image/index.ts",
|
||||
"default": "./src/tool-image/index.ts"
|
||||
},
|
||||
"./web-search-rag": {
|
||||
"types": "./src/web-search-rag/index.ts",
|
||||
"default": "./src/web-search-rag/index.ts"
|
||||
},
|
||||
"./memory": {
|
||||
"types": "./src/memory/index.ts",
|
||||
"default": "./src/memory/index.ts"
|
||||
},
|
||||
"./subagent-managed-agent": {
|
||||
"types": "./src/subagent-managed-agent/index.ts",
|
||||
"default": "./src/subagent-managed-agent/index.ts"
|
||||
},
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": "./dist/index.mjs",
|
||||
"./tool-vision": "./dist/tool-vision/index.mjs",
|
||||
"./tool-image": "./dist/tool-image/index.mjs",
|
||||
"./web-search-rag": "./dist/web-search-rag/index.mjs",
|
||||
"./memory": "./dist/memory/index.mjs",
|
||||
"./subagent-managed-agent": "./dist/subagent-managed-agent/index.mjs",
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vp pack",
|
||||
"dev": "vp pack --watch",
|
||||
"test": "vp test",
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-web": "^0.1.0-rc.6",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "^6.0.2",
|
||||
"vite-plus": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-web": "^0.1.0-rc.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* bailian-cli-dsh — Aliyun Model Studio capabilities as a DeepSeek Harness
|
||||
* profile bundle. The package's substance is `cordis.patch.yml`, declared by
|
||||
* the `dsh.bundle.patch` manifest field and resolved by the profile composer
|
||||
* through that field; this module carries no runtime API.
|
||||
* @module bailian-cli-dsh
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/memory`: cross-session long-term memory backed by Bailian's
|
||||
* hosted memory library.
|
||||
*
|
||||
* dsh has no memory seam — `ctx.compaction` only summarizes within one
|
||||
* session's context window and never writes across sessions — so this plugin
|
||||
* supplies the whole capability: two tools for deliberate reads and writes,
|
||||
* plus automatic retrieval and persistence around each turn.
|
||||
*
|
||||
* Calls go straight to DashScope rather than through `bl memory`, because the
|
||||
* v2 API exposes retrieval controls (`min_score`, `plan_version`,
|
||||
* `enable_rerank`, `meta_data`) the CLI does not surface.
|
||||
*
|
||||
* BILLING: add and search are charged per call, and `pro` costs roughly fifty
|
||||
* times `lite` per search. Automatic behaviour therefore defaults to `lite`,
|
||||
* retrieves once per session rather than once per turn, and never requests
|
||||
* profile extraction unless a schema is configured.
|
||||
*
|
||||
* @module bailian-cli-dsh/memory
|
||||
*/
|
||||
import { userInfo } from "node:os";
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { Agent, PreStepDecision } from "@deepseek-ai/dsh-agent";
|
||||
import type {} from "@deepseek-ai/dsh-agent";
|
||||
import type { ContentBlock, Message } from "@deepseek-ai/dsh-llm";
|
||||
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { dashScopeFetch, resolveApiKey, resolveBaseUrl } from "../shared/http.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-memory";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "agents"];
|
||||
|
||||
export interface Config {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
/** Memory entity id. Falls back to `$BAILIAN_MEMORY_USER_ID`, then the OS user. */
|
||||
userId?: string;
|
||||
memoryLibraryId?: string;
|
||||
projectId?: string;
|
||||
/** Profile template id; omitting it skips profile extraction (and its cost). */
|
||||
profileSchema?: string;
|
||||
/** `lite` disables rerank and is ~50x cheaper per search. */
|
||||
planVersion?: "lite" | "pro";
|
||||
topK?: number;
|
||||
minScore?: number;
|
||||
/** Retrieve relevant memories and inject them into the conversation. */
|
||||
autoInject?: boolean;
|
||||
/** Retrieve on every turn instead of once per session. Costs one search per turn. */
|
||||
injectEveryTurn?: boolean;
|
||||
/** Persist each turn's new messages when the turn closes. */
|
||||
autoPersist?: boolean;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z.string().role("secret").description("DashScope key; defaults to $DASHSCOPE_API_KEY."),
|
||||
baseUrl: z.string().description("DashScope base URL override."),
|
||||
userId: z.string().description("Memory entity id owning these memories."),
|
||||
memoryLibraryId: z.string().description("Memory library id; defaults to the account default."),
|
||||
projectId: z.string().description("Memory extraction rule id."),
|
||||
profileSchema: z.string().description("Profile template id; enables profile extraction."),
|
||||
planVersion: z
|
||||
.union(["lite", "pro"] as const)
|
||||
.description("Search strategy; pro enables rerank at ~50x the cost."),
|
||||
topK: z.natural().description("Maximum memories to recall (1-100)."),
|
||||
minScore: z.number().description("Minimum similarity score, 0-1."),
|
||||
autoInject: z.boolean().description("Inject recalled memories automatically."),
|
||||
injectEveryTurn: z.boolean().description("Retrieve every turn instead of once per session."),
|
||||
autoPersist: z.boolean().description("Persist new messages when a turn closes."),
|
||||
});
|
||||
|
||||
const DEFAULT_TOP_K = 10;
|
||||
const DEFAULT_PLAN_VERSION = "lite";
|
||||
const MEMORY_PATH = "/api/v2/apps/memory";
|
||||
|
||||
interface MemoryNode {
|
||||
memory_node_id?: string;
|
||||
content?: string;
|
||||
event?: string;
|
||||
old_content?: string;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
}
|
||||
|
||||
interface MemoryResponse {
|
||||
request_id?: string;
|
||||
memory_nodes?: readonly MemoryNode[];
|
||||
}
|
||||
|
||||
interface ChatTurn {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Resolution order: explicit config, then environment, then the OS user. */
|
||||
function resolveUserId(ctx: Context, config: Config): string {
|
||||
if (config.userId !== undefined && config.userId.length > 0) return config.userId;
|
||||
const fromEnv = ctx.get("launchEnvironment")?.get("BAILIAN_MEMORY_USER_ID")?.value;
|
||||
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
||||
return userInfo().username;
|
||||
}
|
||||
|
||||
function textOf(content: readonly ContentBlock[]): string {
|
||||
return content
|
||||
.filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Plain user/assistant exchanges; tool traffic and injected context are not memories. */
|
||||
function conversationTurns(messages: readonly Message[]): ChatTurn[] {
|
||||
const turns: ChatTurn[] = [];
|
||||
for (const message of messages) {
|
||||
if (message.role !== "user" && message.role !== "assistant") continue;
|
||||
if (message.role === "user" && message.source.kind !== "user") continue;
|
||||
const text = textOf(message.content);
|
||||
if (text.length > 0) turns.push({ role: message.role, content: text });
|
||||
}
|
||||
return turns;
|
||||
}
|
||||
|
||||
class MemoryClient {
|
||||
constructor(
|
||||
private readonly apiKey: string,
|
||||
private readonly baseUrl: string,
|
||||
private readonly config: Config,
|
||||
private readonly userId: string,
|
||||
) {}
|
||||
|
||||
private shared(): Record<string, unknown> {
|
||||
return {
|
||||
user_id: this.userId,
|
||||
...(this.config.memoryLibraryId !== undefined
|
||||
? { memory_library_id: this.config.memoryLibraryId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
async add(
|
||||
messages: readonly ChatTurn[],
|
||||
signal: AbortSignal | undefined,
|
||||
overrides?: { customContent?: string; metaData?: Record<string, unknown> },
|
||||
): Promise<MemoryResponse> {
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}${MEMORY_PATH}/add`,
|
||||
method: "POST",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
body: {
|
||||
...this.shared(),
|
||||
...(overrides?.customContent !== undefined
|
||||
? { custom_content: overrides.customContent }
|
||||
: { messages }),
|
||||
...(this.config.projectId !== undefined ? { project_id: this.config.projectId } : {}),
|
||||
...(this.config.profileSchema !== undefined
|
||||
? { profile_schema: this.config.profileSchema }
|
||||
: {}),
|
||||
...(overrides?.metaData !== undefined ? { meta_data: overrides.metaData } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async search(
|
||||
messages: readonly ChatTurn[],
|
||||
signal: AbortSignal | undefined,
|
||||
overrides?: { topK?: number; minScore?: number; planVersion?: "lite" | "pro" },
|
||||
): Promise<MemoryResponse> {
|
||||
const planVersion = overrides?.planVersion ?? this.config.planVersion ?? DEFAULT_PLAN_VERSION;
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}${MEMORY_PATH}/memory_nodes/search`,
|
||||
method: "POST",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
body: {
|
||||
...this.shared(),
|
||||
messages,
|
||||
top_k: overrides?.topK ?? this.config.topK ?? DEFAULT_TOP_K,
|
||||
...((overrides?.minScore ?? this.config.minScore) !== undefined
|
||||
? { min_score: overrides?.minScore ?? this.config.minScore }
|
||||
: {}),
|
||||
// `enable_rerank` is what actually selects the billing tier. Sending
|
||||
// `plan_version: lite` alone still bills `pro` (verified against the
|
||||
// live API), despite the documented precedence, and pro costs ~50x
|
||||
// more per search. Send both: the flag that works, plus the
|
||||
// documented field in case the server-side precedence is fixed.
|
||||
enable_rerank: planVersion === "pro",
|
||||
plan_version: planVersion,
|
||||
...(this.config.projectId !== undefined ? { project_ids: [this.config.projectId] } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderMemories(nodes: readonly MemoryNode[]): string {
|
||||
const lines = nodes
|
||||
.map((node) => node.content?.trim())
|
||||
.filter((content): content is string => content !== undefined && content.length > 0)
|
||||
.map((content) => `- ${content}`);
|
||||
return `What you remember about this user from earlier sessions:\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
function registerTools(ctx: Context, client: MemoryClient): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_memory_search",
|
||||
description:
|
||||
"Recall facts stored about this user in earlier sessions. Use when the user refers " +
|
||||
"to prior context, preferences, or decisions you have no record of in this session.",
|
||||
parameters: {
|
||||
query: { type: "string", required: true, description: "What to recall." },
|
||||
top_k: { type: "integer", description: "Maximum memories to return (1-100)." },
|
||||
min_score: { type: "number", description: "Minimum similarity score, 0-1." },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
memories: {
|
||||
type: "array",
|
||||
required: true,
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: { type: "string", required: true },
|
||||
content: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
value.memories.length === 0
|
||||
? "No relevant memories."
|
||||
: value.memories.map((memory) => `- ${memory.content}`).join("\n"),
|
||||
},
|
||||
],
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const response = await client.search([{ role: "user", content: args.query }], exec.signal, {
|
||||
...(args.top_k !== undefined ? { topK: args.top_k } : {}),
|
||||
...(args.min_score !== undefined ? { minScore: args.min_score } : {}),
|
||||
});
|
||||
return {
|
||||
memories: (response.memory_nodes ?? []).map((node) => ({
|
||||
id: node.memory_node_id ?? "",
|
||||
content: node.content ?? "",
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_memory_add",
|
||||
description:
|
||||
"Store a durable fact about this user so later sessions can recall it. Use for stable " +
|
||||
"preferences, decisions, and context — not for transient task state.",
|
||||
parameters: {
|
||||
content: { type: "string", required: true, description: "The fact to remember." },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: { stored: { type: "integer", required: true } },
|
||||
},
|
||||
render: (_args, value) => [
|
||||
{ type: "text", text: `Stored ${value.stored} memory fragment(s).` },
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const response = await client.add([], exec.signal, { customContent: args.content });
|
||||
return { stored: (response.memory_nodes ?? []).length };
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function registerLifecycle(ctx: Context, client: MemoryClient, config: Config): void {
|
||||
const injected = new WeakSet<Agent>();
|
||||
const persistedUpTo = new WeakMap<Agent, number>();
|
||||
|
||||
if (config.autoInject !== false) {
|
||||
ctx.on(
|
||||
"agent/pre-step",
|
||||
async ({ agent, messages, signal }, next): Promise<PreStepDecision> => {
|
||||
const decision = await next();
|
||||
if (decision.kind !== "enter") return decision;
|
||||
if (injected.has(agent) && config.injectEveryTurn !== true) return decision;
|
||||
|
||||
const query = textOf(messages.flatMap((message) => message.content));
|
||||
if (query.length === 0) return decision;
|
||||
|
||||
let nodes: readonly MemoryNode[];
|
||||
try {
|
||||
const response = await client.search([{ role: "user", content: query }], signal);
|
||||
nodes = response.memory_nodes ?? [];
|
||||
} catch {
|
||||
// Recall is an enhancement; a memory-service outage must not stop the turn.
|
||||
return decision;
|
||||
}
|
||||
injected.add(agent);
|
||||
if (nodes.length === 0) return decision;
|
||||
|
||||
const text = renderMemories(nodes);
|
||||
return {
|
||||
...decision,
|
||||
messages: [
|
||||
...decision.messages,
|
||||
createUserMessage({
|
||||
content: [{ type: "text", text }],
|
||||
source: {
|
||||
kind: "plugin",
|
||||
plugin: name,
|
||||
form: "snapshot",
|
||||
sections: [{ name, text }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
{ prepend: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (config.autoPersist !== false) {
|
||||
ctx.on("agent/turn-stopping", async ({ agent, signal }): Promise<void> => {
|
||||
const turns = conversationTurns(agent.session.deriveMessages());
|
||||
const from = persistedUpTo.get(agent) ?? 0;
|
||||
const fresh = turns.slice(from);
|
||||
if (fresh.length === 0) return;
|
||||
persistedUpTo.set(agent, turns.length);
|
||||
try {
|
||||
await client.add(fresh, signal);
|
||||
} catch {
|
||||
// Persistence is best-effort; never fail a turn over it.
|
||||
persistedUpTo.set(agent, from);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const apiKey = resolveApiKey(ctx, config.apiKey);
|
||||
if (apiKey === undefined) {
|
||||
throw new Error(
|
||||
"bailian-memory: no DashScope API key. Set $DASHSCOPE_API_KEY or configure `apiKey`.",
|
||||
);
|
||||
}
|
||||
const client = new MemoryClient(
|
||||
apiKey,
|
||||
resolveBaseUrl(ctx, config.baseUrl),
|
||||
config,
|
||||
resolveUserId(ctx, config),
|
||||
);
|
||||
registerTools(ctx, client);
|
||||
registerLifecycle(ctx, client, config);
|
||||
}
|
||||
@@ -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,98 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com";
|
||||
|
||||
/** 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 from explicit config, then the launch environment
|
||||
* (process env, project `.env`, harness-home `.env`).
|
||||
*/
|
||||
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,205 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/subagent-managed-agent`: runs child agents on Bailian's
|
||||
* hosted managed-agent runtime instead of in this process, through
|
||||
* `bl managed-agent session run`.
|
||||
*
|
||||
* Out-of-process delegation is an established shape here — `subagent-acp` and
|
||||
* `subagent-codex` do the same over their own transports. Going through the
|
||||
* CLI keeps `agents.yaml` resolution, provider selection, and SSE decoding in
|
||||
* one place.
|
||||
*
|
||||
* Two honest limits. The CLI buffers the whole session and emits it at exit,
|
||||
* so no incremental progress reaches the parent and a cancelled run yields no
|
||||
* partial output. And `prepareContinuable` is deliberately absent: method
|
||||
* presence IS the continuable capability, and multi-turn continuation is not
|
||||
* wired up yet.
|
||||
*
|
||||
* @module bailian-cli-dsh/subagent-managed-agent
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
|
||||
import { SessionId } from "@deepseek-ai/dsh-session";
|
||||
import type {
|
||||
ResolvedSubagentStartRequest,
|
||||
SubagentCapabilities,
|
||||
SubagentProvider,
|
||||
SubagentResult,
|
||||
SubagentRun,
|
||||
} from "@deepseek-ai/dsh-subagent";
|
||||
import type {} from "@deepseek-ai/dsh-fs";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { runBlJson } from "../shared/bl.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-subagent-managed-agent";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["subagents", "subprocess", "fs"];
|
||||
|
||||
/** Registry name callers select this transport by. */
|
||||
export const BAILIAN_MANAGED_AGENT_PROVIDER = "bailian-managed-agent";
|
||||
|
||||
export interface Config {
|
||||
/** Manifest passed as `--file`; must already be applied. */
|
||||
file?: string;
|
||||
/** Agent name within the manifest. */
|
||||
agent?: string;
|
||||
/** Backing provider understood by `bl managed-agent`. */
|
||||
provider?: string;
|
||||
/** Cooperative budget for one hosted run. */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
file: z.string().description("Path to agents.yaml; defaults to the CLI's own default."),
|
||||
agent: z.string().description("Agent name declared in the manifest."),
|
||||
provider: z.string().description("Managed-agent backing provider."),
|
||||
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
|
||||
});
|
||||
|
||||
const DEFAULT_MANIFEST = "agents.yaml";
|
||||
const DEFAULT_TIMEOUT_MS = 600_000;
|
||||
|
||||
/** A one-shot transport supports none of the start-time features. */
|
||||
const CAPABILITIES: SubagentCapabilities = {
|
||||
outputSchema: false,
|
||||
depthLimit: false,
|
||||
toolFilter: false,
|
||||
persona: false,
|
||||
};
|
||||
|
||||
interface SessionEvent {
|
||||
type?: string;
|
||||
content?: unknown;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
interface SessionRunResponse {
|
||||
session_id?: string;
|
||||
events?: readonly SessionEvent[];
|
||||
}
|
||||
|
||||
function promptText(blocks: readonly ContentBlock[]): string {
|
||||
return blocks
|
||||
.filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Assistant-visible text of a finished hosted session. */
|
||||
function assistantOutput(events: readonly SessionEvent[]): ContentBlock[] {
|
||||
const text = events
|
||||
.filter((event) => event.type === "message" && typeof event.content === "string")
|
||||
.map((event) => event.content as string)
|
||||
.join("\n")
|
||||
.trim();
|
||||
return text.length > 0 ? [{ type: "text", text }] : [];
|
||||
}
|
||||
|
||||
function isAbort(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === "AbortError";
|
||||
}
|
||||
|
||||
class BailianManagedAgentProvider implements SubagentProvider {
|
||||
readonly name = BAILIAN_MANAGED_AGENT_PROVIDER;
|
||||
readonly capabilities = CAPABILITIES;
|
||||
readonly inheritsParentContext = false;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: Context,
|
||||
private readonly config: Config,
|
||||
) {}
|
||||
|
||||
async start(request: ResolvedSubagentStartRequest): Promise<SubagentRun> {
|
||||
const cwd = request.parent.session.header.cwd ?? process.cwd();
|
||||
const manifest = this.config.file ?? DEFAULT_MANIFEST;
|
||||
|
||||
// Pre-publication: a missing manifest is the common misconfiguration and
|
||||
// deserves a start-time rejection rather than a failed run.
|
||||
const target = await this.ctx.fs.resolve(manifest, { cwd, signal: request.signal });
|
||||
const info = await this.ctx.fs.stat(target, request.signal);
|
||||
if (info === undefined) {
|
||||
throw new Error(
|
||||
`bailian-managed-agent: no manifest at "${target.displayPath}". Create one with ` +
|
||||
`\`bl managed-agent init\` and apply it with \`bl managed-agent apply --yes\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
const prompt = promptText(request.prompt);
|
||||
if (prompt.length === 0) {
|
||||
throw new Error("bailian-managed-agent: the prompt carried no text content.");
|
||||
}
|
||||
|
||||
const argv = ["managed-agent", "session", "run", "--prompt", prompt, "--file", manifest];
|
||||
if (this.config.agent !== undefined) argv.push("--agent", this.config.agent);
|
||||
if (this.config.provider !== undefined) argv.push("--provider", this.config.provider);
|
||||
|
||||
const controller = new AbortController();
|
||||
const abort = (): void => controller.abort();
|
||||
request.signal.addEventListener("abort", abort, { once: true });
|
||||
|
||||
// The seam has no deadline of its own — cancellation arrives only through
|
||||
// the caller's signal — so the transport owns one, or a wedged hosted
|
||||
// session never settles.
|
||||
const deadline = AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
const combined = AbortSignal.any([controller.signal, deadline]);
|
||||
|
||||
// Ownership transfers on fulfillment, so every later failure settles
|
||||
// through `result` — which must not reject for child-level problems.
|
||||
const result = this.execute(argv, cwd, combined, deadline).finally(() => {
|
||||
request.signal.removeEventListener("abort", abort);
|
||||
});
|
||||
|
||||
let disposal: Promise<void> | undefined;
|
||||
return {
|
||||
id: SessionId(`bailian-managed-agent:${crypto.randomUUID()}`),
|
||||
localAgent: undefined,
|
||||
result,
|
||||
dispose: (): Promise<void> => {
|
||||
disposal ??= (async (): Promise<void> => {
|
||||
controller.abort();
|
||||
await result;
|
||||
})();
|
||||
return disposal;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async execute(
|
||||
argv: readonly string[],
|
||||
cwd: string,
|
||||
signal: AbortSignal,
|
||||
deadline: AbortSignal,
|
||||
): Promise<SubagentResult> {
|
||||
try {
|
||||
const response = await runBlJson<SessionRunResponse>(this.ctx, argv, {
|
||||
cwd,
|
||||
signal,
|
||||
graceMs: 10_000,
|
||||
});
|
||||
return { output: assistantOutput(response.events ?? []), stopReason: "completed" };
|
||||
} catch (error) {
|
||||
if (deadline.aborted) {
|
||||
return {
|
||||
output: [
|
||||
{
|
||||
type: "text",
|
||||
text: `the hosted session exceeded ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms and was terminated`,
|
||||
},
|
||||
],
|
||||
stopReason: "error",
|
||||
};
|
||||
}
|
||||
// The CLI emits its JSON envelope only at exit, so a cancelled run has
|
||||
// no partial output to salvage.
|
||||
if (isAbort(error) || signal.aborted) return { output: [], stopReason: "aborted" };
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
return { output: [{ type: "text", text: reason }], stopReason: "error" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.subagents.registerProvider(new BailianManagedAgentProvider(ctx, config));
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tool-image`: image generation through `bl image generate`.
|
||||
*
|
||||
* Delegating to the CLI keeps the async-task polling, model-to-endpoint
|
||||
* routing, and artifact download in one place rather than restating them here.
|
||||
*
|
||||
* Generated files are committed to `ctx.attachments` and returned as
|
||||
* `ImageBlock`s when the calling route declares image input. When it does not
|
||||
* — DeepSeek routes never do — the tool degrades to reporting the saved paths
|
||||
* instead of failing, so the model can hand one to `bailian_vision_describe`.
|
||||
* For that fallback to work the files must survive the call, so this tool
|
||||
* deliberately does not delete what the CLI wrote.
|
||||
*
|
||||
* @module bailian-cli-dsh/tool-image
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { ImageAttachmentRef, ImageMediaType } from "@deepseek-ai/dsh-attachment";
|
||||
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
||||
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
|
||||
import type {} from "@deepseek-ai/dsh-fs";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type { ToolExecution } from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { runBlJson } from "../shared/bl.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tool-image";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "subprocess", "fs"];
|
||||
|
||||
export interface Config {
|
||||
/** Image model passed to `bl image generate --model`. */
|
||||
model?: string;
|
||||
/** Directory for generated files; defaults to the CLI's own output dir. */
|
||||
outDir?: string;
|
||||
/** Cooperative budget; async models poll until the task succeeds. */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
model: z.string().description("Image model; defaults to the CLI's own default."),
|
||||
outDir: z.string().description("Directory for generated files."),
|
||||
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
|
||||
});
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 300_000;
|
||||
const MAX_IMAGES = 6;
|
||||
|
||||
const MEDIA_TYPE_BY_EXTENSION: Readonly<Record<string, ImageMediaType>> = {
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
webp: "image/webp",
|
||||
gif: "image/gif",
|
||||
};
|
||||
|
||||
interface ImageGenerateResponse {
|
||||
urls?: readonly string[];
|
||||
saved?: readonly string[];
|
||||
total?: number;
|
||||
}
|
||||
|
||||
/** One committed image, stored as plain JSON so `render` stays pure. */
|
||||
interface CommittedImage {
|
||||
attachmentId: string;
|
||||
mediaType: ImageMediaType;
|
||||
bytes: number;
|
||||
width: number;
|
||||
height: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function mediaTypeOf(path: string): ImageMediaType | undefined {
|
||||
const extension = path.split(".").pop()?.toLowerCase();
|
||||
return extension === undefined ? undefined : MEDIA_TYPE_BY_EXTENSION[extension];
|
||||
}
|
||||
|
||||
function attachmentRefOf(image: CommittedImage): ImageAttachmentRef {
|
||||
return {
|
||||
attachmentId: AttachmentId(image.attachmentId),
|
||||
mediaType: image.mediaType,
|
||||
bytes: image.bytes,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the calling route declares image input. Unlike `read_image`'s hard
|
||||
* gate this only reports, because an unroutable or text-only model is a reason
|
||||
* to fall back to paths rather than to refuse generating anything.
|
||||
*/
|
||||
async function routeAcceptsImages(ctx: Context, exec: ToolExecution): Promise<boolean> {
|
||||
const routed = exec.agent?.session.requestHeader()?.config;
|
||||
const provider = routed?.provider ?? exec.agent?.options.provider;
|
||||
const model = routed?.model ?? exec.agent?.options.model;
|
||||
const llm = ctx.get("llm");
|
||||
if (provider === undefined || model === undefined || llm === undefined) return false;
|
||||
try {
|
||||
const active = await llm.resolveModelInfo(provider, model, exec.signal);
|
||||
return active.inputModalities?.includes("image") === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_image_generate",
|
||||
description:
|
||||
"Generate images from a text prompt using Aliyun Bailian (Qwen-Image / Wan). " +
|
||||
"Files are written to disk and returned inline when the active model can view " +
|
||||
"images; otherwise the saved paths are reported and you can inspect one with " +
|
||||
"`bailian_vision_describe`.",
|
||||
parameters: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "What to depict. Be specific about subject, style, and composition.",
|
||||
},
|
||||
model: { type: "string", description: "Override the configured image model." },
|
||||
size: {
|
||||
type: "string",
|
||||
description: 'Aspect ratio such as "1:1" / "16:9", or explicit pixels as "1024*1024".',
|
||||
},
|
||||
n: {
|
||||
type: "integer",
|
||||
description: `How many images to generate (1-${MAX_IMAGES}).`,
|
||||
},
|
||||
negative_prompt: { type: "string", description: "What to avoid depicting." },
|
||||
seed: { type: "integer", description: "Seed for reproducible generation." },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
images: {
|
||||
type: "array",
|
||||
required: true,
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
attachmentId: { type: "string", required: true },
|
||||
mediaType: { type: "string", required: true },
|
||||
bytes: { type: "integer", required: true },
|
||||
width: { type: "integer", required: true },
|
||||
height: { type: "integer", required: true },
|
||||
path: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: { type: "array", required: true, items: { type: "string" } },
|
||||
urls: { type: "array", required: true, items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => {
|
||||
const paths = value.paths.join("\n");
|
||||
if (value.images.length === 0) {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Generated ${value.paths.length} image(s); the active model cannot view ` +
|
||||
`images, so they are on disk only. Use bailian_vision_describe to inspect ` +
|
||||
`one.\n${paths}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
const blocks: ContentBlock[] = [
|
||||
{ type: "text", text: `Generated ${value.images.length} image(s):\n${paths}` },
|
||||
];
|
||||
for (const image of value.images) {
|
||||
blocks.push({ type: "image", attachment: attachmentRefOf(image as CommittedImage) });
|
||||
}
|
||||
return blocks;
|
||||
},
|
||||
},
|
||||
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
async execute(args, exec) {
|
||||
if (args.n !== undefined && (args.n < 1 || args.n > MAX_IMAGES)) {
|
||||
throw new Error(`bailian_image_generate accepts n between 1 and ${MAX_IMAGES}.`);
|
||||
}
|
||||
|
||||
const cwd = exec.agent?.session.header.cwd ?? process.cwd();
|
||||
const argv = ["image", "generate", "--prompt", args.prompt];
|
||||
const model = args.model ?? config.model;
|
||||
if (model !== undefined) argv.push("--model", model);
|
||||
if (args.size !== undefined) argv.push("--size", args.size);
|
||||
if (args.n !== undefined) argv.push("--n", String(args.n));
|
||||
if (args.negative_prompt !== undefined)
|
||||
argv.push("--negative-prompt", args.negative_prompt);
|
||||
if (args.seed !== undefined) argv.push("--seed", String(args.seed));
|
||||
if (config.outDir !== undefined) argv.push("--out-dir", config.outDir);
|
||||
|
||||
const response = await runBlJson<ImageGenerateResponse>(ctx, argv, {
|
||||
cwd,
|
||||
signal: exec.signal,
|
||||
});
|
||||
|
||||
const paths = [...(response.saved ?? [])];
|
||||
const urls = [...(response.urls ?? [])];
|
||||
if (paths.length === 0) {
|
||||
throw new Error("bl image generate reported no saved files.");
|
||||
}
|
||||
|
||||
const attachments = ctx.get("attachments");
|
||||
const images: CommittedImage[] = [];
|
||||
if (attachments !== undefined && (await routeAcceptsImages(ctx, exec))) {
|
||||
const byteCap = Math.min(
|
||||
attachments.imageLimits.maxImageBytes,
|
||||
attachments.imageLimits.maxMessageImageBytes,
|
||||
);
|
||||
for (const path of paths) {
|
||||
const mediaType = mediaTypeOf(path);
|
||||
if (mediaType === undefined || !attachments.imageLimits.mediaTypes.includes(mediaType))
|
||||
continue;
|
||||
const target = await ctx.fs.resolve(path, { cwd, signal: exec.signal });
|
||||
const data = await ctx.fs.readBytes(target, exec.signal, byteCap);
|
||||
const ref = await attachments.saveImage({ data, mediaType, name: target.displayPath });
|
||||
images.push({
|
||||
attachmentId: ref.attachmentId,
|
||||
mediaType: ref.mediaType,
|
||||
bytes: ref.bytes,
|
||||
width: ref.width,
|
||||
height: ref.height,
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { images, paths, urls };
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tool-vision`: image and video understanding through
|
||||
* `bl vision describe` (Qwen-VL).
|
||||
*
|
||||
* The tool returns TEXT, never an `ImageBlock` — that is deliberate. dsh gates
|
||||
* image content on the active route's declared input modalities in two places
|
||||
* before a plugin ever sees it (the Web UI paste pre-check and `read_image`),
|
||||
* so a text-only main model such as DeepSeek cannot receive pictures at all.
|
||||
* Handing back a description instead gives those routes vision indirectly.
|
||||
* A genuinely multimodal route does not need this tool and should paste images
|
||||
* directly.
|
||||
*
|
||||
* @module bailian-cli-dsh/tool-vision
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type {} from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { runBlJson } from "../shared/bl.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tool-vision";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "subprocess"];
|
||||
|
||||
export interface Config {
|
||||
/** Vision model passed to `bl vision describe --model`. */
|
||||
model?: string;
|
||||
/** Cooperative budget; video understanding uploads and is slow. */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
model: z.string().description("Vision model; defaults to the CLI's own default."),
|
||||
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
|
||||
});
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 180_000;
|
||||
|
||||
/** The OpenAI-shaped body `bl vision describe --output json` passes through. */
|
||||
interface VisionResponse {
|
||||
model?: string;
|
||||
request_id?: string;
|
||||
choices?: readonly {
|
||||
message?: { content?: unknown };
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Chat content is a string or an array of typed parts; keep only the text. */
|
||||
function readContent(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.map((part) =>
|
||||
typeof part === "object" && part !== null && "text" in part
|
||||
? String((part as { text: unknown }).text)
|
||||
: "",
|
||||
)
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_vision_describe",
|
||||
description:
|
||||
"Understand an image or video using Aliyun Bailian's Qwen-VL models. " +
|
||||
"Accepts a local file path or a URL and returns a text description, so it works " +
|
||||
"even when the active model cannot take image input. Ask a specific question " +
|
||||
"through `prompt` (for example OCR, chart reading, or object identification) " +
|
||||
"instead of relying on the generic default.",
|
||||
parameters: {
|
||||
image: {
|
||||
type: "string",
|
||||
description: "Local image path or http(s)/oss URL. Provide this or `video`.",
|
||||
},
|
||||
video: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description:
|
||||
"Video file paths or URLs (mp4/mov/avi/mkv/webm). Local files are uploaded first.",
|
||||
},
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "Question about the content. Defaults to a plain description request.",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
description: "Override the configured vision model.",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
description: { type: "string", required: true },
|
||||
model: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: "text", text: value.description }],
|
||||
},
|
||||
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const videos = args.video ?? [];
|
||||
if (args.image === undefined && videos.length === 0) {
|
||||
throw new Error("bailian_vision_describe requires `image` or `video`.");
|
||||
}
|
||||
|
||||
const argv = ["vision", "describe"];
|
||||
if (args.image !== undefined) argv.push("--image", args.image);
|
||||
for (const video of videos) argv.push("--video", video);
|
||||
if (args.prompt !== undefined) argv.push("--prompt", args.prompt);
|
||||
|
||||
const model = args.model ?? config.model;
|
||||
if (model !== undefined) argv.push("--model", model);
|
||||
|
||||
const response = await runBlJson<VisionResponse>(ctx, argv, {
|
||||
cwd: exec.agent?.session.header.cwd ?? process.cwd(),
|
||||
signal: exec.signal,
|
||||
});
|
||||
|
||||
const description = readContent(response.choices?.[0]?.message?.content);
|
||||
if (description.length === 0) {
|
||||
throw new Error("Qwen-VL returned an empty description.");
|
||||
}
|
||||
|
||||
return { description, model: response.model ?? model ?? "" };
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/web-search-rag`: registers a Bailian knowledge-base
|
||||
* `WebSearchProvider` with `ctx.web`.
|
||||
*
|
||||
* Retrieval is modelled as a search provider rather than a bespoke tool so the
|
||||
* model reaches private corpora through the `web_search` it already knows —
|
||||
* no new tool, no new prompting. Calls go straight to DashScope because the
|
||||
* seam needs per-call control the CLI does not surface.
|
||||
*
|
||||
* One instance serves one knowledge base: `WebSearchRequest` carries only
|
||||
* `query` and `maxResults`, so the agent id has to come from config. Insert
|
||||
* additional rows with distinct ids to expose more than one.
|
||||
*
|
||||
* @module bailian-cli-dsh/web-search-rag
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type {
|
||||
WebSearchProvider,
|
||||
WebSearchRequest,
|
||||
WebSearchResult,
|
||||
WebSearchSource,
|
||||
} from "@deepseek-ai/dsh-web";
|
||||
import { WebError } from "@deepseek-ai/dsh-web";
|
||||
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { dashScopeFetch, resolveApiKey } from "../shared/http.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-web-search-rag";
|
||||
|
||||
/** The web seam this provider registers into. */
|
||||
export const inject = ["web"];
|
||||
|
||||
/** Stable provider id; pin it as `searchProvider` to disambiguate. */
|
||||
export const BAILIAN_KB_PROVIDER_ID = "bailian-kb";
|
||||
|
||||
export interface Config {
|
||||
/** DashScope key; falls back to `DASHSCOPE_API_KEY`. */
|
||||
apiKey?: string;
|
||||
/** Workspace id; also the retrieval host prefix. Falls back to `BAILIAN_WORKSPACE_ID`. */
|
||||
workspaceId?: string;
|
||||
/** Retrieval service id from the console's knowledge retrieval page. */
|
||||
agentId?: string;
|
||||
/** Default upper bound when the caller sets none. */
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z
|
||||
.string()
|
||||
.role("secret")
|
||||
.description("DashScope API key; defaults to $DASHSCOPE_API_KEY."),
|
||||
workspaceId: z.string().description("Bailian workspace id; defaults to $BAILIAN_WORKSPACE_ID."),
|
||||
agentId: z.string().description("Retrieval service (agent) id identifying the knowledge base."),
|
||||
maxResults: z.natural().description("Default source cap when the caller sets none."),
|
||||
});
|
||||
|
||||
const DEFAULT_MAX_RESULTS = 10;
|
||||
|
||||
interface KnowledgeSearchNode {
|
||||
score?: number;
|
||||
text?: string;
|
||||
metadata?: {
|
||||
title?: string;
|
||||
doc_id?: string;
|
||||
doc_name?: string;
|
||||
doc_url?: string;
|
||||
page_number?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface KnowledgeSearchResponse {
|
||||
data?: { total?: number; nodes?: readonly KnowledgeSearchNode[] };
|
||||
}
|
||||
|
||||
export interface BailianKbProviderOptions {
|
||||
apiKey: string;
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
maxResults: number;
|
||||
}
|
||||
|
||||
function isAbort(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === "AbortError";
|
||||
}
|
||||
|
||||
/** The seam requires a URL; documents without one still deserve a stable identity. */
|
||||
function sourceUrl(node: KnowledgeSearchNode, index: number): string {
|
||||
const url = node.metadata?.doc_url;
|
||||
if (url !== undefined && url.length > 0) return url;
|
||||
return `bailian-kb://${node.metadata?.doc_id ?? `node-${index}`}`;
|
||||
}
|
||||
|
||||
export class BailianKbSearchProvider implements WebSearchProvider {
|
||||
readonly id = BAILIAN_KB_PROVIDER_ID;
|
||||
|
||||
constructor(private readonly options: BailianKbProviderOptions) {}
|
||||
|
||||
available(): boolean {
|
||||
return (
|
||||
this.options.apiKey.length > 0 &&
|
||||
this.options.workspaceId.length > 0 &&
|
||||
this.options.agentId.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
|
||||
const limit = request.maxResults ?? this.options.maxResults;
|
||||
let response: KnowledgeSearchResponse;
|
||||
try {
|
||||
response = await dashScopeFetch<KnowledgeSearchResponse>({
|
||||
url: `https://${this.options.workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`,
|
||||
method: "POST",
|
||||
apiKey: this.options.apiKey,
|
||||
body: { query: request.query, agent_id: this.options.agentId },
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAbort(error))
|
||||
throw new WebError("knowledge base search aborted", "WEB_ABORTED", { cause: error });
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new WebError(`knowledge base search failed: ${reason}`, "WEB_PROVIDER_ERROR", {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
const nodes = (response.data?.nodes ?? []).slice(0, limit);
|
||||
const sources: WebSearchSource[] = nodes.map((node, index) => {
|
||||
const metadata = node.metadata ?? {};
|
||||
const title = metadata.doc_name ?? metadata.title;
|
||||
const text = node.text ?? "";
|
||||
return {
|
||||
url: sourceUrl(node, index),
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(text.length > 0 ? { snippet: text } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
const content = nodes
|
||||
.map((node) => node.text ?? "")
|
||||
.filter((text) => text.length > 0)
|
||||
.join("\n\n");
|
||||
|
||||
// Truncation is the seam's job; report what this provider returned.
|
||||
return { ...(content.length > 0 ? { content } : {}), sources, truncated: false };
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const workspaceId =
|
||||
config.workspaceId ?? launchEnvironmentOf(ctx).get("BAILIAN_WORKSPACE_ID")?.value ?? "";
|
||||
|
||||
ctx.web.registerSearchProvider(
|
||||
new BailianKbSearchProvider({
|
||||
apiKey: resolveApiKey(ctx, config.apiKey) ?? "",
|
||||
workspaceId,
|
||||
agentId: config.agentId ?? "",
|
||||
maxResults: config.maxResults ?? DEFAULT_MAX_RESULTS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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,27 @@
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
// One entry per `exports` subpath: each dsh plugin row imports its own
|
||||
// module specifier, so they cannot share a bundle.
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/tool-vision/index.ts",
|
||||
"src/tool-image/index.ts",
|
||||
"src/web-search-rag/index.ts",
|
||||
"src/memory/index.ts",
|
||||
"src/subagent-managed-agent/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) |
|
||||
@@ -71,28 +75,28 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
|
||||
## By group
|
||||
|
||||
| Group | Commands | Reference |
|
||||
| ------------ | ---------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `advisor` | `recommend` | [advisor.md](advisor.md) |
|
||||
| `app` | `call`, `list` | [app.md](app.md) |
|
||||
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
|
||||
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
|
||||
| `console` | `call` | [console.md](console.md) |
|
||||
| `file` | `upload` | [file.md](file.md) |
|
||||
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
|
||||
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
|
||||
| `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) |
|
||||
| `model` | `list` | [model.md](model.md) |
|
||||
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
|
||||
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
|
||||
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
|
||||
| `update` | `(root)` | [update.md](update.md) |
|
||||
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
|
||||
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
|
||||
| Group | Commands | Reference |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `advisor` | `recommend` | [advisor.md](advisor.md) |
|
||||
| `app` | `call`, `list` | [app.md](app.md) |
|
||||
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
|
||||
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
|
||||
| `console` | `call` | [console.md](console.md) |
|
||||
| `file` | `upload` | [file.md](file.md) |
|
||||
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
|
||||
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
|
||||
| `memory` | `add`, `delete`, `list`, `profile create`, `profile delete`, `profile detail`, `profile get`, `profile list`, `profile update`, `search`, `update` | [memory.md](memory.md) |
|
||||
| `model` | `list` | [model.md](model.md) |
|
||||
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
|
||||
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
|
||||
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
|
||||
| `update` | `(root)` | [update.md](update.md) |
|
||||
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
|
||||
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
|
||||
|
||||
## Global flags
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user