Skip to content
Open
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
16 changes: 10 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@

### 新增

- 知识库支持下载原文、按原排版预览,聊天内可直接预览引用
- 技能可在技能包与专家工作区之间复制
- 记忆树支持手动新建与修正;Token 统计支持日期筛选与 Excel 导出
- 按用户限制存储根目录与 Token 配额;创建专家可带默认知识库与连接器
- 聊天支持 @ 子专家、技能斜杠插入,以及 ask_agent 独立对话线程
- 滴滴连接器;可选分段历史归档;便携运行时升级并支持 SQLite 备份
- 知识库支持下载上传原文;PDF / DOCX / PPTX / XLSX 在预览弹窗中按原排版查看;文档区支持拖拽上传(#591)
- 知识库 / 工作区 PDF 预览改用 PDF.js(react-pdf),替代浏览器内置 iframe 查看器
- 技能包支持将选定技能一次性复制到专家工作区,工作区技能也可推送到当前用户有写权限的技能包(#618)。
- 对话权限模式 Ask / Plan / Craft:turn 级 `conversation_mode`、`ConversationModeMiddleware`(工具过滤 + 系统提示 + denylist 硬拒;Ask 额外禁用 `write_todos`;Ask/Plan 拦截 `task` / `acp_runner` / `ask_agent`)、agent `default_conversation_mode` 与 `default_knowledge_base_ids`;Composer 模式菜单(默认 / 计划 / 问答);IM `/mode`(及 `/ask` `/plan` `/craft`)线程粘性覆盖(#616)
- 计划模式回合结束后展示「按计划执行 / 继续改计划」;确认后切到默认模式,计划 brief 经 `plan_brief` 由网关注入 system(不落对话气泡)(#616)
- 记忆树支持手动新建主题/记忆,以及编辑已有记忆(replace/supersede,保留变更历史)。
- Token 统计支持日期范围筛选(RangePicker 预设 + 自定义起止)并导出 Excel 用量明细(#154)
- Token 用量 Excel 含专家名称、中英列表头,以及按天/专家/模型汇总 sheet 与图表
- Token 用量 Excel 按报表规范打磨:冻结表头、千分位、合计公式行、筛选与仪表盘配色柱图
- 知识库卡片「更多」菜单可一键添加到当前会话

### 修复

Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/context/AgentContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export interface OctopAgent {
description: string | null;
persona_mbti: string | null;
default_model: string | null;
/** Composer default: ask | plan | craft (#616). Missing → craft. */
default_conversation_mode?: "ask" | "plan" | "craft" | null;
/** Composer / turn default knowledge bases when the turn omits an explicit list. */
default_knowledge_base_ids?: string[] | null;
system_prompt: string | null;
template_name: string | null;
state: "running" | "stopped" | "failed" | "starting" | "stopping" | string;
Expand Down
16 changes: 16 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,11 @@
"patchFailed": "Failed to save changes",
"defaultModelLabel": "Default Model",
"defaultModelPlaceholder": "Select a model",
"defaultConversationModeLabel": "Default conversation mode",
"defaultConversationModeHint": "Initial permission mode for new chats; change anytime in the composer.",
"defaultKnowledgeBasesLabel": "Default knowledge bases",
"defaultKnowledgeBasesHint": "Auto-attached when a new chat does not pick bases; still adjustable in the composer.",
"defaultKnowledgeBasesPlaceholder": "Select knowledge bases",
"skillPackagesLabel": "Optional Skill Packages",
"skillPackagesHint": "Mount packages at create time (local storage with root /).",
"skillPackagesUnsupportedHint": "This sandbox cannot mount skill-package directories directly. You can still use Copy skills to create independent workspace copies.",
Expand Down Expand Up @@ -1128,6 +1133,17 @@
"reasoningDisabled": "Off",
"reasoningEffort": "Thinking effort",
"reasoningAlways": "Always reasoning",
"conversationMode": "Mode",
"conversationModeAsk": "Ask",
"conversationModePlan": "Plan",
"conversationModeCraft": "Default",
"conversationModeStatusCraft": "Default mode — execute efficiently and finish the task.",
"conversationModeStatusPlan": "Plan mode — outline the approach first, then execute.",
"conversationModeStatusAsk": "Ask mode — answer and analyze without changing the environment.",
"planReadyTitle": "Ready to build?",
"planReadyHint": "Switch to Default mode and continue with this plan, or keep refining it.",
"planReadyExecute": "Execute plan",
"planReadyContinue": "Keep planning",
"errorOccurred": "An error occurred",
"imageLoadFailed": "Image failed to load",
"mediaLoadFailed": "Unable to load media",
Expand Down
16 changes: 16 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,11 @@
"agentDisable": "停用",
"defaultModelLabel": "默认模型",
"defaultModelPlaceholder": "选择一个模型",
"defaultConversationModeLabel": "默认对话模式",
"defaultConversationModeHint": "新对话的初始权限模式;可在聊天输入栏随时切换。",
"defaultKnowledgeBasesLabel": "默认知识库",
"defaultKnowledgeBasesHint": "新对话未手动选择知识库时自动附带;仍可在输入栏增减。",
"defaultKnowledgeBasesPlaceholder": "选择知识库",
"skillPackagesLabel": "可选技能包",
"skillPackagesHint": "创建时挂载选中的技能包(需本地存储,且根目录为 /)。",
"skillPackagesUnsupportedHint": "当前沙箱无法直接挂载技能包目录,但仍可使用“复制技能”在工作区创建独立副本。",
Expand Down Expand Up @@ -1128,6 +1133,17 @@
"reasoningDisabled": "关闭",
"reasoningEffort": "思考强度",
"reasoningAlways": "始终推理",
"conversationMode": "模式",
"conversationModeAsk": "问答",
"conversationModePlan": "计划",
"conversationModeCraft": "默认",
"conversationModeStatusCraft": "当前为默认模式,可高效执行并完成任务。",
"conversationModeStatusPlan": "当前为计划模式,先梳理方案与步骤,再动手执行。",
"conversationModeStatusAsk": "当前为问答模式,只回答与分析,不改动环境。",
"planReadyTitle": "按计划执行?",
"planReadyHint": "确认后将切换到默认模式并带着这份计划继续执行;也可继续改计划。",
"planReadyExecute": "按计划执行",
"planReadyContinue": "继续改计划",
"errorOccurred": "出现错误",
"imageLoadFailed": "图片加载失败",
"mediaLoadFailed": "无法加载媒体",
Expand Down
70 changes: 70 additions & 0 deletions dashboard/src/pages/Chat/chatInputCore.partial.less
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,76 @@
}
}

.planReadyCard {
max-width: var(--chat-column-max, 960px);
width: calc(100% - 48px);
margin: 0 auto 10px;
padding: 12px 14px;
border: 1px solid var(--fn-border-secondary, rgba(0, 0, 0, 0.08));
border-radius: 12px;
background: var(--fn-bg-elevated, #fff);

@media (max-width: 767px) {
width: calc(100% - 24px);
}
}

.planReadyTitle {
font-size: 14px;
font-weight: 600;
color: var(--fn-text-primary);
margin-bottom: 4px;
}

.planReadyHint {
margin: 0 0 10px;
font-size: 12px;
line-height: 1.45;
color: var(--fn-text-secondary);
}

.planReadyBrief {
margin: 0 0 12px;
padding: 8px 10px;
max-height: 160px;
overflow: auto;
border-radius: 8px;
border: 1px solid var(--fn-border-secondary, rgba(0, 0, 0, 0.06));
background: var(--fn-bg-layout, #f5f5f5);
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
color: var(--fn-text-secondary);
font-family: inherit;
}

.planReadyActions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

.planReadyPrimary,
.planReadySecondary {
border-radius: 8px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
}

.planReadyPrimary {
border: none;
background: var(--fn-brand, #1677ff);
color: #fff;
}

.planReadySecondary {
border: 1px solid var(--fn-border-secondary, rgba(0, 0, 0, 0.12));
background: transparent;
color: var(--fn-text-primary);
}

.askQuestionDockInner {
width: 100%;
max-width: var(--chat-column-max, 960px);
Expand Down
29 changes: 29 additions & 0 deletions dashboard/src/pages/Chat/chatInputPickers.partial.less
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,35 @@
padding-left: 6px;
}

.conversationModeMenuPanel {
width: 260px;
min-width: 260px;
max-height: none;
}

.conversationModeStatus {
padding: 8px 10px 6px;
color: var(--fn-text-tertiary);
font-size: 12px;
line-height: 1.45;
}

.conversationModeChoiceLabel {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;

svg {
display: block;
flex-shrink: 0;
}

span {
line-height: 1;
}
}

.reasoningMenuHeader {
display: flex;
align-items: center;
Expand Down
7 changes: 7 additions & 0 deletions dashboard/src/pages/Chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { KnowledgeBase } from "../../../api/modules/knowledgeBases";
import type { SkillSpec } from "../../Agent/Skills/useSkills";
import type { ChatAgentOption } from "./ExpertAgentAvatar";
import type { AgentSubagentSummary } from "../../../api/modules/subagents";
import type { ConversationMode } from "../utils/conversationMode";
import MentionPickerMenu from "./MentionPickerMenu";
import ChatInputPreviewBar from "./ChatInputPreviewBar";
import ChatInputActionsRow from "./ChatInputActionsRow";
Expand Down Expand Up @@ -86,6 +87,8 @@ interface ChatInputProps {
mode: "auto" | "enabled" | "disabled",
effort: string | null,
) => void;
conversationMode?: ConversationMode;
onConversationModeChange?: (mode: ConversationMode) => void;
availableConnectors?: {
mcp_server_name: string;
label: string;
Expand Down Expand Up @@ -140,6 +143,8 @@ const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
reasoningMode = "auto",
reasoningEffort = null,
onReasoningChange,
conversationMode = "craft",
onConversationModeChange,
availableConnectors,
selectedConnectors = [],
onConnectorsChange,
Expand Down Expand Up @@ -809,6 +814,8 @@ const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
reasoningMode={reasoningMode}
reasoningEffort={reasoningEffort}
onReasoningChange={onReasoningChange}
conversationMode={conversationMode}
onConversationModeChange={onConversationModeChange}
defaultModel={defaultModel}
availableConnectors={availableConnectors}
selectedConnectors={selectedConnectors}
Expand Down
86 changes: 61 additions & 25 deletions dashboard/src/pages/Chat/components/ChatInputActionsRow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, render, waitFor } from "@testing-library/react";
import { fireEvent, render, waitFor, screen } from "@testing-library/react";
import type { ReactElement } from "react";
import { MemoryRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import type { ResolvedModel } from "../../../api/types";
Expand All @@ -19,32 +20,38 @@ const models: ResolvedModel[] = [
},
];

const baseProps = {
isMobile: false,
isStreaming: false,
canSend: false,
text: "",
polishing: false,
uploading: false,
recording: false,
transcribing: false,
slashPickerGroups: null as null,
slashMenuItems: [] as [],
onSlashShortcutSelect: vi.fn(),
onFileSelect: vi.fn(),
onNewChat: vi.fn(),
onPolish: vi.fn(),
onToggleVoice: vi.fn(),
onCancel: vi.fn(),
onSubmit: vi.fn(),
};

function renderRow(ui: ReactElement) {
return render(<MemoryRouter>{ui}</MemoryRouter>);
}

describe("ChatInputActionsRow compact pickers", () => {
it("uses a popover instead of a full-width drawer on narrow desktop", async () => {
const { container } = render(
<MemoryRouter>
<ChatInputActionsRow
isMobile={false}
isStreaming={false}
canSend={false}
text=""
polishing={false}
uploading={false}
recording={false}
transcribing={false}
availableModels={models}
onModelChange={vi.fn()}
slashPickerGroups={null}
slashMenuItems={[]}
onSlashShortcutSelect={vi.fn()}
onFileSelect={vi.fn()}
onNewChat={vi.fn()}
onPolish={vi.fn()}
onToggleVoice={vi.fn()}
onCancel={vi.fn()}
onSubmit={vi.fn()}
/>
</MemoryRouter>,
const { container } = renderRow(
<ChatInputActionsRow
{...baseProps}
availableModels={models}
onModelChange={vi.fn()}
/>,
);

const modelButton = container
Expand All @@ -60,3 +67,32 @@ describe("ChatInputActionsRow compact pickers", () => {
expect(document.querySelector(".ant-drawer-content")).toBeNull();
});
});

describe("ChatInputActionsRow conversation mode (#616 M13)", () => {
it("shows status + Default/Plan/Ask labels and notifies on Ask select", async () => {
const onConversationModeChange = vi.fn();
renderRow(
<ChatInputActionsRow
{...baseProps}
conversationMode="craft"
onConversationModeChange={onConversationModeChange}
/>,
);

fireEvent.click(screen.getByTestId("conversation-mode-trigger"));

await waitFor(() => {
expect(
screen.getAllByTestId("conversation-mode-menu").length,
).toBeGreaterThan(0);
});
const menu = screen.getAllByTestId("conversation-mode-menu")[0]!;
expect(menu).toHaveTextContent("当前为默认模式");
expect(menu).toHaveTextContent("默认");
expect(menu).toHaveTextContent("计划");
expect(menu).toHaveTextContent("问答");

fireEvent.click(screen.getAllByTestId("conversation-mode-option-ask")[0]!);
expect(onConversationModeChange).toHaveBeenCalledWith("ask");
});
});
Loading
Loading