Skip to content

Commit 75a7100

Browse files
committed
feat(memory): 完善记忆及画像相关命令支持并统一参数
- 在 CLI 文档中新增记忆 live 场景说明与变量需求 - 扩展 CLI 命令集,添加记忆画像 Profile 的增删改查功能 - 统一所有记忆相关命令加入 workspace 和 library 参数支持 - 记忆添加命令支持自定义 meta_data,完善输入校验和互斥说明 - 记忆删除、列表、获取用户画像等命令增加工作空间参数支持 - 画像模板相关命令新增分页参数及属性操作接口 - 重构知识命令共享模块,移除重复的 workspace 解析实现,统一导出 - 优化命令输出格式,文本模式下显示更多详细信息和异常提示 - 提供完整的示例参数,增强命令使用说明和提示文档
1 parent 2090293 commit 75a7100

39 files changed

Lines changed: 3793 additions & 584 deletions

docs/agents/cli-e2e-tests.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
7171

7272
| 场景 | 条件 |
7373
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
74-
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
74+
| 文本/搜索/配置 | `isDashScopeE2EReady()` |
7575
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
7676
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
7777
| OpenAPI AK/SK | `isOpenApiE2EReady()``.env` 中必须同时提供完整 AK/SK) |
7878
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` |
7979
| 知识库 chat/search live | `isChatE2EReady()` / `isSearchE2EReady()``knowledge chat/search`,需 `BAILIAN_WORKSPACE_ID` + agent ID) |
80+
| 记忆 live | `isMemoryE2EReady()`(另需 `BAILIAN_E2E_MEMORY_LIBRARY_ID`;记忆服务需账号单独开通) |
8081

8182
## 用例类型
8283

packages/cli/src/commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {
2929
memoryUpdate,
3030
memoryDelete,
3131
memoryProfileCreate,
32+
memoryProfileList,
33+
memoryProfileShow,
34+
memoryProfileUpdate,
35+
memoryProfileDelete,
3236
memoryProfileGet,
3337
knowledgeRetrieve,
3438
knowledgeSearch,
@@ -243,6 +247,10 @@ export const commands: Record<string, AnyCommand> = {
243247
"memory update": memoryUpdate,
244248
"memory delete": memoryDelete,
245249
"memory profile create": memoryProfileCreate,
250+
"memory profile list": memoryProfileList,
251+
"memory profile show": memoryProfileShow,
252+
"memory profile update": memoryProfileUpdate,
253+
"memory profile delete": memoryProfileDelete,
246254
"memory profile get": memoryProfileGet,
247255
"knowledge retrieve": knowledgeRetrieve,
248256
"knowledge search": knowledgeSearch,

packages/commands/src/commands/knowledge/shared.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Shared building blocks for the knowledge admin commands.
22
import {
33
BailianError,
4-
ExitCode,
54
ragEndpoint,
65
RAG_PATHS,
76
type Client,
@@ -12,18 +11,9 @@ import {
1211
} from "bailian-cli-core";
1312
import { poll } from "bailian-cli-runtime";
1413

15-
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
16-
// flag here (the console credential scope does not apply).
17-
export const WORKSPACE_FLAG = {
18-
workspaceId: {
19-
type: "string",
20-
valueHint: "<id>",
21-
description: {
22-
"en-US": "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
23-
"zh-CN": "API Endpoint URL 使用的 Workspace ID(也可设置 BAILIAN_WORKSPACE_ID)",
24-
},
25-
},
26-
} satisfies FlagsDef;
14+
// The workspace scope is shared with the memory commands, so it lives in
15+
// ../shared/workspace.ts; re-exported here to keep the knowledge imports local.
16+
export { resolveWorkspaceId, WORKSPACE_FLAG } from "../shared/workspace.ts";
2717

2818
// Unified pagination flags for admin list commands. The server-side page/size
2919
// parameter names differ per endpoint (page_number/page_num/pageNum/pageNumber…)
@@ -43,23 +33,6 @@ export const PAGE_FLAGS = {
4333
},
4434
} satisfies FlagsDef;
4535

46-
/** Three-level fallback: flag > BAILIAN_WORKSPACE_ID env > config (env/config are merged into settings); missing → USAGE. */
47-
export function resolveWorkspaceId(ctx: {
48-
flags: { workspaceId?: string };
49-
settings: { workspaceId?: string };
50-
identity: { binName: string };
51-
}): string {
52-
const workspaceId = ctx.flags.workspaceId || ctx.settings.workspaceId;
53-
if (!workspaceId) {
54-
throw new BailianError(
55-
"Workspace ID is required.",
56-
ExitCode.USAGE,
57-
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
58-
);
59-
}
60-
return workspaceId;
61-
}
62-
6336
/** Truncate text-mode table rows to the terminal width; no truncation when not a TTY (pipe/redirect). */
6437
export function truncateLine(line: string): string {
6538
if (!process.stdout.isTTY) return line;

packages/commands/src/commands/memory/add.ts

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,80 @@ import {
22
defineCommand,
33
UsageError,
44
memoryAddPath,
5+
memoryEndpoint,
56
detectOutputFormat,
67
type FlagsDef,
78
type ParsedFlags,
89
type MemoryAddRequest,
910
type MemoryAddResponse,
11+
type MemoryMessage,
1012
} from "bailian-cli-core";
1113
import { emitResult, emitBare } from "bailian-cli-runtime";
14+
import {
15+
MEMORY_LIBRARY_FLAG,
16+
MEMORY_WORKSPACE_NOTE,
17+
PROJECT_ID_FLAG,
18+
WORKSPACE_FLAG,
19+
parseJsonArrayFlag,
20+
parseJsonObjectFlag,
21+
resolveWorkspaceId,
22+
} from "./shared.ts";
1223

1324
const ADD_FLAGS = {
1425
userId: {
1526
type: "string",
1627
valueHint: "<id>",
17-
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID(必填)" },
28+
description: {
29+
"en-US": "Memory entity ID that owns the memory (required)",
30+
"zh-CN": "记忆实体 ID,标识记忆归属对象(必填)",
31+
},
1832
required: true,
1933
},
2034
messages: {
2135
type: "string",
2236
valueHint: "<json>",
2337
description: {
24-
"en-US": 'Messages JSON array: [{"role":"user","content":"..."},...]',
25-
"zh-CN": '消息 JSON 数组:[{"role":"user","content":"..."},...]',
38+
"en-US": 'Messages JSON array: [{"role":"user","content":"..."},...] (max 50)',
39+
"zh-CN": '消息 JSON 数组:[{"role":"user","content":"..."},...](最多 50 条)',
2640
},
2741
},
2842
content: {
2943
type: "string",
3044
valueHint: "<text>",
31-
description: { "en-US": "Custom content text to memorize", "zh-CN": "要记忆的自定义内容文本" },
45+
description: {
46+
"en-US": "Custom content to memorize verbatim; takes precedence over --messages",
47+
"zh-CN": "要原样记忆的自定义内容;优先级高于 --messages",
48+
},
3249
},
3350
profileSchema: {
3451
type: "string",
3552
valueHint: "<id>",
3653
description: {
37-
"en-US": "Profile schema ID for user profiling",
38-
"zh-CN": "用于用户画像的 Profile Schema ID",
54+
"en-US": "Profile schema ID; without it no user profile is extracted",
55+
"zh-CN": "画像模板 ID;不传则不提取用户画像",
3956
},
4057
},
41-
memoryLibraryId: {
58+
metaData: {
4259
type: "string",
43-
valueHint: "<id>",
60+
valueHint: "<json>",
4461
description: {
45-
"en-US": "Memory library ID (isolate memory space)",
46-
"zh-CN": "记忆库 ID(用于隔离记忆空间)",
62+
"en-US": 'Custom metadata JSON object: {"location_name":"Beijing"}',
63+
"zh-CN": '用户自定义信息 JSON 对象:{"location_name":"北京"}',
4764
},
4865
},
66+
...PROJECT_ID_FLAG,
67+
...MEMORY_LIBRARY_FLAG,
68+
...WORKSPACE_FLAG,
4969
} satisfies FlagsDef;
5070
type AddFlags = ParsedFlags<typeof ADD_FLAGS>;
5171

72+
/** Max messages accepted per AddMemory call (a Q&A pair counts as 2). */
73+
const MAX_MESSAGES = 50;
74+
/** Max characters accepted for custom_content. */
75+
const MAX_CONTENT_LENGTH = 512;
76+
/** Max characters accepted for user_id. */
77+
const MAX_USER_ID_LENGTH = 64;
78+
5279
export default defineCommand({
5380
description: {
5481
"en-US": "Add memory from messages or custom content",
@@ -57,10 +84,24 @@ export default defineCommand({
5784
auth: "apiKey",
5885
usageArgs: "--user-id <id> [--messages <json>] [--content <text>] [flags]",
5986
flags: ADD_FLAGS,
87+
notes: [
88+
MEMORY_WORKSPACE_NOTE,
89+
{
90+
"en-US":
91+
"--content and --messages are mutually exclusive: when --content is set, --messages is ignored by the server.",
92+
"zh-CN": "--content 与 --messages 互斥:传了 --content 时服务端会忽略 --messages。",
93+
},
94+
{
95+
"en-US":
96+
"The response lists the changed memory nodes; one call can add, update or delete several at once.",
97+
"zh-CN": "返回结果是变更的记忆片段列表;一次调用可能同时新增、更新或删除多条。",
98+
},
99+
],
60100
exampleArgs: [
61101
{
62-
"en-US": '--user-id user1 --content "The user likes Python programming"',
63-
"zh-CN": '--user-id user1 --content "用户喜欢使用 Python 编程"',
102+
"en-US":
103+
'--user-id user1 --content "The user likes Python programming" --workspace-id ws_xxx',
104+
"zh-CN": '--user-id user1 --content "用户喜欢使用 Python 编程" --workspace-id ws_xxx',
64105
},
65106
{
66107
"en-US": '--user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'',
@@ -70,46 +111,61 @@ export default defineCommand({
70111
"en-US": '--user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx',
71112
"zh-CN": '--user-id user1 --content "居住在北京" --profile-schema schema_xxx',
72113
},
114+
{
115+
"en-US": '--user-id user1 --content "Attended WAIC" --meta-data \'{"location":"Shanghai"}\'',
116+
"zh-CN": '--user-id user1 --content "参加了 WAIC" --meta-data \'{"location":"上海"}\'',
117+
},
73118
],
74-
validate: (f: AddFlags) =>
75-
!f.messages && !f.content ? "Provide --messages or --content." : undefined,
119+
validate: (flags: AddFlags) => {
120+
if (!flags.messages && !flags.content) return "Provide --messages or --content.";
121+
if (flags.userId.length > MAX_USER_ID_LENGTH)
122+
return `--user-id must be at most ${MAX_USER_ID_LENGTH} characters.`;
123+
if (flags.content && flags.content.length > MAX_CONTENT_LENGTH)
124+
return `--content must be at most ${MAX_CONTENT_LENGTH} characters.`;
125+
return undefined;
126+
},
76127
async run(ctx) {
77128
const { settings, flags } = ctx;
78-
const userId = flags.userId;
79129

80-
const body: MemoryAddRequest = { user_id: userId };
130+
const body: MemoryAddRequest = { user_id: flags.userId };
81131

82132
if (flags.messages) {
83-
try {
84-
body.messages = JSON.parse(flags.messages);
85-
} catch {
86-
throw new UsageError("--messages must be valid JSON array");
133+
const messages = parseJsonArrayFlag<MemoryMessage>("--messages", flags.messages);
134+
if (messages.length > MAX_MESSAGES) {
135+
throw new UsageError(`--messages accepts at most ${MAX_MESSAGES} messages`);
87136
}
137+
body.messages = messages;
88138
}
89139

90-
if (flags.content) {
91-
body.custom_content = flags.content;
92-
}
93-
140+
if (flags.content) body.custom_content = flags.content;
141+
if (flags.metaData) body.meta_data = parseJsonObjectFlag("--meta-data", flags.metaData);
94142
if (flags.profileSchema) body.profile_schema = flags.profileSchema;
143+
if (flags.projectId) body.project_id = flags.projectId;
95144
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
96145

97146
const format = detectOutputFormat(settings.output);
147+
const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryAddPath());
98148

99149
if (settings.dryRun) {
100-
emitResult({ endpoint: ctx.client.url(memoryAddPath()), request: body }, format);
150+
emitResult({ endpoint: url, method: "POST", request: body }, format);
101151
return;
102152
}
103153

104154
const response = await ctx.client.requestJson<MemoryAddResponse>({
105-
path: memoryAddPath(),
155+
path: url,
106156
method: "POST",
107157
body,
108158
});
109159

110160
if (settings.quiet || format === "text") {
111-
const ids = response.memory_ids?.join(", ") || "none";
112-
emitBare(`Memory added. IDs: ${ids}`);
161+
const nodes = response.memory_nodes ?? [];
162+
if (nodes.length === 0) {
163+
emitBare("No memory node changed.");
164+
return;
165+
}
166+
for (const node of nodes) {
167+
emitBare(`[${node.event ?? "ADD"}] ${node.memory_node_id} ${node.content}`);
168+
}
113169
} else {
114170
emitResult(response, format);
115171
}

packages/commands/src/commands/memory/delete.ts

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,62 @@
1-
import { defineCommand, memoryNodePath, detectOutputFormat } from "bailian-cli-core";
1+
import {
2+
defineCommand,
3+
memoryEndpoint,
4+
memoryNodePath,
5+
detectOutputFormat,
6+
type FlagsDef,
7+
} from "bailian-cli-core";
28
import { emitResult, emitBare } from "bailian-cli-runtime";
9+
import { buildQuery } from "../shared/params.ts";
10+
import {
11+
MEMORY_LIBRARY_FLAG,
12+
MEMORY_WORKSPACE_NOTE,
13+
WORKSPACE_FLAG,
14+
resolveWorkspaceId,
15+
} from "./shared.ts";
16+
17+
const DELETE_FLAGS = {
18+
nodeId: {
19+
type: "string",
20+
valueHint: "<id>",
21+
description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID(必填)" },
22+
required: true,
23+
},
24+
userId: {
25+
type: "string",
26+
valueHint: "<id>",
27+
description: {
28+
"en-US": "Memory entity ID that owns the memory (required)",
29+
"zh-CN": "记忆实体 ID,标识记忆归属对象(必填)",
30+
},
31+
required: true,
32+
},
33+
...MEMORY_LIBRARY_FLAG,
34+
...WORKSPACE_FLAG,
35+
} satisfies FlagsDef;
336

437
export default defineCommand({
538
description: { "en-US": "Delete a memory node", "zh-CN": "删除记忆节点" },
639
auth: "apiKey",
7-
usageArgs: "--node-id <id> --user-id <id>",
8-
flags: {
9-
nodeId: {
10-
type: "string",
11-
valueHint: "<id>",
12-
description: { "en-US": "Memory node ID (required)", "zh-CN": "记忆节点 ID(必填)" },
13-
required: true,
14-
},
15-
userId: {
16-
type: "string",
17-
valueHint: "<id>",
18-
description: { "en-US": "User ID (required)", "zh-CN": "用户 ID(必填)" },
19-
required: true,
20-
},
21-
memoryLibraryId: {
22-
type: "string",
23-
valueHint: "<id>",
24-
description: {
25-
"en-US": "Memory library ID (non-default library)",
26-
"zh-CN": "记忆库 ID(非默认记忆库)",
27-
},
28-
},
29-
},
30-
exampleArgs: ["--node-id node_xxx --user-id user1"],
40+
usageArgs: "--node-id <id> --user-id <id> [flags]",
41+
flags: DELETE_FLAGS,
42+
notes: [MEMORY_WORKSPACE_NOTE],
43+
exampleArgs: ["--node-id node_xxx --user-id user1 --workspace-id ws_xxx"],
3144
async run(ctx) {
3245
const { settings, flags } = ctx;
3346
const nodeId = flags.nodeId;
34-
const userId = flags.userId;
3547

3648
const format = detectOutputFormat(settings.output);
37-
const params = new URLSearchParams({ user_id: userId });
38-
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
39-
const path = `${memoryNodePath(nodeId)}?${params.toString()}`;
49+
const url =
50+
memoryEndpoint(resolveWorkspaceId(ctx), memoryNodePath(nodeId)) +
51+
buildQuery({ user_id: flags.userId, memory_library_id: flags.memoryLibraryId });
4052

4153
if (settings.dryRun) {
42-
emitResult({ endpoint: ctx.client.url(path), method: "DELETE" }, format);
54+
emitResult({ endpoint: url, method: "DELETE" }, format);
4355
return;
4456
}
4557

4658
const response = await ctx.client.requestJson<{ request_id: string }>({
47-
path,
59+
path: url,
4860
method: "DELETE",
4961
});
5062

0 commit comments

Comments
 (0)