Skip to content

Commit ea77c46

Browse files
committed
feat(security): 本地化Agent安全命令的文本输出
- 引入 localize 函数支持中英文本地化显示 - 安全警报和概览命令文本渲染时使用本地化字符串 - 增强输出格式逻辑,--quiet 优先显示告警ID列表确保管道兼容 - 修正安全API错误码 12000092 映射,避免错误提示要求重新登录 - 安全共享组件中统一管理本地化标签和UI文本 - 添加覆盖文本本地化及输出行为的单元测试 - 更新CLI命令索引,补充安全相关命令描述和认证信息
1 parent caf620a commit ea77c46

8 files changed

Lines changed: 284 additions & 153 deletions

File tree

‎packages/commands/src/commands/security/alerts.ts‎

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import {
77
type SecurityAlertList,
88
} from "bailian-cli-core";
99
import { emitResult, emitBare } from "bailian-cli-runtime";
10-
import { WORKSPACE_FLAG, renderAlert, resolveSecurityHost, setSecurityParam } from "./shared.ts";
10+
import {
11+
SECURITY_UI,
12+
WORKSPACE_FLAG,
13+
renderAlert,
14+
resolveSecurityHost,
15+
setSecurityParam,
16+
} from "./shared.ts";
1117

1218
const ASSET_TYPES = ["agent", "tool", "skill", "knowledge_base", "memory", "channel"] as const;
1319

