Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5a44c7e
Merge pull request #216 from TencentCloud/chore/sync-develop-after-ma…
github-actions[bot] Aug 9, 2026
cdc7aef
fix(dashboard): mobile personal settings Drawer and restore rose palette
jubaoliang-tencent Aug 9, 2026
af6100a
fix: compress oversized inbound images so vision models can see them …
chujieHong Aug 10, 2026
4aab7c1
Feature/migrate im channel cronjob from ace (#201)
liukewia Aug 10, 2026
37bf0f4
feat: 插件管理页支持从本地 ZIP 上传安装插件 (#226)
chujieHong Aug 10, 2026
860cdc3
feat: add per-user model and reasoning preferences (#221)
Pandakingxbc Aug 11, 2026
86fa910
feat: Docker sandbox, browser HITL stream, and gateway preemption (#227)
jubaoliang Aug 11, 2026
b5d46eb
feat: enforce password policy and polish account/subagent UX
jubaoliang-tencent Aug 11, 2026
01ba6de
fix: 保留技能 ZIP 导入时的空目录与根级技能的子文件夹
chujieHong Aug 11, 2026
22bff36
feat: 工作区支持 .docx 在线编辑(Markdown 往返,可扩展注册表)
chujieHong Aug 11, 2026
f3b2a62
chore: sync develop onto main after 0.9.21
jubaoliang-tencent Aug 11, 2026
b5843f1
ci: set git identity for Sync Main Into Develop
jubaoliang-tencent Aug 11, 2026
f3fcd9f
Merge pull request #237 from TencentCloud/chore/sync-develop-after-0.…
jubaoliang Aug 11, 2026
a04f203
chore: sync develop onto main after ci identity fix
jubaoliang-tencent Aug 11, 2026
569bcec
Merge pull request #239 from TencentCloud/chore/sync-develop-after-ci…
jubaoliang Aug 11, 2026
ce75527
fix(dashboard): stop PWA stale shell from white-screening after deploy
jubaoliang-tencent Aug 11, 2026
1db6af3
chore: release 0.9.22
jubaoliang-tencent Aug 11, 2026
cf3371b
fix(security): harden dashboard SPA fallback against path traversal
jubaoliang-tencent Aug 12, 2026
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

## [Unreleased]

## [0.9.22] - 2026-08-11

### 新增
- 专家工作区支持 `.docx` 在线编辑:以 Markdown 在 Monaco 中打开/保存,保存时转回 docx 覆盖原文件(标题/加粗/斜体/列表/表格保留,复杂格式简化);工作区新建的 `.docx` 即初始化为合法文档包,预览/编辑立即可用。基于可扩展注册表,新增可编辑后缀只需注册一个后端转换器类 + 前端注册表一行

### 变更
- 依赖新增 `python-docx==1.2.0`(含 `lxml`),用于工作区 `.docx` 的 Markdown 往返转换

### 修复
- 仪表盘发版后或长时间未打开时白屏:Service Worker 不再 Cache-First 钉死旧 `index.html`;hashed 资源改为 CacheFirst;入口脚本失败时清除 SW 缓存并自动刷新一次 (#236)

## [0.9.21] - 2026-08-11

### 新增
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<p align="center">
<a href="https://www.python.org/downloads/"><img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-blue?logo=python&logoColor=white" /></a>
<a href="https://github.com/TencentCloud/Octop/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green" /></a>
<a href="https://github.com/TencentCloud/Octop/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.9.20-orange" /></a>
<a href="https://github.com/TencentCloud/Octop/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.9.22-orange" /></a>
<a href="https://pypi.org/project/octop/"><img src="https://img.shields.io/pypi/v/octop" alt="PyPI" /></a>
<a href="https://github.com/astral-sh/ruff"><img alt="Code Style: Ruff" src="https://img.shields.io/badge/code%20style-ruff-000000?logo=ruff&logoColor=white" /></a>
<a href="https://github.com/TencentCloud/Octop"><img alt="GitHub stars" src="https://img.shields.io/github/stars/TencentCloud/Octop?style=social" /></a>
Expand Down
58 changes: 58 additions & 0 deletions dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,64 @@
<!-- Web App Manifest -->
<link rel="manifest" href="/manifest.json" />
<title>Octop</title>
<!--
Classic script (not type=module) so it runs even when hashed chunks 404.
Shares octop:chunk-reload with reloadOnStaleChunk.ts.
-->
<script>
(function () {
var KEY = "octop:chunk-reload";
function isAsset(url) {
return typeof url === "string" && url.indexOf("/assets/") !== -1;
}
function recover() {
try {
if (sessionStorage.getItem(KEY) === "1") {
sessionStorage.removeItem(KEY);
return;
}
sessionStorage.setItem(KEY, "1");
} catch (e) {}
var reload = function () {
location.reload();
};
var bust = Promise.resolve();
if ("serviceWorker" in navigator) {
bust = navigator.serviceWorker
.getRegistrations()
.then(function (regs) {
return Promise.all(
regs.map(function (r) {
return r.unregister();
}),
);
});
}
if ("caches" in window) {
bust = bust.then(function () {
return caches.keys().then(function (keys) {
return Promise.all(
keys.map(function (k) {
return caches.delete(k);
}),
);
});
});
}
bust.then(reload, reload);
}
window.addEventListener(
"error",
function (e) {
var t = e.target;
if (!t || !t.tagName) return;
if (t.tagName === "SCRIPT" && isAsset(t.src)) recover();
if (t.tagName === "LINK" && isAsset(t.href)) recover();
},
true,
);
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
7 changes: 5 additions & 2 deletions dashboard/src/pages/Agent/Workspace/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
readOnly?: boolean;
/** Override the Monaco language derived from the file extension. */
language?: string;
}

export default function CodeEditor({
path,
value,
onChange,
readOnly = false,
language,
}: CodeEditorProps) {
const language = getEditorLanguage(path);
const resolvedLanguage = language ?? getEditorLanguage(path);
const isDark = useIsDark();
const wrapRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<{
Expand Down Expand Up @@ -96,7 +99,7 @@ export default function CodeEditor({
<Suspense fallback={fallback}>
<MonacoEditor
height="100%"
language={language}
language={resolvedLanguage}
theme={isDark ? "vs-dark" : "light"}
value={value}
onChange={(v) => onChange(v ?? "")}
Expand Down
19 changes: 19 additions & 0 deletions dashboard/src/pages/Agent/Workspace/components/DocumentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default function DocumentPreview({
const [src, setSrc] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<"missing" | "error" | null>(null);
const [empty, setEmpty] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const objectUrlRef = useRef<string | undefined>(undefined);
const pptxViewerRef = useRef<{ destroy: () => void } | null>(null);
Expand All @@ -81,6 +82,7 @@ export default function DocumentPreview({
let cancelled = false;
setLoading(true);
setError(null);
setEmpty(false);
setSrc("");
pptxViewerRef.current?.destroy();
pptxViewerRef.current = null;
Expand Down Expand Up @@ -116,6 +118,13 @@ export default function DocumentPreview({
if (cancelled) return;

if (kind === "word") {
if (buf.byteLength === 0) {
// A 0-byte docx is a common artifact of creating a file in the
// workspace UI; show an empty state instead of failing the renderer.
if (!cancelled) setEmpty(true);
if (!cancelled) setLoading(false);
return;
}
const { renderAsync } = await import("docx-preview");
if (containerRef.current && !cancelled) {
await renderAsync(buf, containerRef.current, undefined, {
Expand Down Expand Up @@ -183,6 +192,16 @@ export default function DocumentPreview({
};
}, [agentId, path, kind, fromWorkspace]);

if (empty) {
return (
<div className={styles.viewerEmpty}>
<p style={{ color: "var(--fn-text-tertiary)", margin: 0 }}>
{t("workspace.emptyFile", "文件为空")}
</p>
</div>
);
}

if (error) {
return (
<div className={styles.viewerEmpty}>
Expand Down
23 changes: 19 additions & 4 deletions dashboard/src/pages/Agent/Workspace/components/FileViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import MediaPreview from "./MediaPreview";
import CodeEditor from "./CodeEditor";
import DocumentPreview from "./DocumentPreview";
import { getMediaKind } from "../utils/mediaKind";
import { getDocKind } from "../utils/docKind";
import {
getDocKind,
getEditableDocLanguage,
isEditableDoc,
} from "../utils/docKind";
import { isProbablyText } from "../utils/fileKind";
import styles from "../index.module.less";

Expand Down Expand Up @@ -61,7 +65,9 @@ export default function FileViewer({
const mediaKind = getMediaKind(path);
const docKind = getDocKind(path);
const previewKind = getPreviewKind(path);
const showEditButton = isProbablyText(path);
const editableDoc = isEditableDoc(path);
const showEditButton = isProbablyText(path) || editableDoc;
const editingDoc = editMode && editableDoc;

if (mediaKind) {
return (
Expand All @@ -75,7 +81,7 @@ export default function FileViewer({
);
}

if (docKind) {
if (docKind && !editingDoc) {
return (
<DocumentPreview
key={`${path}:${refreshToken}`}
Expand Down Expand Up @@ -106,7 +112,16 @@ export default function FileViewer({
}

if (editMode) {
return <CodeEditor path={path} value={value} onChange={onChange} />;
return (
<CodeEditor
path={path}
value={value}
onChange={onChange}
language={
editableDoc ? getEditableDocLanguage(path) ?? "markdown" : undefined
}
/>
);
}

if (value === "") {
Expand Down
64 changes: 53 additions & 11 deletions dashboard/src/pages/Agent/Workspace/components/WorkspaceDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
defaultPreviewMode,
} from "./FilePreview";
import { getMediaKind } from "../utils/mediaKind";
import { getDocKind } from "../utils/docKind";
import { getDocKind, isEditableDoc } from "../utils/docKind";
import { isProbablyText } from "../utils/fileKind";
import styles from "../index.module.less";

Expand Down Expand Up @@ -872,20 +872,60 @@ export default function WorkspaceDrawer({
</div>
);

const save = async () => {
const handleEdit = async () => {
if (!agentId || !selectedKey) return;
const { path } = pathFromKey(selectedKey);
setSaving(true);
if (!isEditableDoc(path)) {
setEditMode(true);
return;
}
setFileLoading(true);
try {
await request(
const r = await request<{ content: string }>(
withFromWorkspace(
`/agents/${agentId}/workspace/file?path=${encodeURIComponent(path)}`,
`/agents/${agentId}/workspace/doc?path=${encodeURIComponent(path)}`,
),
{
method: "PUT",
body: JSON.stringify({ content }),
},
);
setContent(r.content);
setEditMode(true);
} catch (err: unknown) {
message.error(
(err instanceof Error ? err.message : String(err)) ||
t("workspace.readFailed", "读取失败"),
);
} finally {
setFileLoading(false);
}
};

const save = async () => {
if (!agentId || !selectedKey) return;
const { path } = pathFromKey(selectedKey);
setSaving(true);
try {
if (isEditableDoc(path)) {
await request(
withFromWorkspace(
`/agents/${agentId}/workspace/doc?path=${encodeURIComponent(path)}`,
),
{
method: "PUT",
body: JSON.stringify({ content }),
},
);
} else {
await request(
withFromWorkspace(
`/agents/${agentId}/workspace/file?path=${encodeURIComponent(
path,
)}`,
),
{
method: "PUT",
body: JSON.stringify({ content }),
},
);
}
message.success(t("workspace.saved", "已保存"));
setEditMode(false);
} catch (err: unknown) {
Expand Down Expand Up @@ -1010,7 +1050,9 @@ export default function WorkspaceDrawer({
const docKind =
selected && !selected.is_dir ? getDocKind(selected.path) : null;
const showEditButton =
selected && !selected.is_dir && isProbablyText(selected.path);
selected &&
!selected.is_dir &&
(isProbablyText(selected.path) || isEditableDoc(selected.path));
const showPreviewToggle = previewKind !== null && !editMode && content !== "";

const drawerTitle = (
Expand Down Expand Up @@ -1443,7 +1485,7 @@ export default function WorkspaceDrawer({
<button
type="button"
className={styles.toolBtn}
onClick={() => setEditMode(true)}
onClick={() => void handleEdit()}
>
<Pencil size={14} />
{t("common.edit")}
Expand Down
35 changes: 35 additions & 0 deletions dashboard/src/pages/Agent/Workspace/utils/docKind.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { getDocKind, getEditableDocLanguage, isEditableDoc } from "./docKind";

describe("isEditableDoc", () => {
it("accepts .docx in any case and nesting", () => {
expect(isEditableDoc("report.docx")).toBe(true);
expect(isEditableDoc("REPORT.DOCX")).toBe(true);
expect(isEditableDoc("/work/dir/sub/report.docx")).toBe(true);
});

it("rejects other extensions and extension-less paths", () => {
expect(isEditableDoc("report.doc")).toBe(false);
expect(isEditableDoc("report.pdf")).toBe(false);
expect(isEditableDoc("report.md")).toBe(false);
expect(isEditableDoc("report")).toBe(false);
});
});

describe("getEditableDocLanguage", () => {
it("maps editable docs to markdown", () => {
expect(getEditableDocLanguage("/a.docx")).toBe("markdown");
});

it("returns null for non-editable files", () => {
expect(getEditableDocLanguage("/a.doc")).toBeNull();
expect(getEditableDocLanguage("/a.md")).toBeNull();
});
});

describe("getDocKind (regression)", () => {
it("still classifies word docs as word", () => {
expect(getDocKind("/a.docx")).toBe("word");
expect(getDocKind("/a.doc")).toBe("word");
});
});
22 changes: 22 additions & 0 deletions dashboard/src/pages/Agent/Workspace/utils/docKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,25 @@ export function getDocKind(path: string): DocKind | null {
if (ext === "ppt") return "ppt";
return null;
}

/**
* Documents editable online via a Markdown round-trip, mapped to the Monaco
* language used while editing. Mirrors the backend converter registry in
* ``src/octop/infra/utils/doc_edit.py`` — to enable a new extension, add it
* here AND register a matching converter on the backend.
*/
const EDITABLE_DOCS: Record<string, string> = {
docx: "markdown",
};

/** Whether a workspace file can be edited online (Markdown round-trip). */
export function isEditableDoc(path: string): boolean {
const ext = path.split(".").pop()?.toLowerCase() ?? "";
return Object.prototype.hasOwnProperty.call(EDITABLE_DOCS, ext);
}

/** Monaco language for an editable document, or ``null`` when not editable. */
export function getEditableDocLanguage(path: string): string | null {
const ext = path.split(".").pop()?.toLowerCase() ?? "";
return EDITABLE_DOCS[ext] ?? null;
}
Loading
Loading