Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion apps/web/src/components/SettingsSections.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ it("lists audit events with tombstone labels and pages by the Server cursor", as
expect.anything(),
);
expect(view.container.querySelectorAll(".audit-list li")).toHaveLength(3);
expect(view.container.textContent).toContain("unknown kind");
// An unknown type shows its category in Chinese, never the internal code.
expect(view.container.textContent).toContain("其他事件");
expect(view.container.textContent).not.toContain("unknown kind");
expect(view.container.textContent).not.toContain("显示更早的记录");
} finally {
await view.unmount();
Expand Down Expand Up @@ -202,3 +204,39 @@ it.each([
await view.unmount();
}
});

it("names every audit event in Chinese, falling back to its category for an unknown type", async () => {
const event = (id: string, type: string, category: string) => ({
id,
type,
category,
createdAt: "2026-10-10T00:00:00.000Z",
details: {},
});
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
respond({
events: [
event("a", "AUTH_LOGIN_SUCCEEDED", "authentication"),
event("b", "MODEL_CONNECTION_CREATED", "settings"),
event("c", "AUTOMATION_PAUSED", "other"),
event("d", "AUTH_PASSKEY_ENROLLED", "authentication"),
],
}),
),
);
const view = await renderComponent(<AuditLogSettings />);
try {
await interact(() => undefined);
const titles = Array.from(view.container.querySelectorAll("strong")).map(
(item) => item.textContent,
);
expect(titles).toEqual(
expect.arrayContaining(["登录成功", "添加模型服务", "暂停例行任务", "登录事件"]),
);
expect(view.container.textContent).not.toMatch(/auth|passkey|automation/iu);
} finally {
await view.unmount();
}
});
31 changes: 30 additions & 1 deletion apps/web/src/components/SettingsSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,36 @@ export function ApprovalPolicySettings() {
);
}

// Every event type the Server writes (apps/server/src) has a label; an unknown type falls back to
// its category, so the Owner never sees an internal code such as "auth login succeeded".
const auditLabels: Record<string, string> = {
AUTH_LOGIN_SUCCEEDED: "登录成功",
AUTH_LOGIN_FAILED: "登录失败",
AUTH_LOGOUT: "退出登录",
OWNER_PASSWORD_CHANGED: "修改密码",
OWNER_SESSIONS_REVOKED: "退出其他设备",
SETTINGS_OWNER_UPDATED: "更改偏好设置",
SETTINGS_APPROVAL_UPDATED: "更改审批规则",
SETTINGS_STORAGE_UPDATED: "更改存储设置",
SETTINGS_TRASH_AUTO_PURGE_RUN: "自动清空回收站",
MODEL_CONNECTION_CREATED: "添加模型服务",
MODEL_CONNECTION_UPDATED: "更新模型服务",
MODEL_CONNECTION_DELETED: "删除模型服务",
EMPLOYEE_MODEL_UPDATED: "更改 Bot 模型",
EMPLOYEE_APPEARANCE_UPDATED: "更新 Bot 头像",
BOT_GREETING_STARTED: "Bot 开始打招呼",
BOT_GREETING_FAILED: "Bot 打招呼失败",
AUTOMATION_CREATED: "创建例行任务",
AUTOMATION_PAUSED: "暂停例行任务",
AUTOMATION_RESUMED: "恢复例行任务",
AUTOMATION_DELETED: "删除例行任务",
AUTOMATION_OCCURRENCE: "执行例行任务",
CHANNEL_ATTACHMENT_PURGED: "清理频道附件",
BROWSER_COMMAND: "浏览器操作",
BROWSER_CONTROL_STATE: "切换浏览器控制",
BROWSER_HOST_BOUND: "绑定浏览器主机",
BROWSER_TRANSPORT_ADMITTED: "允许浏览器连接",
BROWSER_TRANSPORT_CLAIMED: "接管浏览器连接",
CHANNEL_CREATED: "创建频道",
CHANNEL_RENAMED: "重命名频道",
CHANNEL_DELETED: "删除频道",
Expand Down Expand Up @@ -132,7 +161,7 @@ function auditTitle(event: AuditEvent) {
event.details.primaryBotId == null ? "未设置" : (event.details.to ?? "未知 Bot");
return `更改主 Bot:${previous} → ${current}`;
}
const label = auditLabels[event.type] ?? event.type.replaceAll("_", " ").toLocaleLowerCase();
const label = auditLabels[event.type] ?? `${auditCategoryLabels[event.category ?? "other"]}事件`;
const from = event.details.from;
const to = event.details.to;
if (typeof from === "string" && typeof to === "string") return `${label}:${from} → ${to}`;
Expand Down
44 changes: 44 additions & 0 deletions apps/web/src/components/Sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,50 @@
}
/* 主 Bot leads the list as a raised card (PrimaryBot artboard); the hover and selection rules
below still apply. */
/* Upgraded workspaces without a 主 Bot get a dismissible nudge where the crowned card would be. */
.sb .sb-primary-hint {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
margin: 0 0 6px;
padding: 10px 10px 10px 12px;
border-radius: var(--ob-radius-row);
box-shadow: inset 0 0 0 1px var(--ob-divider);
color: var(--ob-text-secondary);
}
.sb .sb-primary-hint .ob-crown-glyph {
margin-top: 3px;
}
.sb .sb-primary-hint span {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.sb .sb-primary-hint strong {
color: var(--ob-text);
font-size: 14px;
font-weight: 600;
}
.sb .sb-primary-hint small {
font-size: 12px;
line-height: 1.45;
}
.sb .sb-primary-hint button {
display: grid;
width: 22px;
height: 22px;
place-items: center;
border: 0;
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
}
.sb .sb-primary-hint button:hover {
background: var(--ob-fill-hover);
}
.sb .sb-primary {
padding-bottom: 2px;
}
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/components/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,46 @@ it("leads with the crowned 主 Bot and offers 设为主 Bot to the others", asyn
await view.unmount();
}
});

