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
81 changes: 18 additions & 63 deletions .github/workflows/fnos-build-fpk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ name: Build Octop FPK
#
# Jobs:
# version → ensure-image → fpk(Docker 版 .fpk,仅打包 compose)
# ↘ native(本地版 .fpk)→ release(固定版 + 滚动 latest
# ↘ native(本地版 .fpk)→ release(按版本号发布 fnos-<ver>

on:
workflow_dispatch:
Expand All @@ -32,9 +32,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
iter: ${{ steps.iter.outputs.iter }}
tag: ${{ steps.iter.outputs.tag }}
pkg_ver: ${{ steps.iter.outputs.pkg_ver }}
tag: ${{ steps.ver.outputs.tag }}
pkg_ver: ${{ steps.ver.outputs.pkg_ver }}
image: ${{ steps.ver.outputs.image }}
steps:
- uses: actions/checkout@v4
Expand All @@ -49,24 +48,9 @@ jobs:
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"

- name: Compute iteration
id: iter
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VER=${{ steps.ver.outputs.version }}
# 查询已存在的 fnos-vanilla-<ver>-NN releases,取最大序号 +1。
echo "[version] existing releases matching fnos-vanilla-${VER}-NN:"
gh release list --repo "${{ github.repository }}" --limit 100 --json tagName | python3 -c 'import sys,json; [print(" "+r.get("tagName","")) for r in json.load(sys.stdin)]' || true
EXISTING=$(gh release list --repo "${{ github.repository }}" --limit 100 --json tagName | python3 -c 'import sys,json,re; d=json.load(sys.stdin); pat=re.compile(r"fnos-vanilla-[0-9]+\.[0-9]+\.[0-9]+-([0-9]+)$"); nums=[int(pat.match(r.get("tagName","")).group(1)) for r in d if pat.match(r.get("tagName",""))]; print(max(nums) if nums else 0)')
MAX=${EXISTING:-0}
NEXT=$((MAX + 1))
ITER=$(printf "%02d" "$NEXT")
echo "iter=$ITER" >> "$GITHUB_OUTPUT"
echo "tag=fnos-vanilla-${VER}-${ITER}" >> "$GITHUB_OUTPUT"
echo "pkg_ver=${VER}-${ITER}" >> "$GITHUB_OUTPUT"
echo "[version] VER=$VER, EXISTING_MAX=$MAX, ITER=$ITER, TAG=fnos-vanilla-${VER}-${ITER}"
echo "tag=fnos-${VER}" >> "$GITHUB_OUTPUT"
echo "pkg_ver=${VER}" >> "$GITHUB_OUTPUT"
echo "[version] VER=$VER, TAG=fnos-${VER}"

# 复用 docker-publish 已推送的镜像,不在此重新 build/push。
ensure-image:
Expand Down Expand Up @@ -114,15 +98,14 @@ jobs:

- name: Build Docker FPK
env:
FPK_NAME_PREFIX: Fnos-octop-vanilla
FPK_ITER: ${{ needs.version.outputs.iter }}
FPK_NAME_PREFIX: Octop-fnos
run: bash scripts/build-fpk.sh docker

- name: Upload docker fpk artifact
uses: actions/upload-artifact@v4
with:
name: docker-fpk
path: dist/Fnos-octop-vanilla-*.fpk
path: dist/Octop-fnos-docker-*.fpk
if-no-files-found: error

native:
Expand Down Expand Up @@ -219,19 +202,18 @@ jobs:
- name: Build native FPK
id: build_native
env:
FPK_NAME_PREFIX: Fnos-octop-vanilla
FPK_ITER: ${{ needs.version.outputs.iter }}
FPK_NAME_PREFIX: Octop-fnos
run: bash scripts/build-fpk.sh native

- name: Show native fpk size
run: ls -lh dist/Fnos-octop-vanilla-native-*.fpk
run: ls -lh dist/Octop-fnos-native-*.fpk

- name: Upload native fpk artifact
id: upload_native
uses: actions/upload-artifact@v4
with:
name: native-fpk
path: dist/Fnos-octop-vanilla-native-*.fpk
path: dist/Octop-fnos-native-*.fpk
if-no-files-found: error

