mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-09-29 16:58:20 +08:00
fix(skills): expose shared expert skills in chat (#571)
Co-authored-by: jubaoliang <jubaoliang@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
### 修复
|
||||
|
||||
- 共享专家的技能列表现在会在聊天输入框中加载,非所有者可查看并选择专家已配置的技能
|
||||
- 知识库文本文档编辑抽屉在「编辑」模式下点「保存」无响应(`name`/`format` 字段未挂载时 `validateFields` 缺值导致抛错被吞掉;#592)
|
||||
- 知识库文本文档编辑内容未改时点「保存」只关闭抽屉,不触发更新与重建索引
|
||||
|
||||
|
||||
@@ -65,7 +65,10 @@ import {
|
||||
import { useLayoutMode } from "../../context/LayoutModeContext";
|
||||
import { useBrowserSessionState } from "../../hooks/useBrowserSessionState";
|
||||
import { prefetchVoiceConfig } from "../../hooks/useVoiceConfig";
|
||||
import { isSharedExpertViewer } from "../../utils/sharedExpert";
|
||||
import {
|
||||
chatSkillCatalogAgentId,
|
||||
isSharedExpertViewer,
|
||||
} from "../../utils/sharedExpert";
|
||||
import ChatDockPanels from "./components/ChatDockPanels";
|
||||
import { ChatFilePreviewProvider } from "./ChatFilePreviewContext";
|
||||
import {
|
||||
@@ -218,9 +221,7 @@ function ChatPageInner() {
|
||||
const { quickCards: expertQuickCards, welcomeSuffix } =
|
||||
useExpertChatWelcome(activeAgent);
|
||||
const { skills: chatSkills } = useSkills(
|
||||
agentChatReady && !agentsLoading && !sharedExpertViewer
|
||||
? resolvedAgentId ?? null
|
||||
: null,
|
||||
chatSkillCatalogAgentId(resolvedAgentId, agentChatReady, agentsLoading),
|
||||
);
|
||||
const [agentProfileOpen, setAgentProfileOpen] = useState(false);
|
||||
const [workspaceDrawerOpen, setWorkspaceDrawerOpen] = useState(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
chatSkillCatalogAgentId,
|
||||
isOwnedExpert,
|
||||
isSharedExpertViewer,
|
||||
ownedExperts,
|
||||
@@ -22,6 +23,25 @@ describe("isSharedExpertViewer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("chatSkillCatalogAgentId", () => {
|
||||
it("loads skills for a ready shared expert viewed by a non-owner", () => {
|
||||
const shared = {
|
||||
agent_id: "shared",
|
||||
is_shared: true,
|
||||
is_owner: false,
|
||||
};
|
||||
expect(isSharedExpertViewer(shared)).toBe(true);
|
||||
expect(chatSkillCatalogAgentId(shared.agent_id, true, false)).toBe(
|
||||
"shared",
|
||||
);
|
||||
});
|
||||
|
||||
it("waits until the expert is ready and agent loading has finished", () => {
|
||||
expect(chatSkillCatalogAgentId("shared", false, false)).toBeNull();
|
||||
expect(chatSkillCatalogAgentId("shared", true, true)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ownedExperts", () => {
|
||||
it("keeps owned experts and drops shared viewers for manage pages", () => {
|
||||
const agents = [
|
||||
|
||||
@@ -12,6 +12,22 @@ export function isOwnedExpert(agent: SharedExpertAccess): boolean {
|
||||
return !isSharedExpertViewer(agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the expert whose skill catalog can be shown in chat.
|
||||
*
|
||||
* Shared-expert viewers have read access to the expert's skills; ownership is
|
||||
* only required for mutations. Keep readiness/loading as the only UI gates so
|
||||
* the composer can inspect and select skills exposed by a shared expert.
|
||||
*/
|
||||
export function chatSkillCatalogAgentId(
|
||||
agentId: string | null | undefined,
|
||||
agentChatReady: boolean,
|
||||
agentsLoading: boolean,
|
||||
): string | null {
|
||||
if (!agentChatReady || agentsLoading) return null;
|
||||
return agentId ?? null;
|
||||
}
|
||||
|
||||
/** Experts the user owns — for Experts / Personalization / agent bars. */
|
||||
export function ownedExperts<T extends SharedExpertAccess>(agents: T[]): T[] {
|
||||
return agents.filter(isOwnedExpert);
|
||||
|
||||
@@ -44,7 +44,7 @@ import yaml
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
from octop.api.common.agent import require_agent_owner_row
|
||||
from octop.api.common.agent import require_agent_owner_row, require_agent_row
|
||||
from octop.api.deps import current_user, get_server
|
||||
from octop.infra.agents.manager import (
|
||||
skill_package_ids_list,
|
||||
@@ -89,10 +89,12 @@ async def _ctx(
|
||||
user: Any,
|
||||
as_user: int | None,
|
||||
server: Any,
|
||||
owner_only: bool = True,
|
||||
) -> _AgentCtx:
|
||||
assert server.app_runtime is not None
|
||||
registry = server.app_runtime.agent_registry
|
||||
row = require_agent_owner_row(agent_id, user=user, as_user=as_user, server=server)
|
||||
require = require_agent_owner_row if owner_only else require_agent_row
|
||||
row = require(agent_id, user=user, as_user=as_user, server=server)
|
||||
cfg = registry.get_config(agent_id)
|
||||
agent = registry.get_agent(agent_id)
|
||||
return _AgentCtx(runtime=row, workspace=agent.workspace, config=cfg)
|
||||
@@ -415,7 +417,7 @@ async def _enabled_skill_names(
|
||||
user: Any,
|
||||
) -> set[str]:
|
||||
"""Return installed, non-disabled skill names for an agent."""
|
||||
await _ctx(agent_id, user=user, as_user=None, server=server)
|
||||
await _ctx(agent_id, user=user, as_user=None, server=server, owner_only=False)
|
||||
assert server.app_runtime is not None
|
||||
names: set[str] = set()
|
||||
for summary in await server.app_runtime.agent_registry.list_skill_summaries(agent_id):
|
||||
@@ -450,7 +452,7 @@ async def list_skills(
|
||||
user: Any = Depends(current_user),
|
||||
server: Any = Depends(get_server),
|
||||
) -> list[dict[str, Any]]:
|
||||
await _ctx(agent_id, user=user, as_user=as_user, server=server)
|
||||
await _ctx(agent_id, user=user, as_user=as_user, server=server, owner_only=False)
|
||||
assert server.app_runtime is not None
|
||||
return cast(
|
||||
list[dict[str, Any]],
|
||||
@@ -528,7 +530,13 @@ async def get_skill(
|
||||
user: Any = Depends(current_user),
|
||||
server: Any = Depends(get_server),
|
||||
) -> dict[str, Any]:
|
||||
ctx = await _ctx(agent_id, user=user, as_user=as_user, server=server)
|
||||
ctx = await _ctx(
|
||||
agent_id,
|
||||
user=user,
|
||||
as_user=as_user,
|
||||
server=server,
|
||||
owner_only=False,
|
||||
)
|
||||
resolved = await _resolve_skill(ctx.workspace, name)
|
||||
if resolved is None:
|
||||
raise OctopError(ErrorCode.NOT_FOUND, f"skill {name!r} not found")
|
||||
|
||||
@@ -108,6 +108,12 @@ async def test_peer_cannot_mutate_shared_agent_workspace_or_skills(env) -> None:
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
response = await client.get(
|
||||
f"/api/agents/{agent_id}/skills",
|
||||
headers=peer_auth,
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
response = await client.post(
|
||||
"/api/mbti/apply",
|
||||
headers={**peer_auth, "X-Octop-Agent-Id": agent_id},
|
||||
|
||||
Reference in New Issue
Block a user