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