- name: Upload native install log (for debugging)
Expand Down Expand Up @@ -279,11 +261,10 @@ jobs:
id: changelog
run: |
VER="${{ needs.version.outputs.version }}"
ITER="${{ needs.version.outputs.iter }}"
IMAGE="${{ needs.version.outputs.image }}"
PREV_TAG=""
for t in $(git tag --list "fnos-vanilla-*" --sort=-creatordate); do
[ "$t" = "fnos-vanilla-latest" ] && continue
for t in $(git tag --list "fnos-*" --sort=-creatordate); do
[ "$t" = "fnos-latest" ] && continue
if git merge-base --is-ancestor "$t" HEAD 2>/dev/null; then
PREV_TAG="$t"
break
Expand All @@ -294,13 +275,13 @@ jobs:
echo "## 安装说明"
echo ""
echo "1. 飞牛应用中心 → 设置 → 手动安装应用,选择对应 .fpk。"
echo "2. Docker 版 **Fnos-octop-vanilla-${VER}-${ITER}.fpk**(约 80KB):飞牛自动从 GHCR 拉取 \`${IMAGE}:latest\`。"
echo "3. 本地版 **Fnos-octop-vanilla-native-${VER}-${ITER}.fpk**(约 200MB):非 Docker,原生运行在飞牛主机;安装时自动关联系统 Python 3.12 开发工具;浏览器、远程桌面等附加组件在 Octop 应用内按需安装。"
echo "2. Docker 版 **Octop-fnos-docker-${VER}.fpk**(约 80KB):飞牛自动从 GHCR 拉取 \`${IMAGE}:latest\`。"
echo "3. 本地版 **Octop-fnos-native-${VER}.fpk**(约 200MB):非 Docker,原生运行在飞牛主机;安装时自动关联系统 Python 3.12 开发工具;浏览器、远程桌面等附加组件在 Octop 应用内按需安装。"
echo ""
echo "---"
echo "固定版本请从本 Release 附件下载;最新滚动版见 [fnos-vanilla-latest](https://github.com/${{ github.repository }}/releases/tag/fnos-vanilla-latest)。"
echo "按版本号下载对应 .fpk 即可(如 \`Octop-fnos-docker-${VER}.fpk\` / \`Octop-fnos-native-${VER}.fpk\`)。"
echo ""
echo "## 本次更新(v${VER}-${ITER})"
echo "## 本次更新(v${VER})"
echo ""
if [ -n "$PREV_TAG" ]; then
git log --oneline --no-merges "$PREV_TAG"..HEAD | head -30
Expand All @@ -310,7 +291,7 @@ jobs:
} > /tmp/release-body.md
cat /tmp/release-body.md

