From 75a7100702d125041651feafeb38893461034935 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Thu, 10 Sep 2026 14:40:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(memory):=20=E5=AE=8C=E5=96=84=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E5=8F=8A=E7=94=BB=E5=83=8F=E7=9B=B8=E5=85=B3=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E6=94=AF=E6=8C=81=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CLI 文档中新增记忆 live 场景说明与变量需求 - 扩展 CLI 命令集,添加记忆画像 Profile 的增删改查功能 - 统一所有记忆相关命令加入 workspace 和 library 参数支持 - 记忆添加命令支持自定义 meta_data,完善输入校验和互斥说明 - 记忆删除、列表、获取用户画像等命令增加工作空间参数支持 - 画像模板相关命令新增分页参数及属性操作接口 - 重构知识命令共享模块,移除重复的 workspace 解析实现,统一导出 - 优化命令输出格式,文本模式下显示更多详细信息和异常提示 - 提供完整的示例参数,增强命令使用说明和提示文档 --- docs/agents/cli-e2e-tests.md | 3 +- packages/cli/src/commands.ts | 8 + .../commands/src/commands/knowledge/shared.ts | 33 +- packages/commands/src/commands/memory/add.ts | 112 +++-- .../commands/src/commands/memory/delete.ts | 74 ++-- packages/commands/src/commands/memory/list.ts | 94 ++-- .../src/commands/memory/profile-create.ts | 97 +++-- .../src/commands/memory/profile-delete.ts | 81 ++++ .../src/commands/memory/profile-get.ts | 87 ++-- .../src/commands/memory/profile-list.ts | 89 ++++ .../src/commands/memory/profile-show.ts | 90 ++++ .../src/commands/memory/profile-update.ts | 140 ++++++ .../commands/src/commands/memory/search.ts | 134 ++++-- .../commands/src/commands/memory/shared.ts | 81 ++++ .../commands/src/commands/memory/update.ts | 129 ++++-- .../commands/src/commands/shared/workspace.ts | 33 ++ packages/commands/src/index.ts | 4 + packages/commands/tests/e2e/helpers.ts | 1 + .../commands/tests/e2e/memory.e2e.test.ts | 215 ---------- .../tests/e2e/memory/memory-add.e2e.test.ts | 246 +++++++++++ .../e2e/memory/memory-delete.e2e.test.ts | 243 +++++++++++ .../tests/e2e/memory/memory-list.e2e.test.ts | 167 ++++++++ .../memory/memory-profile-create.e2e.test.ts | 148 +++++++ .../memory/memory-profile-delete.e2e.test.ts | 278 ++++++++++++ .../e2e/memory/memory-profile-get.e2e.test.ts | 110 +++++ .../memory/memory-profile-list.e2e.test.ts | 133 ++++++ .../memory/memory-profile-show.e2e.test.ts | 100 +++++ .../memory/memory-profile-update.e2e.test.ts | 214 ++++++++++ .../e2e/memory/memory-search.e2e.test.ts | 284 ++++++++++++ .../e2e/memory/memory-update.e2e.test.ts | 205 +++++++++ packages/commands/tests/e2e/memory/shared.ts | 126 ++++++ packages/commands/tests/e2e/topic-routes.ts | 45 +- packages/core/src/client/endpoints.ts | 36 +- packages/core/src/client/index.ts | 2 + packages/core/src/types/api.ts | 99 ++++- packages/core/src/types/index.ts | 9 + packages/e2e/src/gating.ts | 15 + skills/bailian-cli/reference/index.md | 8 +- skills/bailian-cli/reference/memory.md | 404 ++++++++++++++---- 39 files changed, 3793 insertions(+), 584 deletions(-) create mode 100644 packages/commands/src/commands/memory/profile-delete.ts create mode 100644 packages/commands/src/commands/memory/profile-list.ts create mode 100644 packages/commands/src/commands/memory/profile-show.ts create mode 100644 packages/commands/src/commands/memory/profile-update.ts create mode 100644 packages/commands/src/commands/memory/shared.ts create mode 100644 packages/commands/src/commands/shared/workspace.ts delete mode 100644 packages/commands/tests/e2e/memory.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-add.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-list.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-search.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/memory-update.e2e.test.ts create mode 100644 packages/commands/tests/e2e/memory/shared.ts diff --git a/docs/agents/cli-e2e-tests.md b/docs/agents/cli-e2e-tests.md index ecd5c10a..90303a8f 100644 --- a/docs/agents/cli-e2e-tests.md +++ b/docs/agents/cli-e2e-tests.md @@ -71,12 +71,13 @@ describe.skipIf()("e2e: (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`;记忆服务需账号单独开通) | ## 用例类型 diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index c1ee7fb2..aa51f642 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -29,6 +29,10 @@ import { memoryUpdate, memoryDelete, memoryProfileCreate, + memoryProfileList, + memoryProfileShow, + memoryProfileUpdate, + memoryProfileDelete, memoryProfileGet, knowledgeRetrieve, knowledgeSearch, @@ -243,6 +247,10 @@ export const commands: Record = { "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, diff --git a/packages/commands/src/commands/knowledge/shared.ts b/packages/commands/src/commands/knowledge/shared.ts index f6049adc..a1c4e7fe 100644 --- a/packages/commands/src/commands/knowledge/shared.ts +++ b/packages/commands/src/commands/knowledge/shared.ts @@ -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: "", - 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 `, - ); - } - 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; diff --git a/packages/commands/src/commands/memory/add.ts b/packages/commands/src/commands/memory/add.ts index f55b47fd..abdcc24b 100644 --- a/packages/commands/src/commands/memory/add.ts +++ b/packages/commands/src/commands/memory/add.ts @@ -2,53 +2,80 @@ 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_WORKSPACE_NOTE, + PROJECT_ID_FLAG, + WORKSPACE_FLAG, + parseJsonArrayFlag, + parseJsonObjectFlag, + resolveWorkspaceId, +} from "./shared.ts"; const ADD_FLAGS = { userId: { type: "string", valueHint: "", - 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: "", 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: "", - 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: "", 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: "", + valueHint: "", 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; +/** Max messages accepted per AddMemory call (a Q&A pair counts as 2). */ +const MAX_MESSAGES = 50; +/** Max characters accepted for custom_content. */ +const MAX_CONTENT_LENGTH = 512; +/** Max characters accepted for user_id. */ +const MAX_USER_ID_LENGTH = 64; + export default defineCommand({ description: { "en-US": "Add memory from messages or custom content", @@ -57,10 +84,24 @@ export default defineCommand({ auth: "apiKey", usageArgs: "--user-id [--messages ] [--content ] [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": "返回结果是变更的记忆片段列表;一次调用可能同时新增、更新或删除多条。", + }, + ], 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."; + if (flags.userId.length > MAX_USER_ID_LENGTH) + return `--user-id must be at most ${MAX_USER_ID_LENGTH} characters.`; + if (flags.content && flags.content.length > MAX_CONTENT_LENGTH) + return `--content must be at most ${MAX_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("--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({ - 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); } diff --git a/packages/commands/src/commands/memory/delete.ts b/packages/commands/src/commands/memory/delete.ts index d3febaa9..9724cc06 100644 --- a/packages/commands/src/commands/memory/delete.ts +++ b/packages/commands/src/commands/memory/delete.ts @@ -1,50 +1,62 @@ -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, + resolveWorkspaceId, +} from "./shared.ts"; + +const DELETE_FLAGS = { + nodeId: { + type: "string", + valueHint: "", + description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID(必填)" }, + required: true, + }, + userId: { + type: "string", + valueHint: "", + 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 --user-id ", - flags: { - nodeId: { - type: "string", - valueHint: "", - description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID(必填)" }, - required: true, - }, - userId: { - type: "string", - valueHint: "", - description: { "en-US": "User ID (required)", "zh-CN": "用户 ID(必填)" }, - required: true, - }, - memoryLibraryId: { - type: "string", - valueHint: "", - description: { - "en-US": "Memory library ID (non-default library)", - "zh-CN": "记忆库 ID(非默认记忆库)", - }, - }, - }, - exampleArgs: ["--node-id node_xxx --user-id user1"], + usageArgs: "--node-id --user-id [flags]", + flags: DELETE_FLAGS, + notes: [MEMORY_WORKSPACE_NOTE], + exampleArgs: ["--node-id node_xxx --user-id user1 --workspace-id ws_xxx"], 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", }); diff --git a/packages/commands/src/commands/memory/list.ts b/packages/commands/src/commands/memory/list.ts index 67b0bb63..200c242a 100644 --- a/packages/commands/src/commands/memory/list.ts +++ b/packages/commands/src/commands/memory/list.ts @@ -1,59 +1,84 @@ 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, + resolveWorkspaceId, +} from "./shared.ts"; + +const LIST_FLAGS = { + userId: { + type: "string", + valueHint: "", + description: { + "en-US": "Memory entity ID that owns the memory (required)", + "zh-CN": "记忆实体 ID,标识记忆归属对象(必填)", + }, + required: true, + }, + pageSize: { + type: "number", + valueHint: "", + description: { "en-US": "Results per page (default: 10)", "zh-CN": "每页结果数(默认:10)" }, + }, + page: { + type: "number", + valueHint: "", + 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 [flags]", - flags: { - userId: { - type: "string", - valueHint: "", - description: { "en-US": "User ID (required)", "zh-CN": "用户 ID(必填)" }, - required: true, - }, - pageSize: { - type: "number", - valueHint: "", - description: { "en-US": "Results per page (default: 10)", "zh-CN": "每页结果数(默认:10)" }, - }, - page: { - type: "number", - valueHint: "", - description: { "en-US": "Page number (default: 1)", "zh-CN": "页码(默认:1)" }, - }, - memoryLibraryId: { - type: "string", - valueHint: "", - 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) => { + 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({ - path, + path: url, method: "GET", }); @@ -63,6 +88,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}`); diff --git a/packages/commands/src/commands/memory/profile-create.ts b/packages/commands/src/commands/memory/profile-create.ts index 032b8aed..241514e9 100644 --- a/packages/commands/src/commands/memory/profile-create.ts +++ b/packages/commands/src/commands/memory/profile-create.ts @@ -1,12 +1,49 @@ 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, + parseJsonArrayFlag, + resolveWorkspaceId, +} from "./shared.ts"; + +const PROFILE_CREATE_FLAGS = { + name: { + type: "string", + valueHint: "", + description: { "en-US": "Schema name (required)", "zh-CN": "Schema 名称(必填)" }, + required: true, + }, + description: { + type: "string", + valueHint: "", + description: { "en-US": "Schema description", "zh-CN": "Schema 描述" }, + }, + attributes: { + type: "string", + valueHint: "", + 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 +52,50 @@ export default defineCommand({ }, auth: "apiKey", usageArgs: "--name --attributes [flags]", - flags: { - name: { - type: "string", - valueHint: "", - description: { "en-US": "Schema name (required)", "zh-CN": "Schema 名称(必填)" }, - required: true, - }, - description: { - type: "string", - valueHint: "", - description: { "en-US": "Schema description", "zh-CN": "Schema 描述" }, - }, - attributes: { - type: "string", - valueHint: "", - description: { - "en-US": 'Attributes JSON array: [{"name":"age","description":"age"}]', - "zh-CN": '属性 JSON 数组:[{"name":"age","description":"age"}]', - }, - 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": + "每个属性必须有 name,description 与 default_value 可选。--plan-version 决定抽取策略档位(pro / lite),计费单价不同。", }, - }, + ], 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', }, ], 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("--attributes", flags.attributes); - 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({ - path: profileSchemaPath(), + path: url, method: "POST", body, }); diff --git a/packages/commands/src/commands/memory/profile-delete.ts b/packages/commands/src/commands/memory/profile-delete.ts new file mode 100644 index 00000000..a44ae772 --- /dev/null +++ b/packages/commands/src/commands/memory/profile-delete.ts @@ -0,0 +1,81 @@ +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, + resolveWorkspaceId, +} from "./shared.ts"; + +const PROFILE_DELETE_FLAGS = { + schemaId: { + type: "string", + valueHint: "", + 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 [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"], + 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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/profile-get.ts b/packages/commands/src/commands/memory/profile-get.ts index 1edf3b4d..3fb5d094 100644 --- a/packages/commands/src/commands/memory/profile-get.ts +++ b/packages/commands/src/commands/memory/profile-get.ts @@ -1,62 +1,87 @@ 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, + resolveWorkspaceId, +} from "./shared.ts"; + +const PROFILE_GET_FLAGS = { + schemaId: { + type: "string", + valueHint: "", + description: { + "en-US": "Profile schema ID (required)", + "zh-CN": "Profile Schema ID(必填)", + }, + required: true, + }, + userId: { + type: "string", + valueHint: "", + 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 --user-id ", - flags: { - schemaId: { - type: "string", - valueHint: "", - description: { - "en-US": "Profile schema ID (required)", - "zh-CN": "Profile Schema ID(必填)", - }, - required: true, + usageArgs: "--schema-id --user-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: "", - 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"], 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({ - 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); diff --git a/packages/commands/src/commands/memory/profile-list.ts b/packages/commands/src/commands/memory/profile-list.ts new file mode 100644 index 00000000..47647c70 --- /dev/null +++ b/packages/commands/src/commands/memory/profile-list.ts @@ -0,0 +1,89 @@ +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, + resolveWorkspaceId, +} from "./shared.ts"; + +const PROFILE_LIST_FLAGS = { + pageSize: { + type: "number", + valueHint: "", + description: { "en-US": "Results per page (default: 10)", "zh-CN": "每页结果数(默认:10)" }, + }, + page: { + type: "number", + valueHint: "", + 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) => { + 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({ + 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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/profile-show.ts b/packages/commands/src/commands/memory/profile-show.ts new file mode 100644 index 00000000..0b5a1f37 --- /dev/null +++ b/packages/commands/src/commands/memory/profile-show.ts @@ -0,0 +1,90 @@ +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, + resolveWorkspaceId, +} from "./shared.ts"; + +const PROFILE_SHOW_FLAGS = { + schemaId: { + type: "string", + valueHint: "", + 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 [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", + ], + 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({ + 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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/profile-update.ts b/packages/commands/src/commands/memory/profile-update.ts new file mode 100644 index 00000000..067d0261 --- /dev/null +++ b/packages/commands/src/commands/memory/profile-update.ts @@ -0,0 +1,140 @@ +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, + parseJsonArrayFlag, + resolveWorkspaceId, +} from "./shared.ts"; + +const PROFILE_UPDATE_FLAGS = { + schemaId: { + type: "string", + valueHint: "", + description: { + "en-US": "Profile schema ID (required)", + "zh-CN": "Profile Schema ID(必填)", + }, + required: true, + }, + name: { + type: "string", + valueHint: "", + description: { "en-US": "New schema name", "zh-CN": "新的 Schema 名称" }, + }, + description: { + type: "string", + valueHint: "", + description: { "en-US": "New schema description", "zh-CN": "新的 Schema 描述" }, + }, + attributesOperations: { + type: "string", + valueHint: "", + 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}"`); + } + }); +} + +export default defineCommand({ + description: { + "en-US": "Update a profile schema name, description, or attributes", + "zh-CN": "更新画像模板的名称、描述或属性", + }, + auth: "apiKey", + usageArgs: "--schema-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 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( + "--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); + } + }, +}); diff --git a/packages/commands/src/commands/memory/search.ts b/packages/commands/src/commands/memory/search.ts index ca910980..ce0df6e6 100644 --- a/packages/commands/src/commands/memory/search.ts +++ b/packages/commands/src/commands/memory/search.ts @@ -1,51 +1,107 @@ 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_WORKSPACE_NOTE, + PLAN_VERSION_FLAG, + WORKSPACE_FLAG, + parseJsonArrayFlag, + resolveWorkspaceId, +} from "./shared.ts"; const SEARCH_FLAGS = { userId: { type: "string", valueHint: "", - 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: "", - 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: "", 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: "", 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", + minScore: { + type: "number", + valueHint: "", + description: { + "en-US": "Minimum similarity score, 0-1 (default: 0.3)", + "zh-CN": "最小相似度分数阈值,0~1(默认:0.3)", + }, + }, + enableRerank: { + type: "boolean", + valueHint: "", + description: { + "en-US": "Rerank results (default: false); ignored when --plan-version is set", + "zh-CN": "是否重排序搜索结果(默认:false);传了 --plan-version 时本参数被忽略", + }, + }, + enableJudge: { + type: "boolean", + valueHint: "", + description: { + "en-US": "Run the intent judge callback (default: false)", + "zh-CN": "是否开启意图判别回调(默认:false)", + }, + }, + enableRewrite: { + type: "boolean", + valueHint: "", + description: { + "en-US": "Rewrite the query before searching (default: false)", + "zh-CN": "是否开启 query 重写(默认:false)", + }, + }, + projectId: { + type: "array", valueHint: "", - description: { "en-US": "Memory library ID", "zh-CN": "记忆库 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; +const MAX_TOP_K = 100; + export default defineCommand({ description: { "en-US": "Search memory nodes by query or messages", @@ -54,52 +110,66 @@ export default defineCommand({ auth: "apiKey", usageArgs: "--user-id [--query ] [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 不开启。", + }, + ], 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."; + 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 }; - if (flags.query) body.query = flags.query; + // The API only accepts `messages`; a bare --query is wrapped as one user turn. + const messages = flags.messages + ? parseJsonArrayFlag("--messages", flags.messages) + : [{ role: "user" as const, content: 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({ - path: memorySearchPath(), + path: url, method: "POST", body, }); diff --git a/packages/commands/src/commands/memory/shared.ts b/packages/commands/src/commands/memory/shared.ts new file mode 100644 index 00000000..0ace6f0e --- /dev/null +++ b/packages/commands/src/commands/memory/shared.ts @@ -0,0 +1,81 @@ +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: "", + 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: "", + 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: "", + description: { + "en-US": "Memory fragment rule ID (default: the library's default rule)", + "zh-CN": "记忆片段规则 ID(默认:记忆库的默认规则)", + }, + }, +} satisfies FlagsDef; + +/** Parse a JSON object flag (`--meta-data`), rejecting arrays and primitives. */ +export function parseJsonObjectFlag(flagName: string, raw: string): Record { + 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; +} + +/** Parse a JSON array flag (`--messages`, `--attributes`), rejecting objects and primitives. */ +export function parseJsonArrayFlag(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[]; +} diff --git a/packages/commands/src/commands/memory/update.ts b/packages/commands/src/commands/memory/update.ts index a6de2a19..9dc906a8 100644 --- a/packages/commands/src/commands/memory/update.ts +++ b/packages/commands/src/commands/memory/update.ts @@ -1,76 +1,123 @@ 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, + WORKSPACE_FLAG, + parseJsonObjectFlag, + resolveWorkspaceId, +} from "./shared.ts"; -export default defineCommand({ - description: { "en-US": "Update a memory node content", "zh-CN": "更新记忆节点内容" }, - auth: "apiKey", - usageArgs: "--node-id --user-id --content ", - flags: { - nodeId: { - type: "string", - valueHint: "", - description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID(必填)" }, - required: true, +const UPDATE_FLAGS = { + nodeId: { + type: "string", + valueHint: "", + description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID(必填)" }, + required: true, + }, + userId: { + type: "string", + valueHint: "", + description: { + "en-US": "Memory entity ID that owns the memory (required)", + "zh-CN": "记忆实体 ID,标识记忆归属对象(必填)", }, - userId: { - type: "string", - valueHint: "", - description: { "en-US": "User ID (required)", "zh-CN": "用户 ID(必填)" }, - required: true, + required: true, + }, + content: { + type: "string", + valueHint: "", + description: { + "en-US": "New content for the memory node, max 512 characters (required)", + "zh-CN": "记忆节点的新内容,最多 512 个字符(必填)", }, - content: { - type: "string", - valueHint: "", - description: { - "en-US": "New content for the memory node (required)", - "zh-CN": "记忆节点的新内容(必填)", - }, - required: true, + required: true, + }, + timestamp: { + type: "number", + valueHint: "", + description: { + "en-US": "Unix timestamp (seconds) of when the remembered event happened", + "zh-CN": "记忆片段对应事件发生时的秒级 Unix 时间戳", }, - memoryLibraryId: { - type: "string", - valueHint: "", - description: { - "en-US": "Memory library ID (non-default library)", - "zh-CN": "记忆库 ID(非默认记忆库)", - }, + }, + metaData: { + type: "string", + valueHint: "", + description: { + "en-US": 'Custom metadata JSON object, merged incrementally: {"key":"value"}', + "zh-CN": '用户自定义信息 JSON 对象,增量合并:{"key":"value"}', }, }, + ...MEMORY_LIBRARY_FLAG, + ...WORKSPACE_FLAG, +} satisfies FlagsDef; + +/** Max characters accepted for custom_content. */ +const MAX_CONTENT_LENGTH = 512; + +export default defineCommand({ + description: { "en-US": "Update a memory node content", "zh-CN": "更新记忆节点内容" }, + auth: "apiKey", + usageArgs: "--node-id --user-id --content [flags]", + flags: UPDATE_FLAGS, + notes: [ + MEMORY_WORKSPACE_NOTE, + { + "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"', - "zh-CN": '--node-id node_xxx --user-id user1 --content "更新后的记忆内容"', + "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) => { + if (flags.content.length > MAX_CONTENT_LENGTH) + return `--content must be at most ${MAX_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, }); diff --git a/packages/commands/src/commands/shared/workspace.ts b/packages/commands/src/commands/shared/workspace.ts new file mode 100644 index 00000000..fbeaa11a --- /dev/null +++ b/packages/commands/src/commands/shared/workspace.ts @@ -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: "", + 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 `, + ); + } + return workspaceId; +} diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 028f0ccb..2448ba65 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -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"; diff --git a/packages/commands/tests/e2e/helpers.ts b/packages/commands/tests/e2e/helpers.ts index eef65fc8..e8566c48 100644 --- a/packages/commands/tests/e2e/helpers.ts +++ b/packages/commands/tests/e2e/helpers.ts @@ -34,6 +34,7 @@ export { isDashScopeE2EReady, isImageKbE2EReady, isKbAdminE2EReady, + isMemoryE2EReady, isMultimodalChatE2EReady, isMultimodalSearchE2EReady, isOpenApiE2EReady, diff --git a/packages/commands/tests/e2e/memory.e2e.test.ts b/packages/commands/tests/e2e/memory.e2e.test.ts deleted file mode 100644 index 869b521b..00000000 --- a/packages/commands/tests/e2e/memory.e2e.test.ts +++ /dev/null @@ -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(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(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(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); - }, -); diff --git a/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts new file mode 100644 index 00000000..b74a7c71 --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts @@ -0,0 +1,246 @@ +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("--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(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(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(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(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. diff --git a/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts new file mode 100644 index 00000000..fcef5b3f --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts @@ -0,0 +1,243 @@ +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(/--memory-library-id/i); + expect(stderr).toMatch(/--workspace-id/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("--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", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(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(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(), + "--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(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(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(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(afterUpdateRes.stdout); + const updatedNode = afterUpdate.memory_nodes?.find( + (node) => node.memory_node_id === addedNodeId, + ); + expect(updatedNode?.content, afterUpdateRes.stdout).toBe(contentAfter); + + const deleteRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [ + "memory", + "delete", + ...memoryScopeCliArgs(), + "--node-id", + addedNodeId, + "--user-id", + userId, + "--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(afterDeleteRes.stdout); + expect( + afterDelete.memory_nodes?.some((node) => node.memory_node_id === addedNodeId) ?? false, + afterDeleteRes.stdout, + ).toBe(false); + }, 240_000); +}); diff --git a/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts new file mode 100644 index 00000000..9b4867a5 --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts @@ -0,0 +1,167 @@ +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("--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(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(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(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); + }); +}); diff --git a/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts new file mode 100644 index 00000000..79cabddd --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts @@ -0,0 +1,148 @@ +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 非法 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(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(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. diff --git a/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts new file mode 100644 index 00000000..2393c55e --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts @@ -0,0 +1,278 @@ +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("--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(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(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(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(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(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(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(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(afterDeleteRes.stdout); + expect( + afterDelete.profile_schemas?.some((schema) => schema.profile_schema_id === schemaId) ?? false, + afterDeleteRes.stdout, + ).toBe(false); + }, 240_000); +}); diff --git a/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts new file mode 100644 index 00000000..c10d304d --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts @@ -0,0 +1,110 @@ +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("--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(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(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); + }); +}); diff --git a/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts new file mode 100644 index 00000000..a189ab31 --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts @@ -0,0 +1,133 @@ +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("--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(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(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(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); + }); +}); diff --git a/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts new file mode 100644 index 00000000..c146942e --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts @@ -0,0 +1,100 @@ +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("--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(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(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(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); + }); +}); diff --git a/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts new file mode 100644 index 00000000..c9be25f1 --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts @@ -0,0 +1,214 @@ +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("--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(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(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(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. diff --git a/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts new file mode 100644 index 00000000..bfbfe5e8 --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts @@ -0,0 +1,284 @@ +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("--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(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(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(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(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(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); + }); +}); diff --git a/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts new file mode 100644 index 00000000..601a523b --- /dev/null +++ b/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts @@ -0,0 +1,205 @@ +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("--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(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(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(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. diff --git a/packages/commands/tests/e2e/memory/shared.ts b/packages/commands/tests/e2e/memory/shared.ts new file mode 100644 index 00000000..f6ac9c9d --- /dev/null +++ b/packages/commands/tests/e2e/memory/shared.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; + 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; + 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 }>; + }; +} diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 957a0b8f..5b8e7c09 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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", diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 8717c942..cbb145d0 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -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"; } @@ -89,13 +109,17 @@ export function speechRecognizePath(): string { return "/api/v1/services/audio/asr/transcription"; } -// ---- 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) ---- @@ -106,13 +130,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) ---- @@ -223,7 +247,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 = { diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 0a387f1a..8dffce1d 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -9,12 +9,14 @@ export { knowledgeRetrievePath, knowledgeSearchEndpoint, memoryAddPath, + memoryEndpoint, memoryListPath, memoryNodePath, memorySearchPath, mcpWebSearchPath, modelsLimitsPath, modelsPermissionsPath, + profileSchemaItemPath, profileSchemaPath, ragEndpoint, RAG_PATHS, diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index c6b05314..ea939b4d 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -346,25 +346,48 @@ export interface MemoryMessage { content: string; } +/** 记忆抽取策略版本:pro 开启 Rerank,lite 关闭(单价不同)。 */ +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; +} + +/** 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 +395,10 @@ export interface MemoryNode { content: string; user_id?: string; meta_data?: Record; - created_at?: string; - updated_at?: string; + /** 秒级 Unix 时间戳 */ + created_at?: number; + /** 秒级 Unix 时间戳 */ + updated_at?: number; } export interface MemorySearchResponse { @@ -394,20 +419,26 @@ export interface MemoryNodeUpdateRequest { custom_content: string; /** 非默认记忆库时必填(与控制台记忆库 ID 一致) */ memory_library_id?: string; + /** 记忆片段对应事件发生时的秒级 Unix 时间戳(默认当前时间) */ + timestamp?: number; + /** 用户自定义信息(增量更新) */ + meta_data?: Record; } // ---- Memory Profile (DashScope v2) ---- export interface ProfileAttribute { name: string; - description: string; - 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 +446,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[]; }; } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index cf240e3f..8c608be6 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -70,17 +70,26 @@ export type { KnowledgeSearchResponse, MemoryAddRequest, MemoryAddResponse, + MemoryChangedNode, 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"; diff --git a/packages/e2e/src/gating.ts b/packages/e2e/src/gating.ts index c70dadce..47155105 100644 --- a/packages/e2e/src/gating.ts +++ b/packages/e2e/src/gating.ts @@ -75,6 +75,21 @@ export function isConnectorE2EReady(): boolean { return isKbAdminE2EReady() && process.env.BAILIAN_E2E_CONNECTOR === "1"; } +/** + * 记忆库 fixture(memory / 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) ---- diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 1a0c94b4..fedb4984 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -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) | diff --git a/skills/bailian-cli/reference/memory.md b/skills/bailian-cli/reference/memory.md index b7c19c74..d7536705 100644 --- a/skills/bailian-cli/reference/memory.md +++ b/skills/bailian-cli/reference/memory.md @@ -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,29 @@ Index: [index.md](index.md) #### Flags -| Flag | Type | Required | Description | -| -------------------------- | ------ | -------- | ---------------------------------------------------------- | -| `--user-id ` | string | yes | User ID (required) | -| `--messages ` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] | -| `--content ` | string | no | Custom content text to memorize | -| `--profile-schema ` | string | no | Profile schema ID for user profiling | -| `--memory-library-id ` | string | no | Memory library ID (isolate memory space) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------------------------------------- | +| `--user-id ` | string | yes | Memory entity ID that owns the memory (required) | +| `--messages ` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] (max 50) | +| `--content ` | string | no | Custom content to memorize verbatim; takes precedence over --messages | +| `--profile-schema ` | string | no | Profile schema ID; without it no user profile is extracted | +| `--meta-data ` | string | no | Custom metadata JSON object: {"location_name":"Beijing"} | +| `--project-id ` | string | no | Memory fragment rule ID (default: the library's default rule) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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. #### 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 +67,38 @@ 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 --user-id ` | +| Field | Value | +| ------------------ | -------------------------------------------------------- | +| **Name** | `memory delete` | +| **Description** | Delete a memory node | +| **Authentication** | API Key | +| **Usage** | `bl memory delete --node-id --user-id [flags]` | #### Flags -| Flag | Type | Required | Description | -| -------------------------- | ------ | -------- | --------------------------------------- | -| `--node-id ` | string | yes | Memory node ID (required) | -| `--user-id ` | string | yes | User ID (required) | -| `--memory-library-id ` | string | no | Memory library ID (non-default library) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--node-id ` | string | yes | Memory node ID (required) | +| `--user-id ` | string | yes | Memory entity ID that owns the memory (required) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 delete --node-id node_xxx --user-id user1 +bl memory delete --node-id node_xxx --user-id user1 --workspace-id ws_xxx ``` ### `bl memory list` @@ -90,25 +112,35 @@ bl memory delete --node-id node_xxx --user-id user1 #### Flags -| Flag | Type | Required | Description | -| -------------------------- | ------ | -------- | ------------------------------ | -| `--user-id ` | string | yes | User ID (required) | -| `--page-size ` | number | no | Results per page (default: 10) | -| `--page ` | number | no | Page number (default: 1) | -| `--memory-library-id ` | string | no | Memory library ID | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--user-id ` | string | yes | Memory entity ID that owns the memory (required) | +| `--page-size ` | number | no | Results per page (default: 10) | +| `--page ` | number | no | Page number (default: 1) | +| `--project-id ` | string | no | Memory fragment rule ID (default: the library's default rule) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 +152,210 @@ bl memory list --user-id user1 --page-size 20 --page 2 #### Flags -| Flag | Type | Required | Description | -| ---------------------- | ------ | -------- | ----------------------------------------------------------- | -| `--name ` | string | yes | Schema name (required) | -| `--description ` | string | no | Schema description | -| `--attributes ` | string | yes | Attributes JSON array: [{"name":"age","description":"age"}] | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ---------------------------- | ------ | -------- | -------------------------------------------------------------------------------- | +| `--name ` | string | yes | Schema name (required) | +| `--description ` | string | no | Schema description | +| `--attributes ` | string | yes | Attributes JSON array: [{"name":"age","description":"age","default_value":"18"}] | +| `--plan-version ` | string | no | Strategy version: pro (rerank on) or lite (rerank off); billed differently | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 [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 ` | string | yes | Profile schema ID (required) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key ` | string | no | API key | +| `--base-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 --user-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 --user-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--schema-id ` | string | yes | Profile schema ID (required) | +| `--user-id ` | string | yes | Memory entity ID that owns the profile (required) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 --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 | -| ------------------ | ------ | -------- | ---------------------------- | -| `--schema-id ` | string | yes | Profile schema ID (required) | -| `--user-id ` | string | yes | User ID (required) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--page-size ` | number | no | Results per page (default: 10) | +| `--page ` | number | no | Page number (default: 1) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 get --schema-id schema_xxx --user-id user1 +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 [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--schema-id ` | string | yes | Profile schema ID (required) | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--schema-id ` | string | yes | Profile schema ID (required) | +| `--name ` | string | no | New schema name | +| `--description ` | string | no | New schema description | +| `--attributes-operations ` | string | no | Attribute operations JSON array: [{"op":"add","name":"plan"}] | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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 +369,76 @@ bl memory profile get --schema-id schema_xxx --user-id user1 #### Flags -| Flag | Type | Required | Description | -| -------------------------- | ------ | -------- | -------------------------------------------- | -| `--user-id ` | string | yes | User ID (required) | -| `--query ` | string | no | Search query text | -| `--messages ` | string | no | Messages JSON array for context-based search | -| `--top-k ` | number | no | Number of results to return (default: 10) | -| `--memory-library-id ` | string | no | Memory library ID | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ---------------------------- | ------- | -------- | -------------------------------------------------------------------------- | +| `--user-id ` | string | yes | Memory entity ID that owns the memory (required) | +| `--query ` | string | no | Search text; sent as a single user message | +| `--messages ` | string | no | Messages JSON array for context-based search; overrides --query | +| `--top-k ` | number | no | Max results, 1-100 (default: 10) | +| `--min-score ` | number | no | Minimum similarity score, 0-1 (default: 0.3) | +| `--enable-rerank ` | boolean | no | Rerank results (default: false); ignored when --plan-version is set | +| `--enable-judge ` | boolean | no | Run the intent judge callback (default: false) | +| `--enable-rewrite ` | boolean | no | Rewrite the query before searching (default: false) | +| `--project-id ` | array | no | Memory fragment rule ID (repeatable for hybrid retrieval) | +| `--plan-version ` | string | no | Strategy version: pro (rerank on) or lite (rerank off); billed differently | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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. #### 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 --user-id --content ` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------- | +| **Name** | `memory update` | +| **Description** | Update a memory node content | +| **Authentication** | API Key | +| **Usage** | `bl memory update --node-id --user-id --content [flags]` | #### Flags -| Flag | Type | Required | Description | -| -------------------------- | ------ | -------- | ------------------------------------------ | -| `--node-id ` | string | yes | Memory node ID (required) | -| `--user-id ` | string | yes | User ID (required) | -| `--content ` | string | yes | New content for the memory node (required) | -| `--memory-library-id ` | string | no | Memory library ID (non-default library) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | ------------------------------------------------------------------ | +| `--node-id ` | string | yes | Memory node ID (required) | +| `--user-id ` | string | yes | Memory entity ID that owns the memory (required) | +| `--content ` | string | yes | New content for the memory node, max 512 characters (required) | +| `--timestamp ` | number | no | Unix timestamp (seconds) of when the remembered event happened | +| `--meta-data ` | string | no | Custom metadata JSON object, merged incrementally: {"key":"value"} | +| `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-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"}' ``` From d9134d6f0a7663995ef9f1f951ce20b1d782eb68 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Thu, 10 Sep 2026 17:31:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(memory):=20=E5=A2=9E=E5=8A=A0=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=E6=A0=A1=E9=AA=8C=E5=8F=8A=E9=99=90=E6=B5=81=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E5=B9=B6=E5=BC=BA=E5=8C=96=E5=AE=89=E5=85=A8=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在多条命令中统一添加对 userId 和 memoryLibraryId 长度的校验 - 新增 profile schema 名称、描述及属性字段长度断言 - 为 delete 内存节点命令添加高风险删除确认机制及 --yes 标志 - 增加账号级别限流说明,提示遇 HTTP 429 需降低请求频率 - 统一移除分散的长度常量,集中定义并使用共享常量 - 相关 E2E 测试覆盖长度限制和高风险操作确认情况 - 支持内存消息内容多样化,新增 MemoryContentPart 类型定义 - 内存文档中新增限流描述及删除命令风险提示,强调确认操作要求 --- packages/commands/src/commands/memory/add.ts | 16 ++-- .../commands/src/commands/memory/delete.ts | 23 ++++- packages/commands/src/commands/memory/list.ts | 3 + .../src/commands/memory/profile-create.ts | 14 +++ .../src/commands/memory/profile-delete.ts | 2 + .../src/commands/memory/profile-get.ts | 2 + .../src/commands/memory/profile-list.ts | 3 + .../src/commands/memory/profile-show.ts | 2 + .../src/commands/memory/profile-update.ts | 6 +- .../commands/src/commands/memory/search.ts | 5 + .../commands/src/commands/memory/shared.ts | 94 +++++++++++++++++++ .../commands/src/commands/memory/update.ts | 11 ++- .../tests/e2e/memory/memory-add.e2e.test.ts | 16 ++++ .../e2e/memory/memory-delete.e2e.test.ts | 40 ++++++++ .../tests/e2e/memory/memory-list.e2e.test.ts | 14 +++ .../memory/memory-profile-create.e2e.test.ts | 63 +++++++++++++ .../memory/memory-profile-delete.e2e.test.ts | 15 +++ .../e2e/memory/memory-profile-get.e2e.test.ts | 17 ++++ .../memory/memory-profile-list.e2e.test.ts | 13 +++ .../memory/memory-profile-show.e2e.test.ts | 15 +++ .../memory/memory-profile-update.e2e.test.ts | 30 ++++++ .../e2e/memory/memory-search.e2e.test.ts | 16 ++++ .../e2e/memory/memory-update.e2e.test.ts | 18 ++++ packages/core/src/types/api.ts | 11 ++- packages/core/src/types/index.ts | 1 + skills/bailian-cli/reference/memory.md | 25 +++-- 26 files changed, 452 insertions(+), 23 deletions(-) diff --git a/packages/commands/src/commands/memory/add.ts b/packages/commands/src/commands/memory/add.ts index abdcc24b..0fae118d 100644 --- a/packages/commands/src/commands/memory/add.ts +++ b/packages/commands/src/commands/memory/add.ts @@ -13,9 +13,12 @@ import { 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, @@ -71,10 +74,6 @@ type AddFlags = ParsedFlags; /** Max messages accepted per AddMemory call (a Q&A pair counts as 2). */ const MAX_MESSAGES = 50; -/** Max characters accepted for custom_content. */ -const MAX_CONTENT_LENGTH = 512; -/** Max characters accepted for user_id. */ -const MAX_USER_ID_LENGTH = 64; export default defineCommand({ description: { @@ -96,6 +95,7 @@ export default defineCommand({ "The response lists the changed memory nodes; one call can add, update or delete several at once.", "zh-CN": "返回结果是变更的记忆片段列表;一次调用可能同时新增、更新或删除多条。", }, + MEMORY_RATE_LIMIT_NOTE, ], exampleArgs: [ { @@ -118,10 +118,10 @@ export default defineCommand({ ], validate: (flags: AddFlags) => { if (!flags.messages && !flags.content) return "Provide --messages or --content."; - if (flags.userId.length > MAX_USER_ID_LENGTH) - return `--user-id must be at most ${MAX_USER_ID_LENGTH} characters.`; - if (flags.content && flags.content.length > MAX_CONTENT_LENGTH) - return `--content must be at most ${MAX_CONTENT_LENGTH} characters.`; + 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) { diff --git a/packages/commands/src/commands/memory/delete.ts b/packages/commands/src/commands/memory/delete.ts index 9724cc06..2cfb4e59 100644 --- a/packages/commands/src/commands/memory/delete.ts +++ b/packages/commands/src/commands/memory/delete.ts @@ -11,6 +11,7 @@ import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, + checkMemoryScopeLengths, resolveWorkspaceId, } from "./shared.ts"; @@ -37,10 +38,28 @@ const DELETE_FLAGS = { export default defineCommand({ description: { "en-US": "Delete a memory node", "zh-CN": "删除记忆节点" }, auth: "apiKey", + risk: { + level: "high", + message: { + "en-US": "This permanently deletes the specified memory node and cannot be undone.", + "zh-CN": "该操作会永久删除指定的记忆片段,且无法恢复。", + }, + }, usageArgs: "--node-id --user-id [flags]", flags: DELETE_FLAGS, - notes: [MEMORY_WORKSPACE_NOTE], - exampleArgs: ["--node-id node_xxx --user-id user1 --workspace-id ws_xxx"], + 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; diff --git a/packages/commands/src/commands/memory/list.ts b/packages/commands/src/commands/memory/list.ts index 200c242a..af152eb9 100644 --- a/packages/commands/src/commands/memory/list.ts +++ b/packages/commands/src/commands/memory/list.ts @@ -13,6 +13,7 @@ import { MEMORY_WORKSPACE_NOTE, PROJECT_ID_FLAG, WORKSPACE_FLAG, + checkMemoryScopeLengths, resolveWorkspaceId, } from "./shared.ts"; @@ -53,6 +54,8 @@ export default defineCommand({ "--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."; diff --git a/packages/commands/src/commands/memory/profile-create.ts b/packages/commands/src/commands/memory/profile-create.ts index 241514e9..31db19fb 100644 --- a/packages/commands/src/commands/memory/profile-create.ts +++ b/packages/commands/src/commands/memory/profile-create.ts @@ -1,5 +1,6 @@ import { defineCommand, + UsageError, memoryEndpoint, profileSchemaPath, detectOutputFormat, @@ -15,6 +16,9 @@ import { MEMORY_WORKSPACE_NOTE, PLAN_VERSION_FLAG, WORKSPACE_FLAG, + assertAttributeFieldLengths, + checkMemoryScopeLengths, + checkProfileSchemaTextLengths, parseJsonArrayFlag, resolveWorkspaceId, } from "./shared.ts"; @@ -76,10 +80,20 @@ export default defineCommand({ '--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 attributes = parseJsonArrayFlag("--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: flags.name, attributes }; if (flags.description) body.description = flags.description; diff --git a/packages/commands/src/commands/memory/profile-delete.ts b/packages/commands/src/commands/memory/profile-delete.ts index a44ae772..32f6cd1d 100644 --- a/packages/commands/src/commands/memory/profile-delete.ts +++ b/packages/commands/src/commands/memory/profile-delete.ts @@ -11,6 +11,7 @@ import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, + checkMemoryScopeLengths, resolveWorkspaceId, } from "./shared.ts"; @@ -54,6 +55,7 @@ export default defineCommand({ }, ], 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; diff --git a/packages/commands/src/commands/memory/profile-get.ts b/packages/commands/src/commands/memory/profile-get.ts index 3fb5d094..c818e6d4 100644 --- a/packages/commands/src/commands/memory/profile-get.ts +++ b/packages/commands/src/commands/memory/profile-get.ts @@ -12,6 +12,7 @@ import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, + checkMemoryScopeLengths, resolveWorkspaceId, } from "./shared.ts"; @@ -56,6 +57,7 @@ export default defineCommand({ }, ], exampleArgs: ["--schema-id schema_xxx --user-id user1 --workspace-id ws_xxx"], + validate: (flags) => checkMemoryScopeLengths(flags), async run(ctx) { const { settings, flags } = ctx; diff --git a/packages/commands/src/commands/memory/profile-list.ts b/packages/commands/src/commands/memory/profile-list.ts index 47647c70..93086150 100644 --- a/packages/commands/src/commands/memory/profile-list.ts +++ b/packages/commands/src/commands/memory/profile-list.ts @@ -12,6 +12,7 @@ import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, + checkMemoryScopeLengths, resolveWorkspaceId, } from "./shared.ts"; @@ -42,6 +43,8 @@ export default defineCommand({ "--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."; diff --git a/packages/commands/src/commands/memory/profile-show.ts b/packages/commands/src/commands/memory/profile-show.ts index 0b5a1f37..bbe9eb71 100644 --- a/packages/commands/src/commands/memory/profile-show.ts +++ b/packages/commands/src/commands/memory/profile-show.ts @@ -12,6 +12,7 @@ import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, + checkMemoryScopeLengths, resolveWorkspaceId, } from "./shared.ts"; @@ -50,6 +51,7 @@ export default defineCommand({ "--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; diff --git a/packages/commands/src/commands/memory/profile-update.ts b/packages/commands/src/commands/memory/profile-update.ts index 067d0261..80897b07 100644 --- a/packages/commands/src/commands/memory/profile-update.ts +++ b/packages/commands/src/commands/memory/profile-update.ts @@ -13,6 +13,9 @@ import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, + assertAttributeFieldLengths, + checkMemoryScopeLengths, + checkProfileSchemaTextLengths, parseJsonArrayFlag, resolveWorkspaceId, } from "./shared.ts"; @@ -64,6 +67,7 @@ function validateOperations(operations: ProfileSchemaAttributeOperation[]): void if (operation.op !== "add" && !operation.attribute_id) { throw new UsageError(`${position}.attribute_id is required when op is "${operation.op}"`); } + assertAttributeFieldLengths(position, operation); }); } @@ -99,7 +103,7 @@ export default defineCommand({ validate: (flags) => { if (!flags.name && !flags.description && !flags.attributesOperations) return "Provide --name, --description, or --attributes-operations."; - return undefined; + return checkMemoryScopeLengths(flags) ?? checkProfileSchemaTextLengths(flags) ?? undefined; }, async run(ctx) { const { settings, flags } = ctx; diff --git a/packages/commands/src/commands/memory/search.ts b/packages/commands/src/commands/memory/search.ts index ce0df6e6..6953b483 100644 --- a/packages/commands/src/commands/memory/search.ts +++ b/packages/commands/src/commands/memory/search.ts @@ -13,9 +13,11 @@ import { 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"; @@ -117,6 +119,7 @@ export default defineCommand({ "--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: [ { @@ -135,6 +138,8 @@ export default defineCommand({ ], 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)) diff --git a/packages/commands/src/commands/memory/shared.ts b/packages/commands/src/commands/memory/shared.ts index 0ace6f0e..607d103a 100644 --- a/packages/commands/src/commands/memory/shared.ts +++ b/packages/commands/src/commands/memory/shared.ts @@ -52,6 +52,100 @@ export const PROJECT_ID_FLAG = { }, } 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 { let parsed: unknown; diff --git a/packages/commands/src/commands/memory/update.ts b/packages/commands/src/commands/memory/update.ts index 9dc906a8..b6679ed1 100644 --- a/packages/commands/src/commands/memory/update.ts +++ b/packages/commands/src/commands/memory/update.ts @@ -10,7 +10,9 @@ 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"; @@ -60,9 +62,6 @@ const UPDATE_FLAGS = { ...WORKSPACE_FLAG, } satisfies FlagsDef; -/** Max characters accepted for custom_content. */ -const MAX_CONTENT_LENGTH = 512; - export default defineCommand({ description: { "en-US": "Update a memory node content", "zh-CN": "更新记忆节点内容" }, auth: "apiKey", @@ -90,8 +89,10 @@ export default defineCommand({ }, ], validate: (flags) => { - if (flags.content.length > MAX_CONTENT_LENGTH) - return `--content must be at most ${MAX_CONTENT_LENGTH} characters.`; + 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; diff --git a/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts index b74a7c71..a0672b5c 100644 --- a/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-add.e2e.test.ts @@ -66,6 +66,22 @@ describe("e2e: memory add", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts index fcef5b3f..70d12b89 100644 --- a/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-delete.e2e.test.ts @@ -20,10 +20,27 @@ describe("e2e: memory delete", () => { 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", @@ -44,6 +61,22 @@ describe("e2e: memory delete", () => { 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", @@ -59,6 +92,8 @@ describe("e2e: memory delete", () => { "--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(stdout); const endpoint = data.endpoint ?? ""; @@ -97,6 +132,7 @@ describe.skipIf(!isMemoryE2EReady())("e2e: memory delete (live)", () => { `no-such-node-${Date.now()}`, "--user-id", memoryUserId(), + "--yes", "--output", "json", ]); @@ -208,6 +244,9 @@ describe.skipIf(!isMemoryE2EReady())("e2e: memory delete (live)", () => { (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", @@ -217,6 +256,7 @@ describe.skipIf(!isMemoryE2EReady())("e2e: memory delete (live)", () => { addedNodeId, "--user-id", userId, + "--yes", "--output", "json", ]); diff --git a/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts index 9b4867a5..1e579c8f 100644 --- a/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-list.e2e.test.ts @@ -63,6 +63,20 @@ describe("e2e: memory list", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts index 79cabddd..1c9cff60 100644 --- a/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-profile-create.e2e.test.ts @@ -42,6 +42,69 @@ describe("e2e: memory profile create", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts index 2393c55e..579387d2 100644 --- a/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-profile-delete.e2e.test.ts @@ -54,6 +54,21 @@ describe("e2e: memory profile delete", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts index c10d304d..f1d17f33 100644 --- a/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-profile-get.e2e.test.ts @@ -45,6 +45,23 @@ describe("e2e: memory profile get", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts index a189ab31..f1f7073e 100644 --- a/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-profile-list.e2e.test.ts @@ -34,6 +34,19 @@ describe("e2e: memory profile list", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts index c146942e..4b79f6af 100644 --- a/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-profile-show.e2e.test.ts @@ -28,6 +28,21 @@ describe("e2e: memory profile show", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts index c9be25f1..401de436 100644 --- a/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-profile-update.e2e.test.ts @@ -136,6 +136,36 @@ describe("e2e: memory profile update", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts index bfbfe5e8..cc233bcf 100644 --- a/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-search.e2e.test.ts @@ -65,6 +65,22 @@ describe("e2e: memory search", () => { 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", diff --git a/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts b/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts index 601a523b..e4413791 100644 --- a/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts +++ b/packages/commands/tests/e2e/memory/memory-update.e2e.test.ts @@ -91,6 +91,24 @@ describe("e2e: memory update", () => { 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", diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index ea939b4d..b5a50822 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -341,9 +341,18 @@ 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 开启 Rerank,lite 关闭(单价不同)。 */ diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 8c608be6..84acc240 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -71,6 +71,7 @@ export type { MemoryAddRequest, MemoryAddResponse, MemoryChangedNode, + MemoryContentPart, MemoryMessage, MemoryNode, MemoryNodeListResponse, diff --git a/skills/bailian-cli/reference/memory.md b/skills/bailian-cli/reference/memory.md index d7536705..770736de 100644 --- a/skills/bailian-cli/reference/memory.md +++ b/skills/bailian-cli/reference/memory.md @@ -52,6 +52,7 @@ Index: [index.md](index.md) - 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 @@ -73,12 +74,16 @@ bl memory add --user-id user1 --content "Attended WAIC" --meta-data '{"location" ### `bl memory delete` -| Field | Value | -| ------------------ | -------------------------------------------------------- | -| **Name** | `memory delete` | -| **Description** | Delete a memory node | -| **Authentication** | API Key | -| **Usage** | `bl memory delete --node-id --user-id [flags]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------ | +| **Name** | `memory delete` | +| **Description** | Delete a memory node | +| **Authentication** | API Key | +| **Usage** | `bl memory delete --node-id --user-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 @@ -88,12 +93,14 @@ bl memory add --user-id user1 --content "Attended WAIC" --meta-data '{"location" | `--user-id ` | string | yes | Memory entity ID that owns the memory (required) | | `--memory-library-id ` | string | no | Memory library ID (default: the account's default library) | | `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--yes` | switch | no | Confirm this high-risk operation | | `--api-key ` | string | no | API key | | `--base-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 @@ -101,6 +108,11 @@ bl memory add --user-id user1 --content "Attended WAIC" --meta-data '{"location" 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` | Field | Value | @@ -390,6 +402,7 @@ bl memory profile update --schema-id schema_xxx --attributes-operations '[{"op": - 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