Compare commits

...

3 Commits

Author SHA1 Message Date
zeyu.fz 75699966d4 Merge remote-tracking branch 'origin/main' into feat/memory 2026-09-10 20:43:23 +08:00
zeyu.fz d9134d6f0a feat(memory): 增加长度校验及限流提示并强化安全确认
- 在多条命令中统一添加对 userId 和 memoryLibraryId 长度的校验
- 新增 profile schema 名称、描述及属性字段长度断言
- 为 delete 内存节点命令添加高风险删除确认机制及 --yes 标志
- 增加账号级别限流说明,提示遇 HTTP 429 需降低请求频率
- 统一移除分散的长度常量,集中定义并使用共享常量
- 相关 E2E 测试覆盖长度限制和高风险操作确认情况
- 支持内存消息内容多样化,新增 MemoryContentPart 类型定义
- 内存文档中新增限流描述及删除命令风险提示,强调确认操作要求
2026-09-10 17:31:42 +08:00
zeyu.fz 75a7100702 feat(memory): 完善记忆及画像相关命令支持并统一参数
- 在 CLI 文档中新增记忆 live 场景说明与变量需求
- 扩展 CLI 命令集,添加记忆画像 Profile 的增删改查功能
- 统一所有记忆相关命令加入 workspace 和 library 参数支持
- 记忆添加命令支持自定义 meta_data,完善输入校验和互斥说明
- 记忆删除、列表、获取用户画像等命令增加工作空间参数支持
- 画像模板相关命令新增分页参数及属性操作接口
- 重构知识命令共享模块,移除重复的 workspace 解析实现,统一导出
- 优化命令输出格式,文本模式下显示更多详细信息和异常提示
- 提供完整的示例参数,增强命令使用说明和提示文档
2026-09-10 14:40:39 +08:00
39 changed files with 4223 additions and 585 deletions
+2 -1
View File
@@ -71,12 +71,13 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
| 场景 | 条件 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
| 文本/搜索/配置 | `isDashScopeE2EReady()` |
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
| OpenAPI AK/SK | `isOpenApiE2EReady()``.env` 中必须同时提供完整 AK/SK |
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` |
| 知识库 chat/search live | `isChatE2EReady()` / `isSearchE2EReady()``knowledge chat/search`,需 `BAILIAN_WORKSPACE_ID` + agent ID |
| 记忆 live | `isMemoryE2EReady()`(另需 `BAILIAN_E2E_MEMORY_LIBRARY_ID`;记忆服务需账号单独开通) |
## 用例类型
+8
View File
@@ -29,6 +29,10 @@ import {
memoryUpdate,
memoryDelete,
memoryProfileCreate,
memoryProfileList,
memoryProfileShow,
memoryProfileUpdate,
memoryProfileDelete,
memoryProfileGet,
knowledgeRetrieve,
knowledgeSearch,
@@ -248,6 +252,10 @@ export const commands: Record<string, AnyCommand> = {
"memory update": memoryUpdate,
"memory delete": memoryDelete,
"memory profile create": memoryProfileCreate,
"memory profile list": memoryProfileList,
"memory profile show": memoryProfileShow,
"memory profile update": memoryProfileUpdate,
"memory profile delete": memoryProfileDelete,
"memory profile get": memoryProfileGet,
"knowledge retrieve": knowledgeRetrieve,
"knowledge search": knowledgeSearch,
@@ -1,7 +1,6 @@
// Shared building blocks for the knowledge admin commands.
import {
BailianError,
ExitCode,
ragEndpoint,
RAG_PATHS,
type Client,
@@ -12,18 +11,9 @@ import {
} from "bailian-cli-core";
import { poll } from "bailian-cli-runtime";
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
// flag here (the console credential scope does not apply).
export const WORKSPACE_FLAG = {
workspaceId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
"zh-CN": "API Endpoint URL 使用的 Workspace ID也可设置 BAILIAN_WORKSPACE_ID",
},
},
} satisfies FlagsDef;
// The workspace scope is shared with the memory commands, so it lives in
// ../shared/workspace.ts; re-exported here to keep the knowledge imports local.
export { resolveWorkspaceId, WORKSPACE_FLAG } from "../shared/workspace.ts";
// Unified pagination flags for admin list commands. The server-side page/size
// parameter names differ per endpoint (page_number/page_num/pageNum/pageNumber…)
@@ -43,23 +33,6 @@ export const PAGE_FLAGS = {
},
} satisfies FlagsDef;
/** Three-level fallback: flag > BAILIAN_WORKSPACE_ID env > config (env/config are merged into settings); missing → USAGE. */
export function resolveWorkspaceId(ctx: {
flags: { workspaceId?: string };
settings: { workspaceId?: string };
identity: { binName: string };
}): string {
const workspaceId = ctx.flags.workspaceId || ctx.settings.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
);
}
return workspaceId;
}
/** Truncate text-mode table rows to the terminal width; no truncation when not a TTY (pipe/redirect). */
export function truncateLine(line: string): string {
if (!process.stdout.isTTY) return line;
+84 -28
View File
@@ -2,53 +2,79 @@ import {
defineCommand,
UsageError,
memoryAddPath,
memoryEndpoint,
detectOutputFormat,
type FlagsDef,
type ParsedFlags,
type MemoryAddRequest,
type MemoryAddResponse,
type MemoryMessage,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_RATE_LIMIT_NOTE,
MEMORY_WORKSPACE_NOTE,
MAX_CUSTOM_CONTENT_LENGTH,
PROJECT_ID_FLAG,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
parseJsonArrayFlag,
parseJsonObjectFlag,
resolveWorkspaceId,
} from "./shared.ts";
const ADD_FLAGS = {
userId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID必填" },
description: {
"en-US": "Memory entity ID that owns the memory (required)",
"zh-CN": "记忆实体 ID标识记忆归属对象必填",
},
required: true,
},
messages: {
type: "string",
valueHint: "<json>",
description: {
"en-US": 'Messages JSON array: [{"role":"user","content":"..."},...]',
"zh-CN": '消息 JSON 数组:[{"role":"user","content":"..."},...]',
"en-US": 'Messages JSON array: [{"role":"user","content":"..."},...] (max 50)',
"zh-CN": '消息 JSON 数组:[{"role":"user","content":"..."},...](最多 50 条)',
},
},
content: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Custom content text to memorize", "zh-CN": "要记忆的自定义内容文本" },
description: {
"en-US": "Custom content to memorize verbatim; takes precedence over --messages",
"zh-CN": "要原样记忆的自定义内容;优先级高于 --messages",
},
},
profileSchema: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Profile schema ID for user profiling",
"zh-CN": "用于用户画像的 Profile Schema ID",
"en-US": "Profile schema ID; without it no user profile is extracted",
"zh-CN": "画像模板 ID不传则不提取用户画像",
},
},
memoryLibraryId: {
metaData: {
type: "string",
valueHint: "<id>",
valueHint: "<json>",
description: {
"en-US": "Memory library ID (isolate memory space)",
"zh-CN": "记忆库 ID用于隔离记忆空间",
"en-US": 'Custom metadata JSON object: {"location_name":"Beijing"}',
"zh-CN": '用户自定义信息 JSON 对象:{"location_name":"北京"}',
},
},
...PROJECT_ID_FLAG,
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
type AddFlags = ParsedFlags<typeof ADD_FLAGS>;
/** Max messages accepted per AddMemory call (a Q&A pair counts as 2). */
const MAX_MESSAGES = 50;
export default defineCommand({
description: {
"en-US": "Add memory from messages or custom content",
@@ -57,10 +83,25 @@ export default defineCommand({
auth: "apiKey",
usageArgs: "--user-id <id> [--messages <json>] [--content <text>] [flags]",
flags: ADD_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"--content and --messages are mutually exclusive: when --content is set, --messages is ignored by the server.",
"zh-CN": "--content 与 --messages 互斥:传了 --content 时服务端会忽略 --messages。",
},
{
"en-US":
"The response lists the changed memory nodes; one call can add, update or delete several at once.",
"zh-CN": "返回结果是变更的记忆片段列表;一次调用可能同时新增、更新或删除多条。",
},
MEMORY_RATE_LIMIT_NOTE,
],
exampleArgs: [
{
"en-US": '--user-id user1 --content "The user likes Python programming"',
"zh-CN": '--user-id user1 --content "用户喜欢使用 Python 编程"',
"en-US":
'--user-id user1 --content "The user likes Python programming" --workspace-id ws_xxx',
"zh-CN": '--user-id user1 --content "用户喜欢使用 Python 编程" --workspace-id ws_xxx',
},
{
"en-US": '--user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'',
@@ -70,46 +111,61 @@ export default defineCommand({
"en-US": '--user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx',
"zh-CN": '--user-id user1 --content "居住在北京" --profile-schema schema_xxx',
},
{
"en-US": '--user-id user1 --content "Attended WAIC" --meta-data \'{"location":"Shanghai"}\'',
"zh-CN": '--user-id user1 --content "参加了 WAIC" --meta-data \'{"location":"上海"}\'',
},
],
validate: (f: AddFlags) =>
!f.messages && !f.content ? "Provide --messages or --content." : undefined,
validate: (flags: AddFlags) => {
if (!flags.messages && !flags.content) return "Provide --messages or --content.";
const scopeError = checkMemoryScopeLengths(flags);
if (scopeError) return scopeError;
if (flags.content && flags.content.length > MAX_CUSTOM_CONTENT_LENGTH)
return `--content must be at most ${MAX_CUSTOM_CONTENT_LENGTH} characters.`;
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const userId = flags.userId;
const body: MemoryAddRequest = { user_id: userId };
const body: MemoryAddRequest = { user_id: flags.userId };
if (flags.messages) {
try {
body.messages = JSON.parse(flags.messages);
} catch {
throw new UsageError("--messages must be valid JSON array");
const messages = parseJsonArrayFlag<MemoryMessage>("--messages", flags.messages);
if (messages.length > MAX_MESSAGES) {
throw new UsageError(`--messages accepts at most ${MAX_MESSAGES} messages`);
}
body.messages = messages;
}
if (flags.content) {
body.custom_content = flags.content;
}
if (flags.content) body.custom_content = flags.content;
if (flags.metaData) body.meta_data = parseJsonObjectFlag("--meta-data", flags.metaData);
if (flags.profileSchema) body.profile_schema = flags.profileSchema;
if (flags.projectId) body.project_id = flags.projectId;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(settings.output);
const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryAddPath());
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(memoryAddPath()), request: body }, format);
emitResult({ endpoint: url, method: "POST", request: body }, format);
return;
}
const response = await ctx.client.requestJson<MemoryAddResponse>({
path: memoryAddPath(),
path: url,
method: "POST",
body,
});
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 node changed.");
return;
}
for (const node of nodes) {
emitBare(`[${node.event ?? "ADD"}] ${node.memory_node_id} ${node.content}`);
}
} else {
emitResult(response, format);
}
+60 -29
View File
@@ -1,50 +1,81 @@
import { defineCommand, memoryNodePath, detectOutputFormat } from "bailian-cli-core";
import {
defineCommand,
memoryEndpoint,
memoryNodePath,
detectOutputFormat,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { buildQuery } from "../shared/params.ts";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
resolveWorkspaceId,
} from "./shared.ts";
const DELETE_FLAGS = {
nodeId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID必填" },
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory entity ID that owns the memory (required)",
"zh-CN": "记忆实体 ID标识记忆归属对象必填",
},
required: true,
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Delete a memory node", "zh-CN": "删除记忆节点" },
auth: "apiKey",
usageArgs: "--node-id <id> --user-id <id>",
flags: {
nodeId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID必填" },
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID必填" },
required: true,
},
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory library ID (non-default library)",
"zh-CN": "记忆库 ID非默认记忆库",
},
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the specified memory node and cannot be undone.",
"zh-CN": "该操作会永久删除指定的记忆片段,且无法恢复。",
},
},
exampleArgs: ["--node-id node_xxx --user-id user1"],
usageArgs: "--node-id <id> --user-id <id> [flags]",
flags: DELETE_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"Irreversible — the memory node is permanently removed. Run `memory list` first to confirm the node ID.",
"zh-CN": "该操作不可撤销——记忆片段将被永久删除。建议先用 `memory list` 确认节点 ID。",
},
],
exampleArgs: [
"--node-id node_xxx --user-id user1 --workspace-id ws_xxx",
"--node-id node_xxx --user-id user1 --yes",
],
validate: (flags) => checkMemoryScopeLengths(flags),
async run(ctx) {
const { settings, flags } = ctx;
const nodeId = flags.nodeId;
const userId = flags.userId;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams({ user_id: userId });
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const path = `${memoryNodePath(nodeId)}?${params.toString()}`;
const url =
memoryEndpoint(resolveWorkspaceId(ctx), memoryNodePath(nodeId)) +
buildQuery({ user_id: flags.userId, memory_library_id: flags.memoryLibraryId });
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "DELETE" }, format);
emitResult({ endpoint: url, method: "DELETE" }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path,
path: url,
method: "DELETE",
});
+64 -33
View File
@@ -1,59 +1,87 @@
import {
defineCommand,
memoryEndpoint,
memoryListPath,
detectOutputFormat,
type FlagsDef,
type MemoryNodeListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { buildQuery } from "../shared/params.ts";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
PROJECT_ID_FLAG,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
resolveWorkspaceId,
} from "./shared.ts";
const LIST_FLAGS = {
userId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory entity ID that owns the memory (required)",
"zh-CN": "记忆实体 ID标识记忆归属对象必填",
},
required: true,
},
pageSize: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Results per page (default: 10)", "zh-CN": "每页结果数默认10" },
},
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
...PROJECT_ID_FLAG,
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "List memory nodes for a user", "zh-CN": "列出用户的记忆节点" },
auth: "apiKey",
usageArgs: "--user-id <id> [flags]",
flags: {
userId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID必填" },
required: true,
},
pageSize: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Results per page (default: 10)", "zh-CN": "每页结果数默认10" },
},
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Memory library ID", "zh-CN": "记忆库 ID" },
},
flags: LIST_FLAGS,
notes: [MEMORY_WORKSPACE_NOTE],
exampleArgs: [
"--user-id user1 --workspace-id ws_xxx",
"--user-id user1 --page-size 20 --page 2",
"--user-id user1 --memory-library-id lib_xxx --output json",
],
validate: (flags) => {
const scopeError = checkMemoryScopeLengths(flags);
if (scopeError) return scopeError;
if (flags.page !== undefined && flags.page < 1) return "--page must be at least 1.";
if (flags.pageSize !== undefined && flags.pageSize < 1)
return "--page-size must be at least 1.";
return undefined;
},
exampleArgs: ["--user-id user1", "--user-id user1 --page-size 20 --page 2"],
async run(ctx) {
const { settings, flags } = ctx;
const userId = flags.userId;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
params.set("user_id", userId);
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);
const path = `${memoryListPath()}?${params.toString()}`;
const url =
memoryEndpoint(resolveWorkspaceId(ctx), memoryListPath()) +
buildQuery({
user_id: flags.userId,
page_size: flags.pageSize,
page_num: flags.page,
project_id: flags.projectId,
memory_library_id: flags.memoryLibraryId,
});
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "GET" }, format);
emitResult({ endpoint: url, method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<MemoryNodeListResponse>({
path,
path: url,
method: "GET",
});
@@ -63,6 +91,9 @@ export default defineCommand({
} else {
for (const node of response.memory_nodes) {
emitBare(`[${node.memory_node_id}] ${node.content}`);
if (node.meta_data && Object.keys(node.meta_data).length > 0) {
emitBare(` meta: ${JSON.stringify(node.meta_data)}`);
}
}
if (response.total !== undefined) {
emitBare(`\nTotal: ${response.total}`);
@@ -1,12 +1,53 @@
import {
defineCommand,
UsageError,
memoryEndpoint,
profileSchemaPath,
detectOutputFormat,
type FlagsDef,
type MemoryPlanVersion,
type ProfileAttribute,
type ProfileSchemaCreateRequest,
type ProfileSchemaCreateResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
PLAN_VERSION_FLAG,
WORKSPACE_FLAG,
assertAttributeFieldLengths,
checkMemoryScopeLengths,
checkProfileSchemaTextLengths,
parseJsonArrayFlag,
resolveWorkspaceId,
} from "./shared.ts";
const PROFILE_CREATE_FLAGS = {
name: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Schema name (required)", "zh-CN": "Schema 名称(必填)" },
required: true,
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Schema description", "zh-CN": "Schema 描述" },
},
attributes: {
type: "string",
valueHint: "<json>",
description: {
"en-US": 'Attributes JSON array: [{"name":"age","description":"age","default_value":"18"}]',
"zh-CN": '属性 JSON 数组:[{"name":"age","description":"年龄","default_value":"18"}]',
},
required: true,
},
...PLAN_VERSION_FLAG,
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
@@ -15,60 +56,60 @@ export default defineCommand({
},
auth: "apiKey",
usageArgs: "--name <name> --attributes <json> [flags]",
flags: {
name: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Schema name (required)", "zh-CN": "Schema 名称(必填)" },
required: true,
flags: PROFILE_CREATE_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"Each attribute needs a name; description and default_value are optional. --plan-version picks the extraction tier (pro / lite) and is billed differently.",
"zh-CN":
"每个属性必须有 namedescription 与 default_value 可选。--plan-version 决定抽取策略档位pro / lite计费单价不同。",
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Schema description", "zh-CN": "Schema 描述" },
},
attributes: {
type: "string",
valueHint: "<json>",
description: {
"en-US": 'Attributes JSON array: [{"name":"age","description":"age"}]',
"zh-CN": '属性 JSON 数组:[{"name":"age","description":"age"}]',
},
required: true,
},
},
],
exampleArgs: [
{
"en-US":
'--name "user_basic" --attributes \'[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]\'',
'--name "user_basic" --attributes \'[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]\' --workspace-id ws_xxx',
"zh-CN":
'--name "user_basic" --attributes \'[{"name":"age","description":"年龄"},{"name":"hobby","description":"爱好"}]\'',
'--name "user_basic" --attributes \'[{"name":"age","description":"年龄"},{"name":"hobby","description":"爱好"}]\' --workspace-id ws_xxx',
},
{
"en-US":
'--name "user_basic" --attributes \'[{"name":"age"}]\' --plan-version lite --memory-library-id lib_xxx',
"zh-CN":
'--name "user_basic" --attributes \'[{"name":"age"}]\' --plan-version lite --memory-library-id lib_xxx',
},
],
validate: (flags) =>
checkMemoryScopeLengths(flags) ?? checkProfileSchemaTextLengths(flags) ?? undefined,
async run(ctx) {
const { settings, flags } = ctx;
const name = flags.name;
const attrStr = flags.attributes;
let attributes;
try {
attributes = JSON.parse(attrStr);
} catch {
throw new UsageError("--attributes must be valid JSON array");
const attributes = parseJsonArrayFlag<ProfileAttribute>("--attributes", flags.attributes);
if (attributes.length === 0) {
throw new UsageError("--attributes must contain at least one attribute");
}
attributes.forEach((attribute, index) => {
const position = `--attributes[${index}]`;
if (!attribute.name) throw new UsageError(`${position}.name is required`);
assertAttributeFieldLengths(position, attribute);
});
const body: ProfileSchemaCreateRequest = { name, attributes };
const body: ProfileSchemaCreateRequest = { name: flags.name, attributes };
if (flags.description) body.description = flags.description;
if (flags.planVersion) body.plan_version = flags.planVersion as MemoryPlanVersion;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(settings.output);
const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaPath());
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(profileSchemaPath()), request: body }, format);
emitResult({ endpoint: url, method: "POST", request: body }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaCreateResponse>({
path: profileSchemaPath(),
path: url,
method: "POST",
body,
});
@@ -0,0 +1,83 @@
import {
defineCommand,
memoryEndpoint,
profileSchemaItemPath,
detectOutputFormat,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { buildQuery } from "../shared/params.ts";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
resolveWorkspaceId,
} from "./shared.ts";
const PROFILE_DELETE_FLAGS = {
schemaId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Profile schema ID (required)",
"zh-CN": "Profile Schema ID必填",
},
required: true,
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Delete a profile schema",
"zh-CN": "删除画像模板",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This permanently deletes the profile schema and its attribute definitions. Profiles already extracted for this schema become unreachable.",
"zh-CN": "该操作会永久删除画像模板及其属性定义,已基于该模板提取的用户画像也将无法访问。",
},
},
usageArgs: "--schema-id <id> [flags]",
flags: PROFILE_DELETE_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"Irreversible — the schema and its attribute definitions are permanently removed. Profiles already extracted for this schema become unreachable.",
"zh-CN":
"该操作不可撤销——模板及其属性定义将被永久删除,已基于该模板提取的用户画像也将无法访问。",
},
],
exampleArgs: ["--schema-id schema_xxx --workspace-id ws_xxx", "--schema-id schema_xxx --yes"],
validate: (flags) => checkMemoryScopeLengths(flags),
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const url =
memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaItemPath(flags.schemaId)) +
buildQuery({ memory_library_id: flags.memoryLibraryId });
if (settings.dryRun) {
emitResult({ endpoint: url, method: "DELETE" }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path: url,
method: "DELETE",
});
if (settings.quiet || format === "text") {
emitBare(`Profile schema ${flags.schemaId} deleted.`);
} else {
emitResult(response, format);
}
},
});
@@ -1,62 +1,89 @@
import {
defineCommand,
memoryEndpoint,
userProfilePath,
detectOutputFormat,
type FlagsDef,
type UserProfileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { buildQuery } from "../shared/params.ts";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
resolveWorkspaceId,
} from "./shared.ts";
const PROFILE_GET_FLAGS = {
schemaId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Profile schema ID (required)",
"zh-CN": "Profile Schema ID必填",
},
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory entity ID that owns the profile (required)",
"zh-CN": "记忆实体 ID标识画像归属对象必填",
},
required: true,
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Get user profile by schema ID and user ID",
"zh-CN": "通过 Schema ID 和用户 ID 获取用户画像",
"en-US": "Get the extracted user profile for a schema",
"zh-CN": "获取某个 Schema 下已提取的用户画像",
},
auth: "apiKey",
usageArgs: "--schema-id <id> --user-id <id>",
flags: {
schemaId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Profile schema ID (required)",
"zh-CN": "Profile Schema ID必填",
},
required: true,
usageArgs: "--schema-id <id> --user-id <id> [flags]",
flags: PROFILE_GET_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"Values are extracted only when `memory add --profile-schema` used the same schema ID; otherwise every attribute comes back empty. Use `memory profile show` for the schema definition itself.",
"zh-CN":
"只有 `memory add --profile-schema` 传入同一个 Schema ID 时才会提取属性值,否则所有属性都为空。查看模板定义本身请用 `memory profile show`。",
},
userId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID必填" },
required: true,
},
},
exampleArgs: ["--schema-id schema_xxx --user-id user1"],
],
exampleArgs: ["--schema-id schema_xxx --user-id user1 --workspace-id ws_xxx"],
validate: (flags) => checkMemoryScopeLengths(flags),
async run(ctx) {
const { settings, flags } = ctx;
const schemaId = flags.schemaId;
const userId = flags.userId;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams({ user_id: userId });
const path = `${userProfilePath(schemaId)}?${params.toString()}`;
const url =
memoryEndpoint(resolveWorkspaceId(ctx), userProfilePath(flags.schemaId)) +
buildQuery({ user_id: flags.userId, memory_library_id: flags.memoryLibraryId });
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "GET" }, format);
emitResult({ endpoint: url, method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<UserProfileResponse>({
path,
path: url,
method: "GET",
});
if (settings.quiet || format === "text") {
if (response.profile?.attributes) {
for (const attr of response.profile.attributes) {
emitBare(`${attr.name}: ${attr.value ?? "(empty)"}`);
}
} else {
const attributes = response.profile?.attributes ?? [];
if (attributes.length === 0) {
emitBare("No profile data found.");
} else {
for (const attribute of attributes) {
emitBare(`${attribute.name}: ${attribute.value ?? "(empty)"}`);
}
}
} else {
emitResult(response, format);
@@ -0,0 +1,92 @@
import {
defineCommand,
memoryEndpoint,
profileSchemaPath,
detectOutputFormat,
type FlagsDef,
type ProfileSchemaListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { buildQuery } from "../shared/params.ts";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
resolveWorkspaceId,
} from "./shared.ts";
const PROFILE_LIST_FLAGS = {
pageSize: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Results per page (default: 10)", "zh-CN": "每页结果数默认10" },
},
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "List profile schemas", "zh-CN": "列出画像模板" },
auth: "apiKey",
usageArgs: "[flags]",
flags: PROFILE_LIST_FLAGS,
notes: [MEMORY_WORKSPACE_NOTE],
exampleArgs: [
"--workspace-id ws_xxx",
"--page-size 20 --page 2",
"--memory-library-id lib_xxx --output json",
],
validate: (flags) => {
const scopeError = checkMemoryScopeLengths(flags);
if (scopeError) return scopeError;
if (flags.page !== undefined && flags.page < 1) return "--page must be at least 1.";
if (flags.pageSize !== undefined && flags.pageSize < 1)
return "--page-size must be at least 1.";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const url =
memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaPath()) +
buildQuery({
page_size: flags.pageSize,
page_num: flags.page,
memory_library_id: flags.memoryLibraryId,
});
if (settings.dryRun) {
emitResult({ endpoint: url, method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaListResponse>({
path: url,
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 (schema.description) emitBare(` ${schema.description}`);
}
if (response.total !== undefined) {
emitBare(`\nTotal: ${response.total}`);
}
}
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,92 @@
import {
defineCommand,
memoryEndpoint,
profileSchemaItemPath,
detectOutputFormat,
type FlagsDef,
type ProfileSchemaDetailResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { buildQuery } from "../shared/params.ts";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
resolveWorkspaceId,
} from "./shared.ts";
const PROFILE_SHOW_FLAGS = {
schemaId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Profile schema ID (required)",
"zh-CN": "Profile Schema ID必填",
},
required: true,
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Show a profile schema definition with attribute IDs",
"zh-CN": "查看画像模板定义及属性 ID",
},
auth: "apiKey",
usageArgs: "--schema-id <id> [flags]",
flags: PROFILE_SHOW_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"The attribute_id values returned here are the handles `memory profile update --attributes-operations` needs for update / delete operations.",
"zh-CN":
"这里返回的 attribute_id 就是 `memory profile update --attributes-operations` 做 update / delete 时需要的属性句柄。",
},
],
exampleArgs: [
"--schema-id schema_xxx --workspace-id ws_xxx",
"--schema-id schema_xxx --output json",
],
validate: (flags) => checkMemoryScopeLengths(flags),
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const url =
memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaItemPath(flags.schemaId)) +
buildQuery({ memory_library_id: flags.memoryLibraryId });
if (settings.dryRun) {
emitResult({ endpoint: url, method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaDetailResponse>({
path: url,
method: "GET",
});
if (settings.quiet || format === "text") {
emitBare(`Name: ${response.name ?? "(none)"}`);
if (response.description) emitBare(`Description: ${response.description}`);
const attributes = response.attributes ?? [];
if (attributes.length === 0) {
emitBare("No attributes.");
} else {
emitBare("Attributes:");
for (const attribute of attributes) {
emitBare(` [${attribute.attribute_id}] ${attribute.name}`);
if (attribute.description) emitBare(` desc: ${attribute.description}`);
if (attribute.default_value !== undefined)
emitBare(` default: ${attribute.default_value}`);
}
}
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,144 @@
import {
defineCommand,
UsageError,
memoryEndpoint,
profileSchemaItemPath,
detectOutputFormat,
type FlagsDef,
type ProfileSchemaAttributeOperation,
type ProfileSchemaUpdateRequest,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
WORKSPACE_FLAG,
assertAttributeFieldLengths,
checkMemoryScopeLengths,
checkProfileSchemaTextLengths,
parseJsonArrayFlag,
resolveWorkspaceId,
} from "./shared.ts";
const PROFILE_UPDATE_FLAGS = {
schemaId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Profile schema ID (required)",
"zh-CN": "Profile Schema ID必填",
},
required: true,
},
name: {
type: "string",
valueHint: "<name>",
description: { "en-US": "New schema name", "zh-CN": "新的 Schema 名称" },
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "New schema description", "zh-CN": "新的 Schema 描述" },
},
attributesOperations: {
type: "string",
valueHint: "<json>",
description: {
"en-US": 'Attribute operations JSON array: [{"op":"add","name":"plan"}]',
"zh-CN": '属性操作 JSON 数组:[{"op":"add","name":"plan"}]',
},
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
const VALID_OPS = ["add", "update", "delete"] as const;
/** Reject operation shapes the API would reject anyway, so the error stays local and precise. */
function validateOperations(operations: ProfileSchemaAttributeOperation[]): void {
operations.forEach((operation, index) => {
const position = `--attributes-operations[${index}]`;
if (!VALID_OPS.includes(operation.op)) {
throw new UsageError(`${position}.op must be one of ${VALID_OPS.join(" / ")}`);
}
if (operation.op === "add" && !operation.name) {
throw new UsageError(`${position}.name is required when op is "add"`);
}
if (operation.op !== "add" && !operation.attribute_id) {
throw new UsageError(`${position}.attribute_id is required when op is "${operation.op}"`);
}
assertAttributeFieldLengths(position, operation);
});
}
export default defineCommand({
description: {
"en-US": "Update a profile schema name, description, or attributes",
"zh-CN": "更新画像模板的名称、描述或属性",
},
auth: "apiKey",
usageArgs: "--schema-id <id> [flags]",
flags: PROFILE_UPDATE_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
'Each operation needs "op": add (requires name), update / delete (require attribute_id). Run `memory profile show` first to get attribute IDs.',
"zh-CN":
'每条操作必须带 "op"add需要 name、update / delete需要 attribute_id。属性 ID 先用 `memory profile show` 查。',
},
],
exampleArgs: [
{
"en-US": '--schema-id schema_xxx --name "user_basic_v2" --workspace-id ws_xxx',
"zh-CN": '--schema-id schema_xxx --name "user_basic_v2" --workspace-id ws_xxx',
},
{
"en-US":
'--schema-id schema_xxx --attributes-operations \'[{"op":"add","name":"plan","default_value":"free"},{"op":"delete","attribute_id":"attr_2"}]\'',
"zh-CN":
'--schema-id schema_xxx --attributes-operations \'[{"op":"add","name":"plan","default_value":"free"},{"op":"delete","attribute_id":"attr_2"}]\'',
},
],
validate: (flags) => {
if (!flags.name && !flags.description && !flags.attributesOperations)
return "Provide --name, --description, or --attributes-operations.";
return checkMemoryScopeLengths(flags) ?? checkProfileSchemaTextLengths(flags) ?? undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const body: ProfileSchemaUpdateRequest = {};
if (flags.name) body.name = flags.name;
if (flags.description) body.description = flags.description;
if (flags.attributesOperations) {
const operations = parseJsonArrayFlag<ProfileSchemaAttributeOperation>(
"--attributes-operations",
flags.attributesOperations,
);
validateOperations(operations);
body.attributes_operations = operations;
}
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(settings.output);
const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaItemPath(flags.schemaId));
if (settings.dryRun) {
emitResult({ endpoint: url, method: "PATCH", request: body }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path: url,
method: "PATCH",
body,
});
if (settings.quiet || format === "text") {
emitBare(`Profile schema ${flags.schemaId} updated.`);
} else {
emitResult(response, format);
}
},
});
+108 -33
View File
@@ -1,51 +1,109 @@
import {
defineCommand,
UsageError,
memoryEndpoint,
memorySearchPath,
detectOutputFormat,
type FlagsDef,
type ParsedFlags,
type MemoryMessage,
type MemoryPlanVersion,
type MemorySearchRequest,
type MemorySearchResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_RATE_LIMIT_NOTE,
MEMORY_WORKSPACE_NOTE,
PLAN_VERSION_FLAG,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
parseJsonArrayFlag,
resolveWorkspaceId,
} from "./shared.ts";
const SEARCH_FLAGS = {
userId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID必填" },
description: {
"en-US": "Memory entity ID that owns the memory (required)",
"zh-CN": "记忆实体 ID标识记忆归属对象必填",
},
required: true,
},
query: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Search query text", "zh-CN": "搜索查询文本" },
description: {
"en-US": "Search text; sent as a single user message",
"zh-CN": "搜索文本;作为单条用户消息发送",
},
},
messages: {
type: "string",
valueHint: "<json>",
description: {
"en-US": "Messages JSON array for context-based search",
"zh-CN": "用于上下文搜索的消息 JSON 数组",
"en-US": "Messages JSON array for context-based search; overrides --query",
"zh-CN": "用于上下文搜索的消息 JSON 数组;优先于 --query",
},
},
topK: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Number of results to return (default: 10)",
"zh-CN": "返回结果数量默认10",
"en-US": "Max results, 1-100 (default: 10)",
"zh-CN": "最大召回个数1~100默认10",
},
},
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Memory library ID", "zh-CN": "记忆库 ID" },
minScore: {
type: "number",
valueHint: "<score>",
description: {
"en-US": "Minimum similarity score, 0-1 (default: 0.3)",
"zh-CN": "最小相似度分数阈值0~1默认0.3",
},
},
enableRerank: {
type: "boolean",
valueHint: "<bool>",
description: {
"en-US": "Rerank results (default: false); ignored when --plan-version is set",
"zh-CN": "是否重排序搜索结果默认false传了 --plan-version 时本参数被忽略",
},
},
enableJudge: {
type: "boolean",
valueHint: "<bool>",
description: {
"en-US": "Run the intent judge callback (default: false)",
"zh-CN": "是否开启意图判别回调默认false",
},
},
enableRewrite: {
type: "boolean",
valueHint: "<bool>",
description: {
"en-US": "Rewrite the query before searching (default: false)",
"zh-CN": "是否开启 query 重写默认false",
},
},
projectId: {
type: "array",
valueHint: "<id>",
description: {
"en-US": "Memory fragment rule ID (repeatable for hybrid retrieval)",
"zh-CN": "记忆片段规则 ID可重复用于多规则混合检索",
},
},
...PLAN_VERSION_FLAG,
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
type SearchFlags = ParsedFlags<typeof SEARCH_FLAGS>;
const MAX_TOP_K = 100;
export default defineCommand({
description: {
"en-US": "Search memory nodes by query or messages",
@@ -54,52 +112,69 @@ export default defineCommand({
auth: "apiKey",
usageArgs: "--user-id <id> [--query <text>] [flags]",
flags: SEARCH_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US":
"--plan-version overrides --enable-rerank and changes the price: pro reranks, lite does not.",
"zh-CN": "--plan-version 覆盖 --enable-rerank 且影响计费pro 开启重排lite 不开启。",
},
MEMORY_RATE_LIMIT_NOTE,
],
exampleArgs: [
{
"en-US": '--user-id user1 --query "programming preferences"',
"zh-CN": '--user-id user1 --query "编程偏好"',
"en-US": '--user-id user1 --query "programming preferences" --workspace-id ws_xxx',
"zh-CN": '--user-id user1 --query "编程偏好" --workspace-id ws_xxx',
},
{
"en-US":
'--user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5',
"zh-CN": '--user-id user1 --messages \'[{"role":"user","content":"推荐一本书"}]\' --top-k 5',
},
{
"en-US": '--user-id user1 --query "reminders" --plan-version lite --min-score 0',
"zh-CN": '--user-id user1 --query "提醒事项" --plan-version lite --min-score 0',
},
],
validate: (f: SearchFlags) =>
!f.query && !f.messages ? "Provide --query or --messages." : undefined,
validate: (flags: SearchFlags) => {
if (!flags.query && !flags.messages) return "Provide --query or --messages.";
const scopeError = checkMemoryScopeLengths(flags);
if (scopeError) return scopeError;
if (flags.topK !== undefined && (flags.topK < 1 || flags.topK > MAX_TOP_K))
return `--top-k must be between 1 and ${MAX_TOP_K}.`;
if (flags.minScore !== undefined && (flags.minScore < 0 || flags.minScore > 1))
return "--min-score must be between 0 and 1.";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const userId = flags.userId;
const body: MemorySearchRequest = { user_id: userId };
// The API only accepts `messages`; a bare --query is wrapped as one user turn.
const messages = flags.messages
? parseJsonArrayFlag<MemoryMessage>("--messages", flags.messages)
: [{ role: "user" as const, content: flags.query! }];
if (flags.query) body.query = flags.query;
if (flags.messages) {
try {
body.messages = JSON.parse(flags.messages);
} catch {
throw new UsageError("--messages must be valid JSON array");
}
}
// API requires messages; if only query is given, wrap it as a user message
if (!body.messages && body.query) {
body.messages = [{ role: "user", content: body.query }];
}
const body: MemorySearchRequest = { user_id: flags.userId, messages };
if (flags.topK !== undefined) body.top_k = flags.topK;
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) body.plan_version = flags.planVersion as MemoryPlanVersion;
if (flags.projectId?.length) body.project_ids = flags.projectId;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(settings.output);
const url = memoryEndpoint(resolveWorkspaceId(ctx), memorySearchPath());
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(memorySearchPath()), request: body }, format);
emitResult({ endpoint: url, method: "POST", request: body }, format);
return;
}
const response = await ctx.client.requestJson<MemorySearchResponse>({
path: memorySearchPath(),
path: url,
method: "POST",
body,
});
@@ -0,0 +1,175 @@
import { UsageError, type FlagsDef } from "bailian-cli-core";
// The memory API is served on the per-workspace host, so every memory command
// carries the workspace scope (shared with the knowledge commands).
export { resolveWorkspaceId, WORKSPACE_FLAG } from "../shared/workspace.ts";
/** Shared help note: the workspace scope is required because it selects the API host. */
export const MEMORY_WORKSPACE_NOTE = {
"en-US":
"The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.",
"zh-CN":
"记忆 API 部署在 workspace 专属域名上,因此 --workspace-id 必填;也可通过 BAILIAN_WORKSPACE_ID 环境变量或 workspace_id 配置项提供。",
};
/**
* Memory library scope. Every memory API accepts it and falls back to the
* account's default library, so it stays optional on every command.
*/
export const MEMORY_LIBRARY_FLAG = {
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory library ID (default: the account's default library)",
"zh-CN": "记忆库 ID默认账号的默认记忆库",
},
},
} satisfies FlagsDef;
/** Billing tier shared by search and profile schema creation. */
export const PLAN_VERSION_FLAG = {
planVersion: {
type: "string",
valueHint: "<version>",
choices: ["pro", "lite"] as const,
description: {
"en-US": "Strategy version: pro (rerank on) or lite (rerank off); billed differently",
"zh-CN": "策略版本pro开启 Rerank或 lite关闭 Rerank计费单价不同",
},
},
} satisfies FlagsDef;
/** Memory fragment rule scope, repeatable for the search hybrid form. */
export const PROJECT_ID_FLAG = {
projectId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory fragment rule ID (default: the library's default rule)",
"zh-CN": "记忆片段规则 ID默认记忆库的默认规则",
},
},
} satisfies FlagsDef;
// ---- Documented field limits (long-term memory API reference) ----
/** Max characters accepted for user_id. */
export const MAX_USER_ID_LENGTH = 64;
/** Max characters accepted for memory_library_id. */
export const MAX_MEMORY_LIBRARY_ID_LENGTH = 32;
/** Max characters accepted for custom_content. */
export const MAX_CUSTOM_CONTENT_LENGTH = 512;
/** Max characters accepted for a profile schema name. */
export const MAX_SCHEMA_NAME_LENGTH = 32;
/** Max characters accepted for a profile schema description. */
export const MAX_SCHEMA_DESCRIPTION_LENGTH = 128;
/** Max characters accepted for a profile attribute name. */
export const MAX_ATTRIBUTE_NAME_LENGTH = 32;
/** Max characters accepted for a profile attribute description. */
export const MAX_ATTRIBUTE_DESCRIPTION_LENGTH = 128;
/** Max characters accepted for a profile attribute default value. */
export const MAX_ATTRIBUTE_DEFAULT_VALUE_LENGTH = 128;
/**
* Length guard for the scope flags every memory command carries, so `--dry-run`
* already rejects input the API would reject.
*/
export function checkMemoryScopeLengths(flags: {
userId?: string;
memoryLibraryId?: string;
}): string | undefined {
if (flags.userId !== undefined && flags.userId.length > MAX_USER_ID_LENGTH) {
return `--user-id must be at most ${MAX_USER_ID_LENGTH} characters.`;
}
if (
flags.memoryLibraryId !== undefined &&
flags.memoryLibraryId.length > MAX_MEMORY_LIBRARY_ID_LENGTH
) {
return `--memory-library-id must be at most ${MAX_MEMORY_LIBRARY_ID_LENGTH} characters.`;
}
return undefined;
}
/** Length guard for the profile schema name / description pair. */
export function checkProfileSchemaTextLengths(flags: {
name?: string;
description?: string;
}): string | undefined {
if (flags.name !== undefined && flags.name.length > MAX_SCHEMA_NAME_LENGTH) {
return `--name must be at most ${MAX_SCHEMA_NAME_LENGTH} characters.`;
}
if (flags.description !== undefined && flags.description.length > MAX_SCHEMA_DESCRIPTION_LENGTH) {
return `--description must be at most ${MAX_SCHEMA_DESCRIPTION_LENGTH} characters.`;
}
return undefined;
}
/**
* Length guard for one attribute payload, shared by `profile create` attributes
* and `profile update` attribute operations. Throws because it runs after the
* JSON flag is parsed.
*/
export function assertAttributeFieldLengths(
position: string,
attribute: { name?: string; description?: string; default_value?: string | null },
): void {
if (attribute.name !== undefined && attribute.name.length > MAX_ATTRIBUTE_NAME_LENGTH) {
throw new UsageError(
`${position}.name must be at most ${MAX_ATTRIBUTE_NAME_LENGTH} characters`,
);
}
if (
attribute.description !== undefined &&
attribute.description.length > MAX_ATTRIBUTE_DESCRIPTION_LENGTH
) {
throw new UsageError(
`${position}.description must be at most ${MAX_ATTRIBUTE_DESCRIPTION_LENGTH} characters`,
);
}
if (
attribute.default_value !== undefined &&
attribute.default_value !== null &&
attribute.default_value.length > MAX_ATTRIBUTE_DEFAULT_VALUE_LENGTH
) {
throw new UsageError(
`${position}.default_value must be at most ${MAX_ATTRIBUTE_DEFAULT_VALUE_LENGTH} characters`,
);
}
}
/** Shared help note: the documented account-level QPM ceilings for the memory API. */
export const MEMORY_RATE_LIMIT_NOTE = {
"en-US":
"Account-level rate limits: add 120 QPM, search 300 QPM, 3000 QPM across all memory APIs. On HTTP 429 back off and leave at least 1s between calls.",
"zh-CN":
"账号级限流add 120 QPM、search 300 QPM记忆库全部接口合计 3000 QPM。遇到 HTTP 429 请降速,两次请求间隔建议至少 1 秒。",
};
/** Parse a JSON object flag (`--meta-data`), rejecting arrays and primitives. */
export function parseJsonObjectFlag(flagName: string, raw: string): Record<string, unknown> {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw new UsageError(`${flagName} must be valid JSON`);
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new UsageError(`${flagName} must be a JSON object`);
}
return parsed as Record<string, unknown>;
}
/** Parse a JSON array flag (`--messages`, `--attributes`), rejecting objects and primitives. */
export function parseJsonArrayFlag<T>(flagName: string, raw: string): T[] {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw new UsageError(`${flagName} must be valid JSON`);
}
if (!Array.isArray(parsed)) {
throw new UsageError(`${flagName} must be a JSON array`);
}
return parsed as T[];
}
+92 -44
View File
@@ -1,76 +1,124 @@
import {
defineCommand,
memoryEndpoint,
memoryNodePath,
detectOutputFormat,
type FlagsDef,
type MemoryNodeUpdateRequest,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
MEMORY_LIBRARY_FLAG,
MEMORY_WORKSPACE_NOTE,
MAX_CUSTOM_CONTENT_LENGTH,
WORKSPACE_FLAG,
checkMemoryScopeLengths,
parseJsonObjectFlag,
resolveWorkspaceId,
} from "./shared.ts";
const UPDATE_FLAGS = {
nodeId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID必填" },
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory entity ID that owns the memory (required)",
"zh-CN": "记忆实体 ID标识记忆归属对象必填",
},
required: true,
},
content: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "New content for the memory node, max 512 characters (required)",
"zh-CN": "记忆节点的新内容,最多 512 个字符(必填)",
},
required: true,
},
timestamp: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Unix timestamp (seconds) of when the remembered event happened",
"zh-CN": "记忆片段对应事件发生时的秒级 Unix 时间戳",
},
},
metaData: {
type: "string",
valueHint: "<json>",
description: {
"en-US": 'Custom metadata JSON object, merged incrementally: {"key":"value"}',
"zh-CN": '用户自定义信息 JSON 对象,增量合并:{"key":"value"}',
},
},
...MEMORY_LIBRARY_FLAG,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Update a memory node content", "zh-CN": "更新记忆节点内容" },
auth: "apiKey",
usageArgs: "--node-id <id> --user-id <id> --content <text>",
flags: {
nodeId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID必填" },
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID必填" },
required: true,
},
content: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "New content for the memory node (required)",
"zh-CN": "记忆节点的新内容(必填)",
},
required: true,
},
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory library ID (non-default library)",
"zh-CN": "记忆库 ID非默认记忆库",
},
},
},
exampleArgs: [
usageArgs: "--node-id <id> --user-id <id> --content <text> [flags]",
flags: UPDATE_FLAGS,
notes: [
MEMORY_WORKSPACE_NOTE,
{
"en-US": '--node-id node_xxx --user-id user1 --content "updated memory content"',
"zh-CN": '--node-id node_xxx --user-id user1 --content "更新后的记忆内容"',
"en-US": "--content replaces the node content in full; --meta-data merges incrementally.",
"zh-CN": "--content 整体替换节点内容;--meta-data 为增量合并。",
},
],
exampleArgs: [
{
"en-US":
'--node-id node_xxx --user-id user1 --content "updated memory content" --workspace-id ws_xxx',
"zh-CN":
'--node-id node_xxx --user-id user1 --content "更新后的记忆内容" --workspace-id ws_xxx',
},
{
"en-US":
'--node-id node_xxx --user-id user1 --content "met at WAIC" --timestamp 1747278460 --meta-data \'{"city":"Shanghai"}\'',
"zh-CN":
'--node-id node_xxx --user-id user1 --content "在 WAIC 见面" --timestamp 1747278460 --meta-data \'{"city":"上海"}\'',
},
],
validate: (flags) => {
const scopeError = checkMemoryScopeLengths(flags);
if (scopeError) return scopeError;
if (flags.content.length > MAX_CUSTOM_CONTENT_LENGTH)
return `--content must be at most ${MAX_CUSTOM_CONTENT_LENGTH} characters.`;
if (flags.timestamp !== undefined && flags.timestamp < 0)
return "--timestamp must be a non-negative Unix timestamp in seconds.";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const nodeId = flags.nodeId;
const userId = flags.userId;
const content = flags.content;
const body: MemoryNodeUpdateRequest = {
user_id: userId,
custom_content: content,
user_id: flags.userId,
custom_content: flags.content,
};
if (flags.timestamp !== undefined) body.timestamp = flags.timestamp;
if (flags.metaData) body.meta_data = parseJsonObjectFlag("--meta-data", flags.metaData);
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(settings.output);
const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryNodePath(nodeId));
if (settings.dryRun) {
emitResult(
{ endpoint: ctx.client.url(memoryNodePath(nodeId)), method: "PATCH", request: body },
format,
);
emitResult({ endpoint: url, method: "PATCH", request: body }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path: memoryNodePath(nodeId),
path: url,
method: "PATCH",
body,
});
@@ -0,0 +1,33 @@
// Workspace scope shared by every command whose API lives on a per-workspace
// host (knowledge admin plane, knowledge search/chat, memory). The console
// credential scope does not apply to these, so the workspace is a per-command
// flag instead.
import { BailianError, ExitCode, type FlagsDef } from "bailian-cli-core";
export const WORKSPACE_FLAG = {
workspaceId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
"zh-CN": "API Endpoint URL 使用的 Workspace ID也可设置 BAILIAN_WORKSPACE_ID",
},
},
} satisfies FlagsDef;
/** Three-level fallback: flag > BAILIAN_WORKSPACE_ID env > config (env/config are merged into settings); missing → USAGE. */
export function resolveWorkspaceId(ctx: {
flags: { workspaceId?: string };
settings: { workspaceId?: string };
identity: { binName: string };
}): string {
const workspaceId = ctx.flags.workspaceId || ctx.settings.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
);
}
return workspaceId;
}
+4
View File
@@ -32,6 +32,10 @@ export { default as memoryList } from "./commands/memory/list.ts";
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 memoryProfileList } from "./commands/memory/profile-list.ts";
export { default as memoryProfileShow } from "./commands/memory/profile-show.ts";
export { default as memoryProfileUpdate } from "./commands/memory/profile-update.ts";
export { default as memoryProfileDelete } from "./commands/memory/profile-delete.ts";
export { default as memoryProfileGet } from "./commands/memory/profile-get.ts";
export { default as knowledgeRetrieve } from "./commands/knowledge/retrieve.ts";
export { default as knowledgeSearch } from "./commands/knowledge/search.ts";
+1
View File
@@ -34,6 +34,7 @@ export {
isDashScopeE2EReady,
isImageKbE2EReady,
isKbAdminE2EReady,
isMemoryE2EReady,
isMultimodalChatE2EReady,
isMultimodalSearchE2EReady,
isOpenApiE2EReady,
@@ -1,215 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import {
isBailianE2EEnabled,
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { MEMORY_ROUTES } from "./topic-routes.ts";
interface MemoryAddBody {
memory_ids?: string[];
}
interface MemoryListBody {
memory_nodes?: Array<{ memory_node_id: string; content: string }>;
}
interface MemorySearchBody {
memory_nodes?: Array<{ memory_node_id: string; content: string }>;
}
const DEFAULT_E2E_MEMORY_USER_ID = "e2e-vp-test";
// bailian-cli-test
const DEFAULT_E2E_MEMORY_LIBRARY_ID = "92e8626561c4472e8805d2328030d642";
/**
* 使使
*/
function memoryLibraryCliArgs(): string[] {
const id = process.env.BAILIAN_E2E_MEMORY_LIBRARY_ID?.trim() || DEFAULT_E2E_MEMORY_LIBRARY_ID;
return id ? ["--memory-library-id", id] : [];
}
/**
* Memory help / API
* E2E + DashScope dry-run CRUD skip
*/
describe("e2e: memory", () => {
test("memory add --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/add|--user-id|--content|messages/i);
});
test("memory list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|--user-id|memory-library/i);
});
test("memory search --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
"memory",
"search",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/search|--query|user-id/i);
});
test("memory profile create --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
"memory",
"profile",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/profile|create|user-id/i);
});
});
/**
*
*/
describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())(
"e2e: memory CRUD + search",
() => {
test("memory add 缺少 --user-id 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
...memoryLibraryCliArgs(),
"--content",
"仅内容无用户",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--user-id|Usage:/i);
});
test("memory add 缺少 --messages 与 --content 时报错正常退出", async () => {
const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
...memoryLibraryCliArgs(),
"--user-id",
userId,
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/messages|content|required/i);
});
test("memory add --dry-run 仅输出计划且不入网", async () => {
const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
"--dry-run",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--content",
"dry-run 不入网",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ request?: { user_id?: string; custom_content?: string } }>(
stdout,
);
expect(data.request?.user_id).toBe(userId);
expect(data.request?.custom_content).toContain("dry-run");
});
test("记忆库增删改查", async () => {
const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
const contentA = "CLI vp test记忆写入可删";
const contentB = "CLI vp test记忆已更新";
const addRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--content",
contentA,
"--output",
"json",
]);
expect(addRes.exitCode, addRes.stderr).toBe(0);
const added = parseStdoutJson<MemoryAddBody & { request_id?: string }>(addRes.stdout);
expect(added.request_id?.length ?? 0, addRes.stdout + addRes.stderr).toBeGreaterThan(0);
const listRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"list",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--output",
"json",
]);
expect(listRes.exitCode, listRes.stderr).toBe(0);
const listed = parseStdoutJson<MemoryListBody>(listRes.stdout);
if ((listed.memory_nodes?.length ?? 0) === 0) {
throw new Error(
"memory list 为空add 已成功但列表无节点。请设置 BAILIAN_E2E_MEMORY_LIBRARY_ID 与阿里云百炼控制台当前「记忆库」ID 一致;" +
"并设置 BAILIAN_E2E_MEMORY_USER_ID 与控制台筛选的「用户 ID」一致。stdout=" +
listRes.stdout,
);
}
const nodeId = listed.memory_nodes![0]!.memory_node_id.trim();
expect(nodeId.length).toBeGreaterThan(0);
const searchRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"search",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--query",
"vp test",
"--top-k",
"5",
"--output",
"json",
]);
expect(searchRes.exitCode, searchRes.stderr).toBe(0);
const searched = parseStdoutJson<MemorySearchBody>(searchRes.stdout);
expect(searched.memory_nodes?.length ?? 0).toBeGreaterThan(0);
const updRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"update",
...memoryLibraryCliArgs(),
"--node-id",
nodeId!,
"--user-id",
userId,
"--content",
contentB,
"--output",
"json",
]);
expect(updRes.exitCode, updRes.stderr).toBe(0);
const delRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"delete",
...memoryLibraryCliArgs(),
"--node-id",
nodeId!,
"--user-id",
userId,
"--output",
"json",
]);
expect(delRes.exitCode, delRes.stderr).toBe(0);
}, 180_000);
},
);
@@ -0,0 +1,262 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_ADD_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, memoryUserId, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory add", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--messages/i);
expect(stderr).toMatch(/--content/i);
expect(stderr).toMatch(/--profile-schema/i);
expect(stderr).toMatch(/--meta-data/i);
expect(stderr).toMatch(/--project-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--content",
"仅内容无用户",
]);
expect(exitCode).toBe(2);
});
test("缺 --messages 与 --content 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/messages|content/i);
});
test("--user-id 65 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"u".repeat(65),
"--content",
"over-long user id",
]);
expect(exitCode).toBe(2);
});
test("--content 513 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"x".repeat(513),
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"over-long library id",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--messages 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--messages",
"not-json",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--messages 传对象而非数组报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--messages",
'{"role":"user","content":"hi"}',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--messages 超过 50 条报 USAGE (2)", async () => {
const messages = Array.from({ length: 51 }, (_unused, index) => ({
role: "user",
content: `turn ${index}`,
}));
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--messages",
JSON.stringify(messages),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--meta-data 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"meta 格式错误",
"--meta-data",
"{oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--meta-data 传数组而非对象报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"meta 类型错误",
"--meta-data",
'["a","b"]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run + --content 断言 endpoint / POST / custom_content", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--content",
"dry-run 不入网",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/^https:\/\/ws_test\.cn-beijing\.maas\.aliyuncs\.com\//);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/add$/);
expect(data.method).toBe("POST");
expect(data.request?.user_id).toBe("user1");
expect(data.request?.custom_content).toBe("dry-run 不入网");
// Not provided → must stay out of the body entirely
expect(data.request?.messages).toBeUndefined();
expect(data.request?.meta_data).toBeUndefined();
});
test("--dry-run + --messages 断言 messages 原样进 body", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--messages",
'[{"role":"user","content":"我喜欢旅行"},{"role":"assistant","content":"记住了"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.messages).toEqual([
{ role: "user", content: "我喜欢旅行" },
{ role: "assistant", content: "记住了" },
]);
expect(data.request?.custom_content).toBeUndefined();
});
test("--dry-run 断言 --meta-data/--project-id/--profile-schema/--memory-library-id 映射", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--content",
"参加了 WAIC",
"--meta-data",
'{"location":"上海","year":2026}',
"--project-id",
"proj_test",
"--profile-schema",
"schema_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.meta_data).toEqual({ location: "上海", year: 2026 });
expect(data.request?.project_id).toBe("proj_test");
expect(data.request?.profile_schema).toBe("schema_test");
expect(data.request?.memory_library_id).toBe("lib_test");
});
test("--dry-run 同传 --content 与 --messages 时两者都进 body服务端择一", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--content",
"content wins",
"--messages",
'[{"role":"user","content":"ignored"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.custom_content).toBe("content wins");
expect(data.request?.messages).toHaveLength(1);
});
});
// The live self-cleaning chain (add → list → search → update → delete) lives in
// memory-delete.e2e.test.ts.
@@ -0,0 +1,283 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_DELETE_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryAddBody,
type MemoryDryRunBody,
type MemoryNodeListBody,
} from "./shared.ts";
describe("e2e: memory delete", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--node-id/i);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--yes/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --yes 报 CONFIRMATION_REQUIRED (7)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(7);
expect(stderr).toMatch(/--yes/i);
});
test("缺 --node-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
"--user-id",
"user1",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / DELETE / query 参数", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
"--user-id",
"user1",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
// dry-run must short-circuit before the runtime confirmation gate, otherwise
// this non-TTY run would fail asking for --yes
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
const endpoint = data.endpoint ?? "";
expect(endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\/node_test\?/);
expect(endpoint).toMatch(/user_id=user1/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
expect(data.method).toBe("DELETE");
});
test("--dry-run 不传 --memory-library-id 时 query 里不出现该键", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).not.toMatch(/memory_library_id/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory delete (live)", () => {
test("删除不存在的节点时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
...memoryScopeCliArgs(),
"--node-id",
`no-such-node-${Date.now()}`,
"--user-id",
memoryUserId(),
"--yes",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
test("记忆节点自清理闭环add → list → search → update → delete → list", async () => {
const userId = memoryUserId();
const marker = `vp-${Date.now()}`;
const contentBefore = `CLI vp test ${marker}:记忆写入(可删)`;
const contentAfter = `CLI vp test ${marker}:记忆已更新`;
const addRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"add",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--content",
contentBefore,
"--meta-data",
`{"marker":"${marker}"}`,
"--output",
"json",
]);
expect(addRes.exitCode, addRes.stderr).toBe(0);
const added = parseStdoutJson<MemoryAddBody>(addRes.stdout);
expect(added.request_id?.length ?? 0, addRes.stdout).toBeGreaterThan(0);
// The contract says AddMemory reports the changed nodes; a bare request_id
// with no node means the write silently did nothing.
expect(added.memory_nodes?.length ?? 0, addRes.stdout).toBeGreaterThan(0);
const addedNodeId = added.memory_nodes![0]!.memory_node_id;
expect(addedNodeId.length).toBeGreaterThan(0);
const listRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--page-size",
"50",
"--output",
"json",
]);
expect(listRes.exitCode, listRes.stderr).toBe(0);
const listed = parseStdoutJson<MemoryNodeListBody>(listRes.stdout);
const listedNode = listed.memory_nodes?.find((node) => node.memory_node_id === addedNodeId);
if (!listedNode) {
throw new Error(
`memory list 未包含刚写入的节点 ${addedNodeId}。请确认 BAILIAN_E2E_MEMORY_LIBRARY_ID 与控制台「记忆库」ID 一致。stdout=${listRes.stdout}`,
);
}
expect(listedNode.meta_data?.marker).toBe(marker);
const searchRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"search",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--query",
marker,
"--top-k",
"5",
"--min-score",
"0",
"--plan-version",
"lite",
"--output",
"json",
]);
expect(searchRes.exitCode, searchRes.stderr).toBe(0);
const searched = parseStdoutJson<MemoryNodeListBody>(searchRes.stdout);
expect(searched.memory_nodes?.length ?? 0, searchRes.stdout).toBeGreaterThan(0);
const updateRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"update",
...memoryScopeCliArgs(),
"--node-id",
addedNodeId,
"--user-id",
userId,
"--content",
contentAfter,
"--meta-data",
'{"updated":"true"}',
"--output",
"json",
]);
expect(updateRes.exitCode, updateRes.stderr).toBe(0);
// Verify the update landed on the server rather than trusting the 200
const afterUpdateRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--page-size",
"50",
"--output",
"json",
]);
expect(afterUpdateRes.exitCode, afterUpdateRes.stderr).toBe(0);
const afterUpdate = parseStdoutJson<MemoryNodeListBody>(afterUpdateRes.stdout);
const updatedNode = afterUpdate.memory_nodes?.find(
(node) => node.memory_node_id === addedNodeId,
);
expect(updatedNode?.content, afterUpdateRes.stdout).toBe(contentAfter);
// update mutated both content and meta_data; verify the meta_data write
// landed too rather than only the content
expect(updatedNode?.meta_data?.updated, afterUpdateRes.stdout).toBe("true");
const deleteRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
...memoryScopeCliArgs(),
"--node-id",
addedNodeId,
"--user-id",
userId,
"--yes",
"--output",
"json",
]);
expect(deleteRes.exitCode, deleteRes.stderr).toBe(0);
const afterDeleteRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--page-size",
"50",
"--output",
"json",
]);
expect(afterDeleteRes.exitCode, afterDeleteRes.stderr).toBe(0);
const afterDelete = parseStdoutJson<MemoryNodeListBody>(afterDeleteRes.stdout);
expect(
afterDelete.memory_nodes?.some((node) => node.memory_node_id === addedNodeId) ?? false,
afterDeleteRes.stdout,
).toBe(false);
}, 240_000);
});
@@ -0,0 +1,181 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_LIST_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
type MemoryNodeListBody,
} from "./shared.ts";
describe("e2e: memory list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--page-size/i);
expect(stderr).toMatch(/--page/i);
expect(stderr).toMatch(/--project-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 workspace 时报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(
MEMORY_LIST_ROUTES,
["memory", "list", "--user-id", "user1", "--api-key", "sk-fake", "--output", "json"],
{ BAILIAN_WORKSPACE_ID: "", BAILIAN_CONFIG_DIR: "/tmp" },
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/workspace.*required/i);
});
test("裸调用(无任何 flag打 help 并 exit 0", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, ["memory", "list"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--page-size",
"10",
]);
expect(exitCode).toBe(2);
});
test("--page 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--page",
"0",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--page-size 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--page-size",
"0",
]);
expect(exitCode).toBe(2);
});
test("--page 非数字报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--page",
"abc",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / GET / user_id query", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
// The memory API is served on the workspace-specific host, not the model domain
expect(data.endpoint).toMatch(/^https:\/\/ws_test\.cn-beijing\.maas\.aliyuncs\.com\//);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\?user_id=user1$/);
expect(data.method).toBe("GET");
});
test("--dry-run 断言分页 / project-id / memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
"user1",
"--page-size",
"20",
"--page",
"2",
"--project-id",
"proj_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
const endpoint = data.endpoint ?? "";
expect(endpoint).toMatch(/page_size=20/);
// The API paginates with page_num, not page
expect(endpoint).toMatch(/page_num=2/);
expect(endpoint).toMatch(/project_id=proj_test/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory list (live)", () => {
test("列出不存在的记忆实体返回空列表而非报错", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
`no-such-entity-${Date.now()}`,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryNodeListBody>(stdout);
expect(data.request_id?.length ?? 0).toBeGreaterThan(0);
expect(data.memory_nodes ?? []).toHaveLength(0);
});
test("text 输出对空列表给出明确提示", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
`no-such-entity-${Date.now()}`,
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(/No memory nodes found/i);
});
});
@@ -0,0 +1,211 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_CREATE_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory profile create", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--name/i);
expect(stderr).toMatch(/--description/i);
expect(stderr).toMatch(/--attributes/i);
expect(stderr).toMatch(/--plan-version/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --name 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--attributes",
'[{"name":"age"}]',
]);
expect(exitCode).toBe(2);
});
test("缺 --attributes 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
]);
expect(exitCode).toBe(2);
});
test("--attributes 空数组报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
"[]",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/attribute/i);
});
test("--name 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"n".repeat(33),
"--attributes",
'[{"name":"age"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--description 129 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--description",
"d".repeat(129),
"--attributes",
'[{"name":"age"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--attributes 内属性 name 超长报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
`[{"name":"${"a".repeat(33)}"}]`,
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--attributes 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
"[oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--attributes 传对象而非数组报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
'{"name":"age"}',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--plan-version 非法取值报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
'[{"name":"age"}]',
"--plan-version",
"ultra",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/pro|lite/i);
});
test("--dry-run 断言 endpoint / POST / name / attributes", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
'[{"name":"age","description":"年龄"},{"name":"hobby"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas$/);
expect(data.method).toBe("POST");
expect(data.request?.name).toBe("user_basic");
expect(data.request?.attributes).toEqual([
{ name: "age", description: "年龄" },
{ name: "hobby" },
]);
expect(data.request?.description).toBeUndefined();
expect(data.request?.plan_version).toBeUndefined();
});
test("--dry-run 断言 --description/--plan-version/--memory-library-id 映射", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--description",
"基础画像",
"--attributes",
'[{"name":"age","default_value":"18"}]',
"--plan-version",
"lite",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.description).toBe("基础画像");
expect(data.request?.plan_version).toBe("lite");
expect(data.request?.memory_library_id).toBe("lib_test");
expect(data.request?.attributes).toEqual([{ name: "age", default_value: "18" }]);
});
});
// The live self-cleaning chain (create → list → show → update → get → delete)
// lives in memory-profile-delete.e2e.test.ts.
@@ -0,0 +1,293 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_DELETE_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
type ProfileSchemaCreateBody,
type ProfileSchemaDetailBody,
type ProfileSchemaListBody,
type UserProfileBody,
} from "./shared.ts";
describe("e2e: memory profile delete", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--yes/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--yes",
]);
expect(exitCode).toBe(2);
});
test("缺 --yes 报 CONFIRMATION_REQUIRED (7)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
"--api-key",
"sk-fake",
...TEST_WORKSPACE_ARGS,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(7);
expect(stderr).toMatch(/--yes/i);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / DELETE且不触发确认", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
// dry-run must short-circuit before the runtime confirmation gate, otherwise
// this non-TTY run would fail asking for --yes
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas\/schema_test$/);
expect(data.method).toBe("DELETE");
});
test("--dry-run 断言 --memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/profile_schemas\/schema_test\?memory_library_id=lib_test$/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile delete (live)", () => {
test("删除不存在的模板时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
...memoryScopeCliArgs(),
"--schema-id",
`no-such-schema-${Date.now()}`,
"--yes",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
test("画像模板自清理闭环create → list → show → update → show → get → delete → list", async () => {
const marker = `vp-${Date.now()}`;
const schemaName = `vp_profile_${marker}`;
// plan_version lite keeps the per-call price at the cheaper tier
const createRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"create",
...memoryScopeCliArgs(),
"--name",
schemaName,
"--description",
`CLI vp test ${marker}(可删)`,
"--attributes",
'[{"name":"plan","description":"套餐"},{"name":"tier","description":"等级"}]',
"--plan-version",
"lite",
"--output",
"json",
]);
expect(createRes.exitCode, createRes.stderr).toBe(0);
const created = parseStdoutJson<ProfileSchemaCreateBody>(createRes.stdout);
const schemaId = created.profile_schema_id ?? "";
expect(schemaId.length, createRes.stdout).toBeGreaterThan(0);
try {
const listRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"list",
...memoryScopeCliArgs(),
"--page-size",
"100",
"--output",
"json",
]);
expect(listRes.exitCode, listRes.stderr).toBe(0);
const listed = parseStdoutJson<ProfileSchemaListBody>(listRes.stdout);
const listedSchema = listed.profile_schemas?.find(
(schema) => schema.profile_schema_id === schemaId,
);
if (!listedSchema) {
throw new Error(
`memory profile list 未包含刚创建的模板 ${schemaId}。请确认 BAILIAN_E2E_MEMORY_LIBRARY_ID 与控制台「记忆库」ID 一致。stdout=${listRes.stdout}`,
);
}
expect(listedSchema.name).toBe(schemaName);
const showRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"show",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--output",
"json",
]);
expect(showRes.exitCode, showRes.stderr).toBe(0);
const detail = parseStdoutJson<ProfileSchemaDetailBody>(showRes.stdout);
expect(detail.name).toBe(schemaName);
const planAttribute = detail.attributes?.find((attribute) => attribute.name === "plan");
const tierAttribute = detail.attributes?.find((attribute) => attribute.name === "tier");
// attribute_id is the handle profile update needs; without it the
// update / delete operations below cannot be expressed at all
expect(planAttribute?.attribute_id, showRes.stdout).toBeTruthy();
expect(tierAttribute?.attribute_id, showRes.stdout).toBeTruthy();
const renamed = `${schemaName}_v2`;
const updateRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"update",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--name",
renamed,
"--attributes-operations",
JSON.stringify([
{ op: "add", name: "region", description: "地域" },
{
op: "update",
attribute_id: planAttribute!.attribute_id,
description: "套餐(已更新)",
},
{ op: "delete", attribute_id: tierAttribute!.attribute_id },
]),
"--output",
"json",
]);
expect(updateRes.exitCode, updateRes.stderr).toBe(0);
// Verify the mutation landed on the server rather than trusting the 200
const afterUpdateRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"show",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--output",
"json",
]);
expect(afterUpdateRes.exitCode, afterUpdateRes.stderr).toBe(0);
const afterUpdate = parseStdoutJson<ProfileSchemaDetailBody>(afterUpdateRes.stdout);
expect(afterUpdate.name, afterUpdateRes.stdout).toBe(renamed);
const attributeNames = (afterUpdate.attributes ?? []).map((attribute) => attribute.name);
expect(attributeNames, afterUpdateRes.stdout).toContain("region");
expect(attributeNames, afterUpdateRes.stdout).not.toContain("tier");
expect(
afterUpdate.attributes?.find((attribute) => attribute.name === "plan")?.description,
afterUpdateRes.stdout,
).toBe("套餐(已更新)");
// No memory was written against this schema, so every attribute value
// is expected to be empty — the point is that the endpoint resolves
const getRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"get",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--user-id",
memoryUserId(),
"--output",
"json",
]);
expect(getRes.exitCode, getRes.stderr).toBe(0);
const profile = parseStdoutJson<UserProfileBody>(getRes.stdout);
expect(profile.request_id?.length ?? 0, getRes.stdout).toBeGreaterThan(0);
} finally {
const deleteRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--yes",
"--output",
"json",
]);
expect(deleteRes.exitCode, deleteRes.stderr).toBe(0);
}
const afterDeleteRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"list",
...memoryScopeCliArgs(),
"--page-size",
"100",
"--output",
"json",
]);
expect(afterDeleteRes.exitCode, afterDeleteRes.stderr).toBe(0);
const afterDelete = parseStdoutJson<ProfileSchemaListBody>(afterDeleteRes.stdout);
expect(
afterDelete.profile_schemas?.some((schema) => schema.profile_schema_id === schemaId) ?? false,
afterDeleteRes.stdout,
).toBe(false);
}, 240_000);
});
@@ -0,0 +1,127 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_GET_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
} from "./shared.ts";
describe("e2e: memory profile get", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--user-id",
"user1",
]);
expect(exitCode).toBe(2);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
"--user-id",
"user1",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run endpoint 命中 /user_profile 子资源", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
// Regression guard: this path used to point at /profiles, which the API rejects
expect(data.endpoint).toMatch(
/api\/v2\/apps\/memory\/profile_schemas\/schema_test\/user_profile\?user_id=user1$/,
);
expect(data.method).toBe("GET");
});
test("--dry-run 断言 --memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
"--user-id",
"user1",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const endpoint = parseStdoutJson<MemoryDryRunBody>(stdout).endpoint ?? "";
expect(endpoint).toMatch(/user_id=user1/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile get (live)", () => {
test("不存在的 schema 被服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
...memoryScopeCliArgs(),
"--schema-id",
`no-such-schema-${Date.now()}`,
"--user-id",
memoryUserId(),
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,146 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_LIST_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
type MemoryDryRunBody,
type ProfileSchemaListBody,
} from "./shared.ts";
describe("e2e: memory profile list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--page-size/i);
expect(stderr).toMatch(/--page/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("--page 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--page",
"0",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--page-size 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--page-size",
"0",
]);
expect(exitCode).toBe(2);
});
test("未知 flag 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--user-id",
"user1",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 无参数时 endpoint 不带 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas$/);
expect(data.method).toBe("GET");
});
test("--dry-run 断言分页与 memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--page-size",
"20",
"--page",
"2",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
const endpoint = data.endpoint ?? "";
expect(endpoint).toMatch(/page_size=20/);
expect(endpoint).toMatch(/page_num=2/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile list (live)", () => {
test("列出画像模板返回 request_id 与 total", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
...memoryScopeCliArgs(),
"--page-size",
"5",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<ProfileSchemaListBody>(stdout);
expect(data.request_id?.length ?? 0).toBeGreaterThan(0);
for (const schema of data.profile_schemas ?? []) {
expect(schema.profile_schema_id.length).toBeGreaterThan(0);
expect(schema.name.length).toBeGreaterThan(0);
}
});
test("--memory-library-id 不存在时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--memory-library-id",
"no-such-library-000000000000000",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,115 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_SHOW_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, memoryScopeCliArgs, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory profile show", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--memory-library-id",
"lib_test",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"schema_test",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / GET", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"schema_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas\/schema_test$/);
expect(data.method).toBe("GET");
});
test("--dry-run 断言 --memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"schema_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/profile_schemas\/schema_test\?memory_library_id=lib_test$/);
});
test("--dry-run schema id 做 URL 编码", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"a/b c",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/profile_schemas\/a%2Fb%20c$/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile show (live)", () => {
test("不存在的 schema 被服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
...memoryScopeCliArgs(),
"--schema-id",
`no-such-schema-${Date.now()}`,
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,244 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_UPDATE_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory profile update", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--name/i);
expect(stderr).toMatch(/--description/i);
expect(stderr).toMatch(/--attributes-operations/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--name",
"user_basic_v2",
]);
expect(exitCode).toBe(2);
});
test("三个可改字段全不传报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--attributes-operations/i);
});
test("--attributes-operations 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
"[oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--attributes-operations 传对象而非数组报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'{"op":"add","name":"plan"}',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("op 非法取值报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"upsert","name":"plan"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/op/i);
});
test("op=add 缺 name 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"add","description":"套餐"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/name/i);
});
test("op=delete 缺 attribute_id 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"delete"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/attribute_id/i);
});
test("op=update 缺 attribute_id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"update","name":"plan"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--name 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--name",
"n".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("op=add 属性 name 超长报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
`[{"op":"add","name":"${"a".repeat(33)}"}]`,
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / PATCH / name 与 description", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--name",
"user_basic_v2",
"--description",
"升级后的画像",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas\/schema_test$/);
expect(data.method).toBe("PATCH");
expect(data.request?.name).toBe("user_basic_v2");
expect(data.request?.description).toBe("升级后的画像");
expect(data.request?.attributes_operations).toBeUndefined();
});
test("--dry-run 断言三种 op 原样进 body含 default_value: null", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"add","name":"plan","default_value":"free"},' +
'{"op":"update","attribute_id":"attr_1","default_value":null},' +
'{"op":"delete","attribute_id":"attr_2"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.attributes_operations).toEqual([
{ op: "add", name: "plan", default_value: "free" },
{ op: "update", attribute_id: "attr_1", default_value: null },
{ op: "delete", attribute_id: "attr_2" },
]);
});
test("--dry-run 断言 memory_library_id 进 body 而不是 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--name",
"user_basic_v2",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
// UpdateProfileSchema is the only profile endpoint carrying the library in the body
expect(data.request?.memory_library_id).toBe("lib_test");
expect(data.endpoint).not.toMatch(/memory_library_id/);
});
});
// The live update path is exercised inside the self-cleaning chain in
// memory-profile-delete.e2e.test.ts, which owns the schema it mutates.
@@ -0,0 +1,300 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_SEARCH_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
type MemoryNodeListBody,
} from "./shared.ts";
describe("e2e: memory search", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--query/i);
expect(stderr).toMatch(/--messages/i);
expect(stderr).toMatch(/--top-k/i);
expect(stderr).toMatch(/--min-score/i);
expect(stderr).toMatch(/--enable-rerank/i);
expect(stderr).toMatch(/--enable-judge/i);
expect(stderr).toMatch(/--enable-rewrite/i);
expect(stderr).toMatch(/--plan-version/i);
expect(stderr).toMatch(/--workspace-id/i);
expect(stderr).toMatch(/--project-id/i);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--query",
"编程偏好",
]);
expect(exitCode).toBe(2);
});
test("缺 --query 与 --messages 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/query|messages/i);
});
test("--top-k 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--top-k",
"0",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--top-k 101 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--top-k",
"101",
]);
expect(exitCode).toBe(2);
});
test("--min-score 1.5 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--min-score",
"1.5",
]);
expect(exitCode).toBe(2);
});
test("--plan-version 非法取值报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--plan-version",
"ultra",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/pro|lite/i);
});
test("--enable-rerank 非 true/false 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--enable-rerank",
"yes",
]);
expect(exitCode).toBe(2);
});
test("--messages 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--messages",
"[oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run + --query 断言 endpoint 与 query 被包成单条 user message", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"编程偏好",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\/search$/);
expect(data.method).toBe("POST");
expect(data.request?.messages).toEqual([{ role: "user", content: "编程偏好" }]);
// `query` is not part of the API contract — it must never reach the body
expect(data.request?.query).toBeUndefined();
});
test("--dry-run + --messages 覆盖 --query", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"被忽略",
"--messages",
'[{"role":"user","content":"推荐一本书"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.messages).toEqual([{ role: "user", content: "推荐一本书" }]);
});
test("--dry-run 断言检索调参全部映射进 body", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"提醒事项",
"--top-k",
"5",
"--min-score",
"0",
"--enable-rerank",
"true",
"--enable-judge",
"true",
"--enable-rewrite",
"false",
"--plan-version",
"lite",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.top_k).toBe(5);
// 0 is a meaningful threshold — it must not be dropped as falsy
expect(data.request?.min_score).toBe(0);
expect(data.request?.enable_rerank).toBe(true);
expect(data.request?.enable_judge).toBe(true);
expect(data.request?.enable_rewrite).toBe(false);
expect(data.request?.plan_version).toBe("lite");
expect(data.request?.memory_library_id).toBe("lib_test");
});
test("--dry-run 断言可重复 --project-id 汇成 project_ids", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"多规则混检",
"--project-id",
"proj_a",
"--project-id",
"proj_b",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.project_ids).toEqual(["proj_a", "proj_b"]);
expect(data.request?.project_id).toBeUndefined();
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory search (live)", () => {
test("检索不存在的记忆实体返回空列表而非报错", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
...memoryScopeCliArgs(),
"--user-id",
`no-such-entity-${Date.now()}`,
"--query",
"任何内容",
"--top-k",
"3",
"--plan-version",
"lite",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryNodeListBody>(stdout);
expect(data.request_id?.length ?? 0).toBeGreaterThan(0);
expect(data.memory_nodes ?? []).toHaveLength(0);
});
test("--memory-library-id 不存在时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--memory-library-id",
"no-such-library-000000000000000",
"--user-id",
memoryUserId(),
"--query",
"任何内容",
"--plan-version",
"lite",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,223 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_UPDATE_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
} from "./shared.ts";
describe("e2e: memory update", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--node-id/i);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--content/i);
expect(stderr).toMatch(/--timestamp/i);
expect(stderr).toMatch(/--meta-data/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --node-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--user-id",
memoryUserId(),
"--content",
"新内容",
]);
expect(exitCode).toBe(2);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--content",
"新内容",
]);
expect(exitCode).toBe(2);
});
test("缺 --content 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
});
test("--content 513 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"x".repeat(513),
]);
expect(exitCode).toBe(2);
});
test("--timestamp 负数报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"新内容",
"--timestamp",
"-1",
]);
expect(exitCode).toBe(2);
});
test("--memory-library-id 33 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"新内容",
"--memory-library-id",
"l".repeat(33),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--meta-data 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"新内容",
"--meta-data",
"{oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / PATCH / custom_content", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
"user1",
"--content",
"更新后的记忆内容",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\/node_test$/);
expect(data.method).toBe("PATCH");
expect(data.request?.user_id).toBe("user1");
expect(data.request?.custom_content).toBe("更新后的记忆内容");
expect(data.request?.timestamp).toBeUndefined();
expect(data.request?.meta_data).toBeUndefined();
});
test("--dry-run 断言 --timestamp/--meta-data/--memory-library-id 映射", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
"user1",
"--content",
"在 WAIC 见面",
"--timestamp",
"1747278460",
"--meta-data",
'{"city":"上海"}',
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.timestamp).toBe(1747278460);
expect(data.request?.meta_data).toEqual({ city: "上海" });
expect(data.request?.memory_library_id).toBe("lib_test");
});
test("--dry-run 断言 node id 被 URL 编码", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node/with space",
"--user-id",
"user1",
"--content",
"编码校验",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/memory_nodes\/node%2Fwith%20space$/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory update (live)", () => {
test("更新不存在的节点时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
...memoryScopeCliArgs(),
"--node-id",
`no-such-node-${Date.now()}`,
"--user-id",
memoryUserId(),
"--content",
"不存在的节点",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
// The live update-in-place assertion lives in the CRUD chain in memory-delete.e2e.test.ts.
@@ -0,0 +1,126 @@
/**
* Shared fixtures for the memory E2E suite.
*
* The memory API has no "list memory libraries" endpoint, so the library ID
* cannot be discovered from the CLI it has to be copied from the console
* (Bailian card) into `BAILIAN_E2E_MEMORY_LIBRARY_ID`.
* No hardcoded fallback on purpose: a stale ID would make live cases fail with
* a confusing InvalidParameter instead of skipping via `isMemoryE2EReady()`.
*
* The memory API is served on a workspace-specific host, so every case that
* reaches `run()` including `--dry-run` needs a workspace ID.
*/
const DEFAULT_E2E_MEMORY_USER_ID = "e2e-vp-test";
/** Memory entity ID used by every live case; env var wins over the default. */
export function memoryUserId(): string {
return process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
}
/**
* Fixed workspace for offline cases: it only shapes the endpoint host, so a
* literal keeps `--dry-run` assertions deterministic on any machine.
*/
export const TEST_WORKSPACE_ARGS = ["--workspace-id", "ws_test"];
/**
* Scope argv for live cases: memory library + workspace. `isMemoryE2EReady()`
* guarantees both env vars are set.
*/
export function memoryScopeCliArgs(): string[] {
const libraryId = process.env.BAILIAN_E2E_MEMORY_LIBRARY_ID?.trim();
const workspaceId = process.env.BAILIAN_WORKSPACE_ID?.trim();
return [
...(libraryId ? ["--memory-library-id", libraryId] : []),
...(workspaceId ? ["--workspace-id", workspaceId] : []),
];
}
/** `--dry-run --output json` payload shape shared by the memory commands. */
export interface MemoryDryRunBody {
endpoint?: string;
method?: string;
request?: {
user_id?: string;
custom_content?: string;
messages?: Array<{ role?: string; content?: string }>;
meta_data?: Record<string, unknown>;
project_id?: string;
project_ids?: string[];
profile_schema?: string;
memory_library_id?: string;
timestamp?: number;
top_k?: number;
min_score?: number;
enable_rerank?: boolean;
enable_judge?: boolean;
enable_rewrite?: boolean;
plan_version?: string;
query?: string;
name?: string;
description?: string;
attributes?: Array<{ name?: string; description?: string; default_value?: string }>;
attributes_operations?: Array<{
op?: string;
attribute_id?: string;
name?: string;
description?: string;
default_value?: string | null;
}>;
};
}
export interface MemoryNodeListBody {
request_id?: string;
total?: number;
memory_nodes?: Array<{
memory_node_id: string;
content: string;
meta_data?: Record<string, unknown>;
created_at?: number;
updated_at?: number;
}>;
}
export interface MemoryAddBody {
request_id?: string;
memory_nodes?: Array<{
memory_node_id: string;
content: string;
event?: string;
old_content?: string;
}>;
}
export interface ProfileSchemaCreateBody {
request_id?: string;
profile_schema_id?: string;
}
export interface ProfileSchemaListBody {
request_id?: string;
total?: number;
profile_schemas?: Array<{ profile_schema_id: string; name: string; description?: string }>;
}
export interface ProfileSchemaDetailBody {
request_id?: string;
name?: string;
description?: string;
attributes?: Array<{
attribute_id: string;
name: string;
description?: string;
default_value?: string;
}>;
}
export interface UserProfileBody {
request_id?: string;
profile?: {
schema_name?: string;
schema_description?: string;
attributes?: Array<{ id: string; name: string; value?: string }>;
};
}
+44 -1
View File
@@ -25,16 +25,59 @@ export const CONFIG_ROUTES: E2eRouteExports = {
"config agent": "configAgent",
};
export const MEMORY_ROUTES: E2eRouteExports = {
export const MEMORY_ADD_ROUTES: E2eRouteExports = {
"memory add": "memoryAdd",
};
export const MEMORY_SEARCH_ROUTES: E2eRouteExports = {
"memory search": "memorySearch",
};
export const MEMORY_LIST_ROUTES: E2eRouteExports = {
"memory list": "memoryList",
};
export const MEMORY_UPDATE_ROUTES: E2eRouteExports = {
"memory update": "memoryUpdate",
};
export const MEMORY_DELETE_ROUTES: E2eRouteExports = {
"memory delete": "memoryDelete",
"memory add": "memoryAdd", // live self-cleaning chain
"memory list": "memoryList", // live: resolve the node id add just produced
"memory search": "memorySearch", // live: verify the node is retrievable
"memory update": "memoryUpdate", // live update step in the chain
};
export const MEMORY_PROFILE_CREATE_ROUTES: E2eRouteExports = {
"memory profile create": "memoryProfileCreate",
};
export const MEMORY_PROFILE_LIST_ROUTES: E2eRouteExports = {
"memory profile list": "memoryProfileList",
};
export const MEMORY_PROFILE_SHOW_ROUTES: E2eRouteExports = {
"memory profile show": "memoryProfileShow",
};
export const MEMORY_PROFILE_UPDATE_ROUTES: E2eRouteExports = {
"memory profile update": "memoryProfileUpdate",
};
export const MEMORY_PROFILE_GET_ROUTES: E2eRouteExports = {
"memory profile get": "memoryProfileGet",
};
export const MEMORY_PROFILE_DELETE_ROUTES: E2eRouteExports = {
"memory profile delete": "memoryProfileDelete",
"memory profile create": "memoryProfileCreate", // live self-cleaning chain
"memory profile list": "memoryProfileList", // live: the new schema shows up in the list
"memory profile show": "memoryProfileShow", // live: attribute_id handles for update
"memory profile update": "memoryProfileUpdate", // live update step in the chain
"memory profile get": "memoryProfileGet", // live: user profile reachable through the schema
};
export const KNOWLEDGE_ROUTES: E2eRouteExports = {
"knowledge retrieve": "knowledgeRetrieve",
"knowledge search": "knowledgeSearch",
+30 -6
View File
@@ -1,5 +1,18 @@
// API path builders — return the path only; the Client prepends the
// credential's baseUrl. Commands never see baseUrl.
//
// Exception: workspace-scoped APIs (RAG admin plane, knowledge search/chat,
// memory) live on a per-workspace host, so their builders return an absolute
// URL — see `workspaceEndpoint`.
/**
* Workspace-scoped API host. Everything reached through the workspace gateway
* (RAG admin plane, knowledge search/chat, memory) shares this host shape, so
* it is spelled out exactly once here.
*/
function workspaceEndpoint(workspaceId: string, path: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${path}`;
}
// ---- Chat (OpenAI Compatible) ----
export function chatPath(): string {
@@ -62,7 +75,14 @@ export function appCompletionPath(appId: string): string {
return `/api/v1/apps/${encodeURIComponent(appId)}/completion`;
}
// ---- Memory (DashScope v2) ----
// ---- Memory (long-term memory v2, workspace-based host) ----
// The path builders below return the path portion only; wrap them in
// `memoryEndpoint` to get the URL the memory API is actually served on.
export function memoryEndpoint(workspaceId: string, path: string): string {
return workspaceEndpoint(workspaceId, path);
}
export function memoryAddPath(): string {
return "/api/v2/apps/memory/add";
}
@@ -94,13 +114,17 @@ export function speechVocabularyPath(): string {
return "/api/v1/services/audio/asr/customization";
}
// ---- Memory Profile (DashScope v2) ----
// ---- Memory Profile (long-term memory v2, workspace-based host) ----
export function profileSchemaPath(): string {
return "/api/v2/apps/memory/profile_schemas";
}
export function profileSchemaItemPath(schemaId: string): string {
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}`;
}
export function userProfilePath(schemaId: string): string {
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/profiles`;
return `${profileSchemaItemPath(schemaId)}/user_profile`;
}
// ---- Knowledge Base Retrieve (DashScope) ----
@@ -111,13 +135,13 @@ export function knowledgeRetrievePath(): string {
// ---- Knowledge Search (新版 RAG 检索, workspace-based host) ----
export function knowledgeSearchEndpoint(workspaceId: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`;
return workspaceEndpoint(workspaceId, "/api/v1/indices/knowledge/search");
}
// ---- Knowledge Chat (新版 RAG 问答, workspace-based host) ----
export function knowledgeChatEndpoint(workspaceId: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat`;
return workspaceEndpoint(workspaceId, "/api/v2/apps/knowledge/chat");
}
// ---- MCP Services (Streamable HTTP) ----
@@ -228,7 +252,7 @@ export function deploymentsModelsPath(): string {
// RAG_PATHS to avoid per-endpoint boilerplate.
export function ragEndpoint(workspaceId: string, path: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${path}`;
return workspaceEndpoint(workspaceId, path);
}
export const RAG_PATHS = {
+2
View File
@@ -9,12 +9,14 @@ export {
knowledgeRetrievePath,
knowledgeSearchEndpoint,
memoryAddPath,
memoryEndpoint,
memoryListPath,
memoryNodePath,
memorySearchPath,
mcpWebSearchPath,
modelsLimitsPath,
modelsPermissionsPath,
profileSchemaItemPath,
profileSchemaPath,
ragEndpoint,
RAG_PATHS,
+99 -11
View File
@@ -341,30 +341,62 @@ export interface AppStreamChunk {
// ---- Memory (DashScope v2) ----
/**
* Multimodal message part. The memory API reference only documents `content`
* as `string | array` without pinning the element shape, so this mirrors the
* sibling Knowledge Chat contract (`KnowledgeChatContentPart`).
*/
export type MemoryContentPart =
| { type: "text"; text: string }
| { type: "image_url"; image_url: { url: string } };
export interface MemoryMessage {
role: "user" | "assistant";
content: string;
content: string | MemoryContentPart[];
}
/** 记忆抽取策略版本pro 开启 Reranklite 关闭(单价不同)。 */
export type MemoryPlanVersion = "pro" | "lite";
export interface MemoryAddRequest {
user_id: string;
messages?: MemoryMessage[];
custom_content?: string;
profile_schema?: string;
memory_library_id?: string;
/** 记忆片段规则 ID不传则用记忆库的默认规则 */
project_id?: string;
meta_data?: Record<string, unknown>;
}
/** AddMemory 返回的变更记录:一次调用可能产生多条 ADD/UPDATE/DELETE。 */
export interface MemoryChangedNode {
memory_node_id: string;
content: string;
event?: "ADD" | "UPDATE" | "DELETE";
/** 仅 event 为 UPDATE 时有效 */
old_content?: string;
}
export interface MemoryAddResponse {
request_id: string;
memory_ids?: string[];
memory_nodes?: MemoryChangedNode[];
}
export interface MemorySearchRequest {
user_id: string;
messages?: MemoryMessage[];
query?: string;
top_k?: number;
/** 最小相似度阈值,值域 [0,1] */
min_score?: number;
enable_rerank?: boolean;
/** 优先级高于 enable_rerank传了本字段时 enable_rerank 被忽略 */
plan_version?: MemoryPlanVersion;
enable_judge?: boolean;
enable_rewrite?: boolean;
memory_library_id?: string;
/** 记忆片段规则 ID 数组,可多规则混合检索 */
project_ids?: string[];
}
export interface MemoryNode {
@@ -372,8 +404,10 @@ export interface MemoryNode {
content: string;
user_id?: string;
meta_data?: Record<string, unknown>;
created_at?: string;
updated_at?: string;
/** 秒级 Unix 时间戳 */
created_at?: number;
/** 秒级 Unix 时间戳 */
updated_at?: number;
}
export interface MemorySearchResponse {
@@ -394,20 +428,26 @@ 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;
value?: string;
description?: string;
default_value?: string;
}
export interface ProfileSchemaCreateRequest {
name: string;
description?: string;
attributes: ProfileAttribute[];
memory_library_id?: string;
plan_version?: MemoryPlanVersion;
}
export interface ProfileSchemaCreateResponse {
@@ -415,12 +455,60 @@ 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;
}
/** GetProfileSchema属性带 attribute_id是 UpdateProfileSchema 的操作句柄 */
export interface ProfileSchemaAttribute extends ProfileAttribute {
attribute_id: string;
}
export interface ProfileSchemaDetailResponse {
request_id: string;
name?: string;
description?: string;
attributes?: ProfileSchemaAttribute[];
}
export interface ProfileSchemaAttributeOperation {
op: "add" | "update" | "delete";
/** op 为 update / delete 时必填 */
attribute_id?: string;
/** op 为 add 时必填 */
name?: string;
description?: string;
default_value?: string | null;
}
export interface ProfileSchemaUpdateRequest {
name?: string;
description?: string;
attributes_operations?: ProfileSchemaAttributeOperation[];
memory_library_id?: string;
}
/** GetUserProfile 的属性value 未提取时字段缺失 */
export interface UserProfileAttribute {
id: string;
name: string;
value?: string;
}
export interface UserProfileResponse {
request_id: string;
profile: {
schema_id: string;
user_id: string;
attributes: ProfileAttribute[];
profile?: {
schema_name?: string;
schema_description?: string;
attributes?: UserProfileAttribute[];
};
}
+10
View File
@@ -71,17 +71,27 @@ export type {
KnowledgeSearchResponse,
MemoryAddRequest,
MemoryAddResponse,
MemoryChangedNode,
MemoryContentPart,
MemoryMessage,
MemoryNode,
MemoryNodeListResponse,
MemoryNodeUpdateRequest,
MemoryPlanVersion,
MemorySearchRequest,
MemorySearchResponse,
ProfileAttribute,
ProfileSchemaAttribute,
ProfileSchemaAttributeOperation,
ProfileSchemaCreateRequest,
ProfileSchemaCreateResponse,
ProfileSchemaDetailResponse,
ProfileSchemaListResponse,
ProfileSchemaSummary,
ProfileSchemaUpdateRequest,
StreamChoice,
StreamChunk,
UserProfileAttribute,
UserProfileResponse,
} from "./api.ts";
export type * from "./knowledge-admin.ts";
+15
View File
@@ -75,6 +75,21 @@ export function isConnectorE2EReady(): boolean {
return isKbAdminE2EReady() && process.env.BAILIAN_E2E_CONNECTOR === "1";
}
/**
* fixturememory / memory profile live
*
* API ID
* ServiceNotOpened
* API workspace BAILIAN_WORKSPACE_ID
*/
export function isMemoryE2EReady(): boolean {
return (
isDashScopeE2EReady() &&
!!process.env.BAILIAN_E2E_MEMORY_LIBRARY_ID?.trim() &&
!!process.env.BAILIAN_WORKSPACE_ID?.trim()
);
}
// ---- Long-lived knowledge fixtures (created manually in the console; the CLI
// cannot create table/image-type bases or multimodal services itself) ----
+6 -2
View File
@@ -67,7 +67,11 @@ Use this index for the skill-scoped quick index and global flags.
| `bl memory delete` | API Key | Delete a memory node | [memory.md](memory.md) |
| `bl memory list` | API Key | List memory nodes for a user | [memory.md](memory.md) |
| `bl memory profile create` | API Key | Create a user profile schema for memory profiling | [memory.md](memory.md) |
| `bl memory profile get` | API Key | Get user profile by schema ID and user ID | [memory.md](memory.md) |
| `bl memory profile delete` | API Key | Delete a profile schema | [memory.md](memory.md) |
| `bl memory profile get` | API Key | Get the extracted user profile for a schema | [memory.md](memory.md) |
| `bl memory profile list` | API Key | List profile schemas | [memory.md](memory.md) |
| `bl memory profile show` | API Key | Show a profile schema definition with attribute IDs | [memory.md](memory.md) |
| `bl memory profile update` | API Key | Update a profile schema name, description, or attributes | [memory.md](memory.md) |
| `bl memory search` | API Key | Search memory nodes by query or messages | [memory.md](memory.md) |
| `bl memory update` | API Key | Update a memory node content | [memory.md](memory.md) |
| `bl model list` | No Auth | Browse model families or show detailed model info in the Bailian model marketplace | [model.md](model.md) |
@@ -118,7 +122,7 @@ Use this index for the skill-scoped quick index and global flags.
| `file` | `upload` | [file.md](file.md) |
| `knowledge` | `category add`, `category delete`, `category list`, `chat`, `chunk add`, `chunk delete`, `chunk list`, `chunk update`, `collection create`, `collection get`, `create`, `delete`, `doc delete`, `doc import-oss`, `doc list`, `doc status`, `doc tag`, `doc upload`, `file delete`, `file get`, `file list`, `info`, `list`, `retrieve`, `search`, `service copy`, `service create`, `service delete`, `service deploy`, `service get`, `service list`, `service update`, `stats`, `update` | [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) |
| `memory` | `add`, `delete`, `list`, `profile create`, `profile delete`, `profile get`, `profile list`, `profile show`, `profile update`, `search`, `update` | [memory.md](memory.md) |
| `model` | `list` | [model.md](model.md) |
| `permission` | `grant`, `list`, `revoke` | [permission.md](permission.md) |
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
+329 -88
View File
@@ -7,15 +7,19 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Authentication | Description |
| -------------------------- | -------------- | ------------------------------------------------- |
| `bl memory add` | API Key | Add memory from messages or custom content |
| `bl memory delete` | API Key | Delete a memory node |
| `bl memory list` | API Key | List memory nodes for a user |
| `bl memory profile create` | API Key | Create a user profile schema for memory profiling |
| `bl memory profile get` | API Key | Get user profile by schema ID and user ID |
| `bl memory search` | API Key | Search memory nodes by query or messages |
| `bl memory update` | API Key | Update a memory node content |
| Command | Authentication | Description |
| -------------------------- | -------------- | -------------------------------------------------------- |
| `bl memory add` | API Key | Add memory from messages or custom content |
| `bl memory delete` | API Key | Delete a memory node |
| `bl memory list` | API Key | List memory nodes for a user |
| `bl memory profile create` | API Key | Create a user profile schema for memory profiling |
| `bl memory profile delete` | API Key | Delete a profile schema |
| `bl memory profile get` | API Key | Get the extracted user profile for a schema |
| `bl memory profile list` | API Key | List profile schemas |
| `bl memory profile show` | API Key | Show a profile schema definition with attribute IDs |
| `bl memory profile update` | API Key | Update a profile schema name, description, or attributes |
| `bl memory search` | API Key | Search memory nodes by query or messages |
| `bl memory update` | API Key | Update a memory node content |
## Command details
@@ -30,20 +34,30 @@ 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 | Memory entity ID that owns the memory (required) |
| `--messages <json>` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] (max 50) |
| `--content <text>` | string | no | Custom content to memorize verbatim; takes precedence over --messages |
| `--profile-schema <id>` | string | no | Profile schema ID; without it no user profile is extracted |
| `--meta-data <json>` | string | no | Custom metadata JSON object: {"location_name":"Beijing"} |
| `--project-id <id>` | string | no | Memory fragment rule ID (default: the library's default rule) |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- --content and --messages are mutually exclusive: when --content is set, --messages is ignored by the server.
- The response lists the changed memory nodes; one call can add, update or delete several at once.
- Account-level rate limits: add 120 QPM, search 300 QPM, 3000 QPM across all memory APIs. On HTTP 429 back off and leave at least 1s between calls.
#### Examples
```bash
bl memory add --user-id user1 --content "The user likes Python programming"
bl memory add --user-id user1 --content "The user likes Python programming" --workspace-id ws_xxx
```
```bash
@@ -54,29 +68,49 @@ 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 "Attended WAIC" --meta-data '{"location":"Shanghai"}'
```
### `bl memory delete`
| Field | Value |
| ------------------ | ------------------------------------------------ |
| **Name** | `memory delete` |
| **Description** | Delete a memory node |
| **Authentication** | API Key |
| **Usage** | `bl memory delete --node-id <id> --user-id <id>` |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------ |
| **Name** | `memory delete` |
| **Description** | Delete a memory node |
| **Authentication** | API Key |
| **Usage** | `bl memory delete --node-id <id> --user-id <id> [flags]` |
| **Risk** | `high` |
| **Risk message** | This permanently deletes the specified memory node and cannot be undone. |
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | --------------------------------------- |
| `--node-id <id>` | string | yes | Memory node ID (required) |
| `--user-id <id>` | string | yes | User ID (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 | Memory entity ID that owns the memory (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--yes` | switch | no | Confirm this high-risk operation |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- Irreversible — the memory node is permanently removed. Run `memory list` first to confirm the node ID.
#### Examples
```bash
bl memory delete --node-id node_xxx --user-id user1
bl memory delete --node-id node_xxx --user-id user1 --workspace-id ws_xxx
```
```bash
# Only after explicit user confirmation:
bl memory delete --node-id node_xxx --user-id user1 --yes
```
### `bl memory list`
@@ -90,25 +124,35 @@ 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 | Memory entity ID that owns the memory (required) |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--project-id <id>` | string | no | Memory fragment rule ID (default: the library's default rule) |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
#### Examples
```bash
bl memory list --user-id user1
bl memory list --user-id user1 --workspace-id ws_xxx
```
```bash
bl memory list --user-id user1 --page-size 20 --page 2
```
```bash
bl memory list --user-id user1 --memory-library-id lib_xxx --output json
```
### `bl memory profile create`
| Field | Value |
@@ -120,42 +164,210 @@ bl memory list --user-id user1 --page-size 20 --page 2
#### Flags
| Flag | Type | Required | Description |
| ---------------------- | ------ | -------- | ----------------------------------------------------------- |
| `--name <name>` | string | yes | Schema name (required) |
| `--description <text>` | string | no | Schema description |
| `--attributes <json>` | string | yes | Attributes JSON array: [{"name":"age","description":"age"}] |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ---------------------------- | ------ | -------- | -------------------------------------------------------------------------------- |
| `--name <name>` | string | yes | Schema name (required) |
| `--description <text>` | string | no | Schema description |
| `--attributes <json>` | string | yes | Attributes JSON array: [{"name":"age","description":"age","default_value":"18"}] |
| `--plan-version <pro\|lite>` | string | no | Strategy version: pro (rerank on) or lite (rerank off); billed differently |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- Each attribute needs a name; description and default_value are optional. --plan-version picks the extraction tier (pro / lite) and is billed differently.
#### Examples
```bash
bl memory profile create --name "user_basic" --attributes '[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]'
bl memory profile create --name "user_basic" --attributes '[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]' --workspace-id ws_xxx
```
```bash
bl memory profile create --name "user_basic" --attributes '[{"name":"age"}]' --plan-version lite --memory-library-id lib_xxx
```
### `bl memory profile delete`
| Field | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `memory profile delete` |
| **Description** | Delete a profile schema |
| **Authentication** | API Key |
| **Usage** | `bl memory profile delete --schema-id <id> [flags]` |
| **Risk** | `high` |
| **Risk message** | This permanently deletes the profile schema and its attribute definitions. Profiles already extracted for this schema become unreachable. |
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | --------------------------------------------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--yes` | switch | no | Confirm this high-risk operation |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- Irreversible — the schema and its attribute definitions are permanently removed. Profiles already extracted for this schema become unreachable.
#### Examples
```bash
bl memory profile delete --schema-id schema_xxx --workspace-id ws_xxx
```
```bash
# Only after explicit user confirmation:
bl memory profile delete --schema-id schema_xxx --yes
```
### `bl memory profile get`
| Field | Value |
| ------------------ | ------------------------------------------------------- |
| **Name** | `memory profile get` |
| **Description** | Get user profile by schema ID and user ID |
| **Authentication** | API Key |
| **Usage** | `bl memory profile get --schema-id <id> --user-id <id>` |
| Field | Value |
| ------------------ | --------------------------------------------------------------- |
| **Name** | `memory profile get` |
| **Description** | Get the extracted user profile for a schema |
| **Authentication** | API Key |
| **Usage** | `bl memory profile get --schema-id <id> --user-id <id> [flags]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ---------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--user-id <id>` | string | yes | User ID (required) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | --------------------------------------------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--user-id <id>` | string | yes | Memory entity ID that owns the profile (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- Values are extracted only when `memory add --profile-schema` used the same schema ID; otherwise every attribute comes back empty. Use `memory profile show` for the schema definition itself.
#### Examples
```bash
bl memory profile get --schema-id schema_xxx --user-id user1
bl memory profile get --schema-id schema_xxx --user-id user1 --workspace-id ws_xxx
```
### `bl memory profile list`
| Field | Value |
| ------------------ | -------------------------------- |
| **Name** | `memory profile list` |
| **Description** | List profile schemas |
| **Authentication** | API Key |
| **Usage** | `bl memory profile list [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | --------------------------------------------------------------- |
| `--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 (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
#### Examples
```bash
bl memory profile list --workspace-id ws_xxx
```
```bash
bl memory profile list --page-size 20 --page 2
```
```bash
bl memory profile list --memory-library-id lib_xxx --output json
```
### `bl memory profile show`
| Field | Value |
| ------------------ | --------------------------------------------------- |
| **Name** | `memory profile show` |
| **Description** | Show a profile schema definition with attribute IDs |
| **Authentication** | API Key |
| **Usage** | `bl memory profile show --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 (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- The attribute_id values returned here are the handles `memory profile update --attributes-operations` needs for update / delete operations.
#### Examples
```bash
bl memory profile show --schema-id schema_xxx --workspace-id ws_xxx
```
```bash
bl memory profile show --schema-id schema_xxx --output json
```
### `bl memory profile update`
| Field | Value |
| ------------------ | -------------------------------------------------------- |
| **Name** | `memory profile update` |
| **Description** | Update a profile schema name, description, or attributes |
| **Authentication** | API Key |
| **Usage** | `bl memory profile update --schema-id <id> [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 |
| `--attributes-operations <json>` | string | no | Attribute operations JSON array: [{"op":"add","name":"plan"}] |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- Each operation needs "op": add (requires name), update / delete (require attribute_id). Run `memory profile show` first to get attribute IDs.
#### Examples
```bash
bl memory profile update --schema-id schema_xxx --name "user_basic_v2" --workspace-id ws_xxx
```
```bash
bl memory profile update --schema-id schema_xxx --attributes-operations '[{"op":"add","name":"plan","default_value":"free"},{"op":"delete","attribute_id":"attr_2"}]'
```
### `bl memory search`
@@ -169,48 +381,77 @@ 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 | Memory entity ID that owns the memory (required) |
| `--query <text>` | string | no | Search text; sent as a single user message |
| `--messages <json>` | string | no | Messages JSON array for context-based search; overrides --query |
| `--top-k <n>` | number | no | Max results, 1-100 (default: 10) |
| `--min-score <score>` | number | no | Minimum similarity score, 0-1 (default: 0.3) |
| `--enable-rerank <bool>` | boolean | no | Rerank results (default: false); ignored when --plan-version is set |
| `--enable-judge <bool>` | boolean | no | Run the intent judge callback (default: false) |
| `--enable-rewrite <bool>` | boolean | no | Rewrite the query before searching (default: false) |
| `--project-id <id>` | array | no | Memory fragment rule ID (repeatable for hybrid retrieval) |
| `--plan-version <pro\|lite>` | string | no | Strategy version: pro (rerank on) or lite (rerank off); billed differently |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- --plan-version overrides --enable-rerank and changes the price: pro reranks, lite does not.
- Account-level rate limits: add 120 QPM, search 300 QPM, 3000 QPM across all memory APIs. On HTTP 429 back off and leave at least 1s between calls.
#### Examples
```bash
bl memory search --user-id user1 --query "programming preferences"
bl memory search --user-id user1 --query "programming preferences" --workspace-id ws_xxx
```
```bash
bl memory search --user-id user1 --messages '[{"role":"user","content":"recommend a book"}]' --top-k 5
```
```bash
bl memory search --user-id user1 --query "reminders" --plan-version lite --min-score 0
```
### `bl memory update`
| Field | Value |
| ------------------ | ----------------------------------------------------------------- |
| **Name** | `memory update` |
| **Description** | Update a memory node content |
| **Authentication** | API Key |
| **Usage** | `bl memory update --node-id <id> --user-id <id> --content <text>` |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------- |
| **Name** | `memory update` |
| **Description** | Update a memory node content |
| **Authentication** | API Key |
| **Usage** | `bl memory update --node-id <id> --user-id <id> --content <text> [flags]` |
#### 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 | Memory entity ID that owns the memory (required) |
| `--content <text>` | string | yes | New content for the memory node, max 512 characters (required) |
| `--timestamp <seconds>` | number | no | Unix timestamp (seconds) of when the remembered event happened |
| `--meta-data <json>` | string | no | Custom metadata JSON object, merged incrementally: {"key":"value"} |
| `--memory-library-id <id>` | string | no | Memory library ID (default: the account's default library) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The memory API lives on a workspace-specific host, so --workspace-id is required; it can also come from BAILIAN_WORKSPACE_ID or the workspace_id config field.
- --content replaces the node content in full; --meta-data merges incrementally.
#### Examples
```bash
bl memory update --node-id node_xxx --user-id user1 --content "updated memory content"
bl memory update --node-id node_xxx --user-id user1 --content "updated memory content" --workspace-id ws_xxx
```
```bash
bl memory update --node-id node_xxx --user-id user1 --content "met at WAIC" --timestamp 1747278460 --meta-data '{"city":"Shanghai"}'
```