- name: Publish fixed release
- name: Publish versioned release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
Expand All @@ -322,31 +303,5 @@ jobs:
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Delete rolling release if exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG=fnos-vanilla-latest
REL_ID=$(curl -sS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG \
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$REL_ID" ]; then
curl -sS -X DELETE -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
https://api.github.com/repos/${{ github.repository }}/releases/$REL_ID || true
fi
curl -sS -X DELETE -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$TAG || true

- name: Publish rolling release
uses: softprops/action-gh-release@v2
with:
tag_name: fnos-vanilla-latest
target_commitish: ${{ github.sha }}
name: Octop (FnOS) latest
body_path: /tmp/release-body.md
files: dist/*.fpk
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@

## [Unreleased]

## [0.9.31] - 2026-09-01

### 新增

- 聊天流式输出时默认展开思考/工具过程,回答完成后收起(历史记录仍收起)
- 聊天侧栏与 @ 选择仅展示运行中的专家
- 默认专家与控制台创建的专家一样使用家目录存储;聊天待办按计划顺序展示
- 工作台浏览器可显式结束本地 Chrome 进程;空闲超时后也会回收(登录态仍保留在磁盘 profile)
- 桌面端右上角窗口控制(最小化 / 最大化 / 关闭进托盘)

### 修复

- 过期 hashed 静态资源返回 404,避免升级后桌面壳加载旧脚本
- 桌面无边框窗口改为 CSS/JS 拖拽,去掉会挡住右上角按钮的 `InvisibleTitleBarHeight`
- macOS 点击程序坞只恢复主窗口,不再同时弹出托盘设置窗
- 专家详情技能/子智能体卡片:图标在标题左侧,状态或 id 在标题右侧,描述单独一行
- 桌面端等待本机服务就绪失败时改为中英文说明(跟随桌面语言设置),不再显示 `/api/health` 英文报错
- 桌面启动页补齐右上角窗口按钮,并收紧启动/失败状态的展示
- 桌面托盘设置窗去掉多余空白,右上角只保留关闭;macOS 单击菜单栏图标也会弹出设置

## [0.9.30] - 2026-08-31

### 新增
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.30-orange" /></a>
<a href="https://github.com/TencentCloud/Octop/releases"><img alt="Version" src="https://img.shields.io/badge/version-0.9.31-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
6 changes: 6 additions & 0 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ConfigProvider, theme as antdTheme } from "antd";
import zhCN from "antd/locale/zh_CN";
import enUS from "antd/locale/en_US";
import { useEffect } from "react";
import DesktopWindowControls from "./components/DesktopWindowControls";
import { useDesktopChrome } from "./hooks/useDesktopChrome";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useTranslation } from "react-i18next";
import MainLayout from "./layouts/MainLayout";
Expand Down Expand Up @@ -36,6 +38,7 @@ function ThemedApp() {
const { isDark, palette, customColor } = useTheme();
const { t, i18n } = useTranslation();
const isMobile = useIsMobile();
const desktopChrome = useDesktopChrome();
const brandTokens = brandTokensFor(palette, isDark, customColor);
// Make antd built-ins (Popconfirm OK/Cancel, Modal default footer, Empty,
// Pagination, DatePicker, Table… ) follow the current UI language.
Expand Down Expand Up @@ -110,6 +113,9 @@ function ThemedApp() {
return (
<ConfigProvider theme={themeConfig} prefixCls="octop" locale={antdLocale}>
<AntdAppProvider>
{desktopChrome ? (
<DesktopWindowControls chrome={desktopChrome} />
) : null}
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/login/oidc/complete" element={<OidcComplete />} />
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/api/modules/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ export const browserApi = {
},
),

/** Stop the local Chrome process. Login cookies stay in the on-disk profile. */
shutdown: (profile?: string) => {
const path = profile
? `/browser/shutdown?profile=${encodeURIComponent(profile)}`
: "/browser/shutdown";
return request<{ ok: boolean; profile: string }>(path, { method: "POST" });
},

// -- Browser stream (WebSocket CDP screencast, ~10 fps) --

/**
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/api/modules/publishedExperts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export interface InstalledPublishedExpert {
agent_id: string;
name: string;
description: string | null;
state: string;
published_expert_id: string;
bootstrap_pending: boolean;
}

const publishedPath = (expertId: string) =>
Expand Down
25 changes: 22 additions & 3 deletions dashboard/src/components/AgentProfileDrawer.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,34 @@
font-family: inherit;
}

.skillItemRow {
.entityCard {
display: flex;
align-items: center;
gap: 12px;
flex-direction: column;
gap: 2px;
width: 100%;
min-width: 0;
padding: 10px 12px;
}

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

> :last-child {
flex-shrink: 0;
}
}

.entityCardTitle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.sectionTitleRow {
display: flex;
align-items: center;
Expand Down
63 changes: 62 additions & 1 deletion dashboard/src/components/AgentProfileDrawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,23 @@ import type { OctopAgent } from "../context/AgentContext";
vi.mock("../api/request", () => ({
request: vi.fn(async (path: string) => {
if (path.endsWith("/skills")) {
return [{ slug: "demo-skill", name: "Demo skill", kind: "workspace" }];
return [
{
slug: "demo-skill",
name: "Demo skill",
description: "A demo skill",
kind: "workspace",
emoji: "🎯",
enabled: true,
},
{
slug: "hub-skill",
name: "Hub skill",
kind: "workspace",
icon_url: "https://cdn.example/skill.png",
enabled: false,
},
];
}
return { name: "Demo agent", description: "Description" };
}),
Expand All @@ -19,6 +35,7 @@ vi.mock("../api/modules/subagents", () => ({
slug: "demo-subagent",
name: "Demo subagent",
description: "Subagent description",
emoji: "🤖",
},
]),
}));
Expand Down Expand Up @@ -101,4 +118,48 @@ describe("AgentProfileDrawer sections", () => {
expect(skillHeader).toHaveAttribute("aria-expanded", "true");
expect(subagentHeader).toHaveAttribute("aria-expanded", "true");
});

it("shows skill icons the same way the subagent list does", async () => {
render(
<I18nextProvider i18n={i18n}>
<AgentProfileDrawer open agent={agent} onClose={vi.fn()} />
</I18nextProvider>,
);

await waitFor(() => {
expect(screen.getByText("🎯")).toBeInTheDocument();
expect(screen.getByText("🤖")).toBeInTheDocument();
});

const hubIcon = screen.getByRole("img", { name: "Hub skill" });
expect(hubIcon).toHaveAttribute("src", "https://cdn.example/skill.png");
});

it("puts skill status and subagent id on the title row above the description", async () => {
render(
<I18nextProvider i18n={i18n}>
<AgentProfileDrawer open agent={agent} onClose={vi.fn()} />
</I18nextProvider>,
);

const skillTitle = await screen.findByText("Demo skill");
const skillStatus = screen.getByText("common.enabled");
const skillDesc = screen.getByText("A demo skill");
const skillIcon = screen.getByText("🎯");
const skillHeader = skillTitle.parentElement;
expect(skillHeader).toContainElement(skillIcon);
expect(skillHeader).toContainElement(skillStatus);
expect(skillHeader).not.toContainElement(skillDesc);
expect(skillHeader?.nextElementSibling).toBe(skillDesc);

const subTitle = screen.getByText("Demo subagent");
const subId = screen.getByText("demo-subagent");
const subDesc = screen.getByText("Subagent description");
const subIcon = screen.getByText("🤖");
const subHeader = subTitle.parentElement;
expect(subHeader).toContainElement(subIcon);
expect(subHeader).toContainElement(subId);
expect(subHeader).not.toContainElement(subDesc);
expect(subHeader?.nextElementSibling).toBe(subDesc);
});
});
Loading
Loading