it("nudges an upgraded workspace without a 主 Bot until one is chosen or the hint is dismissed", async () => {
window.localStorage.removeItem("openbot.sidebar.primaryHintDismissed");
const props = {
bots,
channels,
runs: [],
ownerName: "Owner",
onSelectChannel: vi.fn(),
onSelectBot: vi.fn(),
onCreateBot: vi.fn(),
onCreateChannel: vi.fn(),
onSetPrimaryBot: vi.fn(async () => undefined),
};
const hint = (root: HTMLElement) => root.querySelector(".sb-primary-hint");
const none = await renderComponent(<Sidebar {...props} primaryBotId={null} />);
try {
expect(hint(none.container)?.textContent).toContain("选一个主 Bot");
await interact(() =>
none.container.querySelector<HTMLButtonElement>('[aria-label="不再提示"]')?.click(),
);
expect(hint(none.container)).toBeNull();
} finally {
await none.unmount();
}
// Dismissal is remembered on this device.
const again = await renderComponent(<Sidebar {...props} primaryBotId={null} />);
try {
expect(hint(again.container)).toBeNull();
} finally {
await again.unmount();
}
window.localStorage.removeItem("openbot.sidebar.primaryHintDismissed");
// A chosen 主 Bot, an older Server (undefined) or no Bots never show the hint.
for (const extra of [{ primaryBotId: "reviewer" }, { primaryBotId: undefined }, { bots: [] }]) {
const view = await renderComponent(<Sidebar {...props} primaryBotId={null} {...extra} />);
try {
expect(hint(view.container)).toBeNull();
} finally {
await view.unmount();
}
}
});
36 changes: 36 additions & 0 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ interface SidebarProps {
}

/** The shared sidebar of the design contract (docs/design/desktop-ui-2026-10/Sidebar.dc.html). */
const primaryHintKey = "openbot.sidebar.primaryHintDismissed";
function readPrimaryHintDismissed() {
try {
return window.localStorage.getItem(primaryHintKey) === "1";
} catch {
return false;
}
}

export function Sidebar({
creatingBot,
onWork,
Expand Down Expand Up @@ -177,6 +186,22 @@ export function Sidebar({
const primaryEntry = primaryBotId
? entries.find((entry) => entry.key === `bot:${primaryBotId}`)
: undefined;
// A workspace upgraded from before 主 Bot existed has none; nudge once, per device, until the
// Owner picks one or dismisses it. The Server never elects one silently during an upgrade.
const [primaryHintDismissed, setPrimaryHintDismissed] = useState(readPrimaryHintDismissed);
const suggestPrimary =
primaryBotId === null &&
bots.length > 0 &&
onSetPrimaryBot !== undefined &&
!primaryHintDismissed;
function dismissPrimaryHint() {
setPrimaryHintDismissed(true);
try {
window.localStorage.setItem(primaryHintKey, "1");
} catch {
// Storage can be unavailable (private window); the hint then hides for this session only.
}
}
const sections = arrangeSidebar(
primaryEntry ? entries.filter((entry) => entry !== primaryEntry) : entries,
organization,
Expand Down Expand Up @@ -440,6 +465,17 @@ export function Sidebar({
<>
{primaryEntry ? (
<div className="sb-section sb-primary">{renderRow(primaryEntry, "", true)}</div>
) : suggestPrimary ? (
<div className="sb-primary-hint" role="note">
<CrownGlyph />
<span>
<strong>选一个主 Bot</strong>
<small>没有 @ 人的消息会先交给它。右键任意 Bot,选「设为主 Bot」。</small>
</span>
<button type="button" aria-label="不再提示" onClick={dismissPrimaryHint}>
<ClearGlyph />
</button>
</div>
) : null}
{sections.map((section) => (
<div className="sb-section" key={section.group?.id ?? "ungrouped"}>
Expand Down
Loading