Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ src/octop/dashboard/assets/*
!src/octop/dashboard/__init__.py
!src/octop/dashboard/.gitkeep

# 美团生活助手专家:随包分发 pt-passport 运行时(Docker 镜像无 npm,
# 依赖此预装产物执行认证;包内文件由来源 tgz 固定,不会被误提交污染)
# 注意:git 不会进入被忽略的目录,因此必须先反排除 node_modules 目录本身
!src/octop/infra/agents/experts/library/meituan-living-assistant/
!src/octop/infra/agents/experts/library/meituan-living-assistant/skills/
!src/octop/infra/agents/experts/library/meituan-living-assistant/skills/meituan-deals/
!src/octop/infra/agents/experts/library/meituan-living-assistant/skills/meituan-deals/scripts/
!src/octop/infra/agents/experts/library/meituan-living-assistant/skills/meituan-deals/scripts/node_modules/
!src/octop/infra/agents/experts/library/meituan-living-assistant/skills/meituan-deals/scripts/node_modules/**

# Local databases
*.sqlite
*.sqlite-journal
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

## [Unreleased]

### 新增

- 智能体状态接口返回 `memory_maintenance`(queued / pruning / compacting)。聊天页显示阶段进度条,整理本库时暂停发送;专家卡片显示「整理记忆」标签。

### 修复

- 插件工具使用中文等非 ASCII 名称时 LLM 调用失败:主流 API 要求工具名匹配 `^[a-zA-Z0-9_-]{1,64}$`,现自动将非法名称转写为合法拼音名(`pypinyin` 缺失时退回下划线替换),冲突追加 `_2`/`_3` 后缀,并在工具描述前缀 `[原名: …]` 保留原名映射;`config_json.plugins` 配置键与插件内部仍使用原始名称,路由不受影响
- 修复聊天页在"生成中"时于输入框持续打字导致消息列表上下轻微抖动的问题:输入框高度测量改为在离屏克隆节点上进行,不再瞬态改变页面布局

## [0.9.24] - 2026-08-15

### 新增
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AgentProvider } from "./context/AgentContext";
import { VoiceOutputProvider } from "./context/VoiceOutputContext";
import { useIsMobile } from "./hooks/useIsMobile";
import { useUnauthorizedRedirect } from "./hooks/useUnauthorizedRedirect";
import { ANTD_BRAND_TOKENS } from "./styles/themePalettes";
import { brandTokensFor } from "./styles/themePalettes";
import "./styles/theme-vars.css";
import "./styles/layout.css";
import "./styles/form-override.css";
Expand All @@ -28,10 +28,10 @@ const GlobalStyle = createGlobalStyle`
`;

function ThemedApp() {
const { isDark, palette } = useTheme();
const { isDark, palette, customColor } = useTheme();
const { t } = useTranslation();
const isMobile = useIsMobile();
const brandTokens = ANTD_BRAND_TOKENS[palette][isDark ? "dark" : "light"];
const brandTokens = brandTokensFor(palette, isDark, customColor);

useUnauthorizedRedirect();

Expand Down
11 changes: 11 additions & 0 deletions dashboard/src/api/modules/knowledgeBases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ export const knowledgeBasesApi = {
"/knowledge-bases/onnx-download-status",
),

testOnnx: (model: string) =>
request<{
ok: boolean;
latency_ms?: number | null;
dim?: number | null;
error?: string | null;
}>("/knowledge-bases/onnx-test", {
method: "POST",
body: JSON.stringify({ model }),
}),

activateOnnx: (model: string) =>
request<{ enabled: boolean; model: string; ready: boolean }>(
"/knowledge-bases/onnx-activate",
Expand Down
11 changes: 11 additions & 0 deletions dashboard/src/api/modules/memoryDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ export interface RejectCandidateResponse {
status: "rejected";
}

export interface LastExtractRun {
timestamp?: string;
session_id?: string | null;
quiet?: boolean;
note?: string;
events_extracted?: number;
candidates?: number;
failure_reason?: string | null;
}

export interface StatsCounts {
raw_events: number;
atoms: number;
Expand All @@ -247,6 +257,7 @@ export interface StatsCounts {
atoms_delta_7d: number;
entities_delta_7d: number;
episodes_delta_7d: number;
last_extract_run?: LastExtractRun | null;
}

export interface StatsAtomKindsResponse {
Expand Down
19 changes: 18 additions & 1 deletion dashboard/src/api/modules/voice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { request, requestBlob, requestUpload } from "../request";
import { request, requestBlob, requestStream, requestUpload } from "../request";

export interface VoicePreset {
id: string;
Expand Down Expand Up @@ -66,6 +66,23 @@ export const voiceApi = {
...(provider ? { provider } : {}),
}),
}),
/**
* Streamed variant for low-latency playback. The server flushes chunks as
* they arrive (MiMo streams live WAV); other providers stream MP3, in
* which case the buffered `synthesize` path is used instead.
*/
synthesizeStream: (
text: string,
provider?: string,
): Promise<{ contentType: string; body: ReadableStream<Uint8Array> }> =>
requestStream("/voice/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text,
...(provider ? { provider } : {}),
}),
}),
createProvider: (body: {
name: string;
kind: string;
Expand Down
41 changes: 41 additions & 0 deletions dashboard/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,47 @@ export async function probeAuthResource(
}
}

/**
* POST JSON and hand back the response body as a byte stream (chunked TTS).
* Mirrors requestBlob()'s auth/setup/401 handling but never buffers.
*/
export async function requestStream(
path: string,
options: RequestInit = {},
): Promise<{ contentType: string; body: ReadableStream<Uint8Array> }> {
const url = getApiUrl(path);
const headers = buildAuthHeaders(path);
const response = await fetch(url, {
...options,
headers: { ...headers, ...(options.headers as Record<string, string>) },
});

if (await check503ForSetupRequired(path, response)) {
throw new Error("Setup required — redirecting to /setup");
}

await throwIfUnauthorized(path, response);
applyRenewedAccessToken(response);

if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`Request failed: ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}

if (!response.body) {
throw new Error("Empty stream from server");
}

return {
contentType: response.headers.get("content-type") || "",
body: response.body,
};
}

export type UploadProgressHandler = (percent: number) => void;

/**
Expand Down
45 changes: 41 additions & 4 deletions dashboard/src/components/ExpertColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { Tooltip } from "antd";
import { ColorPicker, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
import type { AggregationColor } from "antd/es/color-picker/color";
import {
DEFAULT_CUSTOM_COLOR,
PALETTE_SWATCH,
VALID_PALETTES,
type ThemePalette,
} from "../styles/themePalettes";
import styles from "./PaletteSwitcher.module.less";

interface ExpertColorPickerProps {
value: ThemePalette;
onChange: (palette: ThemePalette) => void;
/** Curated palette key, or an arbitrary hex string for a custom color. */
value: string;
onChange: (value: string) => void;
}

/** Curated 8-swatch picker for expert/agent accent color (list cards). */
function isCurated(value: string): value is ThemePalette {
return (VALID_PALETTES as string[]).includes(value);
}

/**
* Curated 8-swatch picker for expert/agent accent color, plus a custom
* color swatch backed by the Ant Design color picker (palette + hex input).
* The onChange callback receives a palette key or a hex string.
*/
export default function ExpertColorPicker({
value,
onChange,
}: ExpertColorPickerProps) {
const { t } = useTranslation();
const curated = isCurated(value);

return (
<div className={styles.picker} role="group" aria-label={t("experts.color")}>
Expand All @@ -42,6 +54,31 @@ export default function ExpertColorPicker({
</Tooltip>
);
})}
<Tooltip title={t("experts.customColor")} mouseEnterDelay={0.35}>
<span
className={`${styles.option} ${!curated ? styles.active : ""}`}
aria-label={t("experts.customColor")}
aria-pressed={!curated}
role="button"
>
<ColorPicker
value={curated ? undefined : value}
onChangeComplete={(color: AggregationColor) => {
onChange(color.toHexString());
}}
disabledAlpha
>
<span
className={`${styles.swatch} ${styles.customSwatch}`}
style={!curated ? { backgroundColor: value } : undefined}
aria-hidden
/>
</ColorPicker>
</span>
</Tooltip>
{!curated && value !== DEFAULT_CUSTOM_COLOR && (
<span className={styles.customHex}>{value.toUpperCase()}</span>
)}
</div>
);
}
29 changes: 29 additions & 0 deletions dashboard/src/components/PaletteSwitcher.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,32 @@
0 0 0 2px var(--fn-bg-elevated),
0 0 0 4px var(--fn-text-primary);
}

/* Custom color swatch: a rainbow conic gradient hints "pick any color".
When the custom palette is active the gradient is dropped so the inline
background-color (the chosen hex) shows through. */
.customSwatch {
background-image: conic-gradient(
#f59e0b,
#ef4444,
#ec4899,
#8b5cf6,
#3b82f6,
#06b6d4,
#10b981,
#f59e0b
);
}

.active .customSwatch {
background-image: none;
}

/* Hex readout next to the custom swatch when a non-curated color is active. */
.customHex {
font-size: 11px;
font-family: var(--fn-font-mono, monospace);
color: var(--fn-text-tertiary);
align-self: center;
user-select: text;
}
34 changes: 32 additions & 2 deletions dashboard/src/components/PaletteSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Tooltip } from "antd";
import { ColorPicker, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
import type { AggregationColor } from "antd/es/color-picker/color";
import { useTheme } from "../context/ThemeContext";
import { PALETTE_SWATCH, VALID_PALETTES } from "../styles/themePalettes";
import styles from "./PaletteSwitcher.module.less";

/**
* Curated 8-swatch brand palette picker plus a custom color swatch.
* The custom swatch opens the Ant Design color picker (palette + hex input);
* picking a color switches the active brand palette to "custom".
*/
export default function PaletteSwitcher() {
const { palette, setPalette } = useTheme();
const { palette, setPalette, customColor, setCustomColor } = useTheme();
const { t } = useTranslation();

const isCustom = palette === "custom";

return (
<div
className={styles.picker}
Expand Down Expand Up @@ -35,6 +43,28 @@ export default function PaletteSwitcher() {
</Tooltip>
);
})}
<Tooltip title={t("header.palette.custom")} mouseEnterDelay={0.35}>
<span
className={`${styles.option} ${isCustom ? styles.active : ""}`}
aria-label={t("header.palette.custom")}
aria-pressed={isCustom}
role="button"
>
<ColorPicker
value={isCustom ? customColor : undefined}
onChangeComplete={(color: AggregationColor) => {
setCustomColor(color.toHexString());
}}
disabledAlpha
>
<span
className={`${styles.swatch} ${styles.customSwatch}`}
style={isCustom ? { backgroundColor: customColor } : undefined}
aria-hidden
/>
</ColorPicker>
</span>
</Tooltip>
</div>
);
}
Loading
Loading