@@ -139,7 +145,7 @@ export default defineCommand({
139145
},
140146
],
141147
async run(ctx) {
142-
const { settings, flags } = ctx;
148+
const { settings, flags, localize } = ctx;
143149
const format = detectOutputFormat(settings.output);
144150
const host = resolveSecurityHost(ctx);
145151

@@ -169,33 +175,38 @@ export default defineCommand({
169175
const data = await securityGet<SecurityAlertList>(ctx.client, endpoint);
170176
const alerts = data?.data ?? [];
171177

172-
if (format === "json") {
173-
emitResult(data ?? { stats: null, data: [], next_page: null }, format);
178+
// --quiet wins over the output format: emit a bare, pipe-friendly list of
179+
// alert IDs even when output=json is configured, so ID-driven pipelines
180+
// (`bl agents security alerts --quiet | xargs …`) keep working.
181+
if (settings.quiet) {
182+
for (const alert of alerts) emitBare(alert.alert_id);
174183
return;
175184
}
176185

177-
if (settings.quiet) {
178-
for (const alert of alerts) emitBare(alert.alert_id);
186+
if (format === "json") {
187+
emitResult(data ?? { stats: null, data: [], next_page: null }, format);
179188
return;
180189
}
181190

182191
const stats = data?.stats;
183192
if (stats) {
184193
emitBare(
185-
`Total: ${stats.total ?? "-"} high: ${stats.high ?? "-"} ` +
186-
`medium: ${stats.medium ?? "-"} low: ${stats.low ?? "-"}\n`,
194+
`${localize(SECURITY_UI.total)}: ${stats.total ?? "-"} ` +
195+
`${localize(SECURITY_UI.high)}: ${stats.high ?? "-"} ` +
196+
`${localize(SECURITY_UI.medium)}: ${stats.medium ?? "-"} ` +
197+
`${localize(SECURITY_UI.low)}: ${stats.low ?? "-"}\n`,
187198
);
188199
}
189200

190201
if (alerts.length === 0) {
191-
emitBare("No alerts found.");
202+
emitBare(localize(SECURITY_UI.noAlerts));
192203
return;
193204
}
194205

195-
for (const alert of alerts) renderAlert(alert);
206+
for (const alert of alerts) renderAlert(localize, alert);
196207

197208
if (data?.next_page) {
198-
emitBare(`Next page cursor: ${data.next_page}`);
209+
emitBare(`${localize(SECURITY_UI.nextPageCursor)}: ${data.next_page}`);
199210
}
200211
},
201212
});

‎packages/commands/src/commands/security/overview.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
CAPABILITY_LABELS,
1313
PROTECTION_LABELS,
1414
SCAN_CARDS,
15+
SECURITY_UI,
1516
WORKSPACE_FLAG,
1617
renderToggles,
1718
resolveSecurityHost,
@@ -58,7 +59,7 @@ export default defineCommand({
5859
},
5960
],
6061
async run(ctx) {
61-
const { settings } = ctx;
62+
const { settings, localize } = ctx;
6263
const format = detectOutputFormat(settings.output);
6364
const endpoint = securityOverviewEndpoint(resolveSecurityHost(ctx));
6465

@@ -70,7 +71,7 @@ export default defineCommand({
7071
const data = await securityGet<SecurityOverview>(ctx.client, endpoint);
7172
if (!data) {
7273
if (format === "json") emitResult({}, format);
73-
else emitBare("Overview unavailable.");
74+
else emitBare(localize(SECURITY_UI.overviewUnavailable));
7475
return;
7576
}
7677

@@ -85,22 +86,28 @@ export default defineCommand({
8586
const stat = keys
8687
.map((key) => data[key] as SecurityScanStat | null | undefined)
8788
.find((value) => value !== undefined);
88-
return { label, stat: stat ?? null };
89+
return { label: localize(label), stat: stat ?? null };
8990
});
9091
const sum = (pick: (stat: SecurityScanStat) => number | null): number =>
9192
cards.reduce((total, card) => total + (card.stat ? (pick(card.stat) ?? 0) : 0), 0);
9293

93-
emitBare(`Scanned: ${sum((stat) => stat.scanned)} Risks: ${sum((stat) => stat.hit)}`);
94+
emitBare(
95+
`${localize(SECURITY_UI.scannedLabel)}: ${sum((stat) => stat.scanned)} ` +
96+
`${localize(SECURITY_UI.risksLabel)}: ${sum((stat) => stat.hit)}`,
97+
);
9498

95-
renderToggles("Capabilities", data.capabilities, CAPABILITY_LABELS);
96-
renderToggles("Protection", data.protection, PROTECTION_LABELS);
99+
renderToggles(localize, SECURITY_UI.capabilities, data.capabilities, CAPABILITY_LABELS);
100+
renderToggles(localize, SECURITY_UI.protection, data.protection, PROTECTION_LABELS);
97101

98-
emitBare("\nDetections");
102+
emitBare(`\n${localize(SECURITY_UI.detections)}`);
99103
for (const { label, stat } of cards) {
100104
if (!stat) {
101-
emitBare(` ${label} (unavailable)`);
105+
emitBare(` ${label} ${localize(SECURITY_UI.unavailable)}`);
102106
} else {
103-
emitBare(` ${label} hit ${stat.hit ?? "-"} / scanned ${stat.scanned ?? "-"}`);
107+
emitBare(
108+
` ${label} ${localize(SECURITY_UI.hit)} ${stat.hit ?? "-"} / ` +
109+
`${localize(SECURITY_UI.scanned)} ${stat.scanned ?? "-"}`,
110+
);
104111
}
105112
}
106113
},

‎packages/commands/src/commands/security/shared.ts‎

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Shared building blocks for the Agent security commands (bl security *).
1+
// Shared building blocks for the Agent security commands (bl agents security *).
22
//
33
// AgentStudio uses the same per-workspace host scheme as the knowledge admin
44
// plane, so the --workspace-id flag and its three-level resolver are reused
@@ -7,6 +7,7 @@
77
import {
88
agentStudioHost,
99
isDashScopeGateway,
10+
type LocalizedText,
1011
type SecurityAlert,
1112
type SecurityOverview,
1213
type SecurityToggle,
@@ -36,33 +37,66 @@ export function resolveSecurityHost(ctx: {
3637
return agentStudioHost(resolveWorkspaceId(ctx));
3738
}
3839

39-
// The API returns codes only — display names live here to match the console.
40-
export const CAPABILITY_LABELS: Record<string, string> = {
41-
agent_identity: "Agent 身份签发",
42-
content_safety: "内容安全",
43-
supply_chain_scan: "供应链静态扫描",
44-
credential_isolation: "凭证隔离",
45-
session_lifecycle: "session 生命周期治理",
40+
/** Locale selector supplied by the command context (`ctx.localize`). */
41+
export type Localize = (text: LocalizedText) => string;
42+
43+
// The API returns codes only — display names live here to match the console,
44+
// localized to en-US / zh-CN per the resolved language.
45+
export const CAPABILITY_LABELS: Record<string, LocalizedText> = {
46+
agent_identity: { "en-US": "Agent identity issuance", "zh-CN": "Agent 身份签发" },
47+
content_safety: { "en-US": "Content safety", "zh-CN": "内容安全" },
48+
supply_chain_scan: { "en-US": "Supply-chain static scan", "zh-CN": "供应链静态扫描" },
49+
credential_isolation: { "en-US": "Credential isolation", "zh-CN": "凭证隔离" },
50+
session_lifecycle: { "en-US": "Session lifecycle governance", "zh-CN": "session 生命周期治理" },
4651
};
4752

4853
// Protection entries are keyed by asset type; the doc lists only the codes, so
4954
// the display names are maintained here.
50-
export const PROTECTION_LABELS: Record<string, string> = {
51-
flow_agent: "Flow Agent",
52-
managed_agent: "Managed Agents",
53-
knowledge_base: "RAG",
54-
memory: "Memory",
55-
mcp: "Store",
56-
external_agent: "BYOA 托管",
55+
export const PROTECTION_LABELS: Record<string, LocalizedText> = {
56+
flow_agent: { "en-US": "Flow Agent", "zh-CN": "Flow Agent" },
57+
managed_agent: { "en-US": "Managed Agents", "zh-CN": "Managed Agents" },
58+
knowledge_base: { "en-US": "RAG", "zh-CN": "RAG" },
59+
memory: { "en-US": "Memory", "zh-CN": "Memory" },
60+
mcp: { "en-US": "Store", "zh-CN": "Store" },
61+
external_agent: { "en-US": "BYOA hosting", "zh-CN": "BYOA 托管" },
5762
};
5863

5964
/** Detection cards summed into the overview banner: label + snake_case (REST) / camelCase (gateway) keys. */
60-
export const SCAN_CARDS: Array<{ label: string; keys: Array<keyof SecurityOverview> }> = [
61-
{ label: "内容安全", keys: ["content_safety", "contentSafety"] },
62-
{ label: "文件扫描", keys: ["file_scan", "fileScan"] },
63-
{ label: "技能扫描", keys: ["skill_scan", "skillScan"] },
65+
export const SCAN_CARDS: Array<{ label: LocalizedText; keys: Array<keyof SecurityOverview> }> = [
66+
{
67+
label: { "en-US": "Content safety", "zh-CN": "内容安全" },
68+
keys: ["content_safety", "contentSafety"],
69+
},
70+
{ label: { "en-US": "File scan", "zh-CN": "文件扫描" }, keys: ["file_scan", "fileScan"] },
71+
{ label: { "en-US": "Skill scan", "zh-CN": "技能扫描" }, keys: ["skill_scan", "skillScan"] },
6472
];
6573

74+
/** User-facing strings shared by the security commands, localized en-US / zh-CN. */
75+
export const SECURITY_UI = {
76+
unavailable: { "en-US": "(unavailable)", "zh-CN": "(不可用)" },
77+
on: { "en-US": "on", "zh-CN": "开启" },
78+
off: { "en-US": "off", "zh-CN": "关闭" },
79+
scannedLabel: { "en-US": "Scanned", "zh-CN": "扫描" },
80+
risksLabel: { "en-US": "Risks", "zh-CN": "风险" },
81+
capabilities: { "en-US": "Capabilities", "zh-CN": "能力项" },
82+
protection: { "en-US": "Protection", "zh-CN": "防护项" },
83+
detections: { "en-US": "Detections", "zh-CN": "检测项" },
84+
overviewUnavailable: { "en-US": "Overview unavailable.", "zh-CN": "总览不可用。" },
85+
hit: { "en-US": "hit", "zh-CN": "命中" },
86+
scanned: { "en-US": "scanned", "zh-CN": "扫描" },
87+
total: { "en-US": "Total", "zh-CN": "总计" },
88+
high: { "en-US": "high", "zh-CN": "高" },
89+
medium: { "en-US": "medium", "zh-CN": "中" },
90+
low: { "en-US": "low", "zh-CN": "低" },
91+
noAlerts: { "en-US": "No alerts found.", "zh-CN": "未找到告警。" },
92+
nextPageCursor: { "en-US": "Next page cursor", "zh-CN": "下一页游标" },
93+
app: { "en-US": "app", "zh-CN": "应用" },
94+
asset: { "en-US": "asset", "zh-CN": "资产" },
95+
status: { "en-US": "status", "zh-CN": "状态" },
96+
source: { "en-US": "source", "zh-CN": "来源" },
97+
checked: { "en-US": "checked", "zh-CN": "检测时间" },
98+
} satisfies Record<string, LocalizedText>;
99+
66100
/** Append a query param only when present; arrays append each item (repeatable). */
67101
export function setSecurityParam(
68102
params: URLSearchParams,
@@ -77,20 +111,23 @@ export function setSecurityParam(
77111
params.set(key, String(value));
78112
}
79113

80-
/** Render a capability / protection toggle group; codes map to display names. */
114+
/** Render a capability / protection toggle group; codes map to localized display names. */
81115
export function renderToggles(
82-
title: string,
116+
localize: Localize,
117+
title: LocalizedText,
83118
toggles: SecurityToggle[] | null | undefined,
84-
labels: Record<string, string>,
119+
labels: Record<string, LocalizedText>,
85120
): void {
86-
emitBare(`\n${title}`);
121+
emitBare(`\n${localize(title)}`);
87122
if (!toggles || toggles.length === 0) {
88-
emitBare(" (unavailable)");
123+
emitBare(` ${localize(SECURITY_UI.unavailable)}`);
89124
return;
90125
}
91126
for (const toggle of toggles) {
92127
const count = typeof toggle.count === "number" ? ` (${toggle.count})` : "";
93-
emitBare(` ${toggle.enabled ? "on " : "off"} ${labels[toggle.key] ?? toggle.key}${count}`);
128+
const state = localize(toggle.enabled ? SECURITY_UI.on : SECURITY_UI.off);
129+
const label = labels[toggle.key] ? localize(labels[toggle.key]) : toggle.key;
130+
emitBare(` ${state} ${label}${count}`);
94131
}
95132
}
96133

@@ -102,15 +139,15 @@ export function formatSecurityTime(value: string | null | undefined): string {
102139
return new Date(millis).toISOString().replace("T", " ").slice(0, 19);
103140
}
104141

105-
/** Render one alert row in text mode. */
106-
export function renderAlert(alert: SecurityAlert): void {
142+
/** Render one alert row in text mode; field labels are localized. */
143+
export function renderAlert(localize: Localize, alert: SecurityAlert): void {
107144
const level = (alert.risk_level ?? "unknown").toUpperCase();
108145
emitBare(`[${level}] ${alert.risk_name ?? "-"} (${alert.alert_id})`);
109146
emitBare(
110-
` app: ${alert.app_name ?? "-"} asset: ${alert.asset_name ?? "-"} (${alert.asset_type ?? "-"})`,
147+
` ${localize(SECURITY_UI.app)}: ${alert.app_name ?? "-"} ${localize(SECURITY_UI.asset)}: ${alert.asset_name ?? "-"} (${alert.asset_type ?? "-"})`,
111148
);
112149
emitBare(
113-
` status: ${alert.status ?? "-"} source: ${alert.source ?? "-"} checked: ${formatSecurityTime(alert.check_time)}`,
150+
` ${localize(SECURITY_UI.status)}: ${alert.status ?? "-"} ${localize(SECURITY_UI.source)}: ${alert.source ?? "-"} ${localize(SECURITY_UI.checked)}: ${formatSecurityTime(alert.check_time)}`,
114151
);
115152
if (alert.risk_desc) emitBare(` ${alert.risk_desc}`);
116153
emitBare("");

0 commit comments

Comments
 (0)