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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

## [Unreleased]

### 新增
- 专家:发布专家模板时新增「允许查看技能详情」开关;关闭后,安装者只能使用技能,无法查看或修改技能内容(发布者与管理员除外)

### 修复

- 插件工具使用中文等非 ASCII 名称时 LLM 调用失败:主流 API 要求工具名匹配 `^[a-zA-Z0-9_-]{1,64}$`,现自动将非法名称转写为合法拼音名(`pypinyin` 缺失时退回下划线替换),冲突追加 `_2`/`_3` 后缀,并在工具描述前缀 `[原名: …]` 保留原名映射;`config_json.plugins` 配置键与插件内部仍使用原始名称,路由不受影响
Expand Down
5 changes: 5 additions & 0 deletions dashboard/src/api/modules/publishedExperts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ describe("publishedExpertsApi", () => {
"/experts/published/expert%2F1/refresh",
{
method: "POST",
body: JSON.stringify({
name: "Updated",
description: "New description",
welcome_message: { zh: "欢迎", en: "Welcome" },
}),
},
);
expect(request).toHaveBeenNthCalledWith(
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/api/modules/publishedExperts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ export interface PublishedExpert {
color: string | null;
created_at: string;
updated_at: string;
/** When false, installers can use the expert's skills but not view or
* edit their contents. */
allow_skill_details?: boolean;
}

export interface PublishExpertBody {
name: string;
description?: string;
slug?: string;
welcome_message?: { zh?: string; en?: string };
allow_skill_details?: boolean;
}

export type RefreshPublishedExpertBody = PublishExpertBody;
Expand Down
10 changes: 9 additions & 1 deletion dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"TLS_DOMAIN_MISMATCH": "The domain does not match the existing certificate.",
"WORKSPACE_OP_UNSUPPORTED": "This workspace backend does not support that file operation.",
"SKILL_IMPORT_UNSUPPORTED_URL": "This skill URL source is not supported.",
"SKILL_DETAILS_PROTECTED": "The publisher of this expert has disabled viewing or editing its skill contents.",
"SKILL_IMPORT_FAILED": "Skill import failed: {{reason}}",
"SKILL_ALREADY_EXISTS": "Skill {{name}} already exists. Enable overwrite to replace it.",
"SKILL_PACKAGE_NOT_FOUND": "Skill package not found.",
Expand Down Expand Up @@ -594,7 +595,10 @@
"fieldDescription": "Description",
"fieldWelcomeZh": "Welcome message (Chinese)",
"fieldWelcomeEn": "Welcome message (English)",
"fieldWelcomePlaceholder": "One-line greeting shown on the chat welcome screen"
"fieldWelcomePlaceholder": "One-line greeting shown on the chat welcome screen",
"fieldAllowSkillDetails": "Allow installers to view skill details",
"allowSkillDetailsHint": "When off, users who install this template can use its skills but cannot view or edit skill contents.",
"skillsProtected": "Skill details locked after install"
},
"noExperts": "No experts available",
"selectExpertHint": "Pick an expert on the left to view details",
Expand Down Expand Up @@ -1073,6 +1077,8 @@
"dockFileListCount": "{{count}} files",
"dockFileListHint": "This list only shows files produced during execution. They are not guaranteed to be kept, and the model may delete them after processing finishes.",
"dockFileMaybeDeleted": "This file may have been a temporary artifact during processing and has already been deleted.",
"dockFileDelete": "Delete file",
"dockFileDeleteConfirm": "Delete this file? This cannot be undone.",
"remoteBrowserTitle": "Remote Browser",
"dockTerminalTitle": "Terminal",
"loadEarlierMessages": "Load earlier messages"
Expand Down Expand Up @@ -1322,6 +1328,8 @@
"importFailed": "Failed to import skill",
"loadFailed": "Failed to load skills",
"loadDetailFailed": "Failed to load skill details",
"protectedTag": "Protected",
"protectedHint": "This skill is protected by the expert publisher. It can be used but its contents cannot be viewed or edited.",
"enabledSuccess": "Skill enabled",
"disabledSuccess": "Skill disabled",
"operationFailed": "Operation failed",
Expand Down
10 changes: 9 additions & 1 deletion dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"TLS_DOMAIN_MISMATCH": "域名与现有证书不一致。",
"WORKSPACE_OP_UNSUPPORTED": "当前工作区后端不支持该文件操作。",
"SKILL_IMPORT_UNSUPPORTED_URL": "不支持该技能 URL 来源。",
"SKILL_DETAILS_PROTECTED": "该专家的发布者未开放技能内容的查看与修改权限。",
"SKILL_IMPORT_FAILED": "技能导入失败:{{reason}}",
"SKILL_ALREADY_EXISTS": "技能 {{name}} 已存在,如需覆盖请启用 overwrite。",
"SKILL_PACKAGE_NOT_FOUND": "未找到技能包。",
Expand Down Expand Up @@ -594,7 +595,10 @@
"fieldDescription": "模板描述",
"fieldWelcomeZh": "中文引导语",
"fieldWelcomeEn": "英文引导语",
"fieldWelcomePlaceholder": "聊天欢迎页展示的一行引导语"
"fieldWelcomePlaceholder": "聊天欢迎页展示的一行引导语",
"fieldAllowSkillDetails": "允许安装者查看技能详情",
"allowSkillDetailsHint": "关闭后,安装该模板的用户只能使用其中的技能,无法查看或修改技能内容。",
"skillsProtected": "安装后技能详情不可查看"
},
"noExperts": "暂无可用专家",
"selectExpertHint": "请从左侧选择一个专家以查看详情",
Expand Down Expand Up @@ -1073,6 +1077,8 @@
"dockFileListCount": "{{count}} 个文件",
"dockFileListHint": "当前仅列出执行过程中生成的文件,不代表最终一定存储,可能在处理结束后被大模型删除。",
"dockFileMaybeDeleted": "该文件可能为处理过程中的临时文件,当前已经被删除。",
"dockFileDelete": "删除文件",
"dockFileDeleteConfirm": "确定删除该文件吗?删除后不可恢复。",
"remoteBrowserTitle": "远程浏览器",
"dockTerminalTitle": "终端",
"loadEarlierMessages": "加载更早的消息"
Expand Down Expand Up @@ -1321,6 +1327,8 @@
"importFailed": "技能导入失败",
"loadFailed": "加载技能失败",
"loadDetailFailed": "加载技能详情失败",
"protectedTag": "已保护",
"protectedHint": "该技能由专家模板发布者保护,仅可使用,无法查看或修改内容。",
"enabledSuccess": "技能已启用",
"disabledSuccess": "技能已禁用",
"operationFailed": "操作失败",
Expand Down
16 changes: 14 additions & 2 deletions dashboard/src/pages/Agent/Skills/components/InstalledSkillsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo, useState, useCallback } from "react";
import { Form, Segmented, Tooltip } from "antd";
import { message } from "@/utils/antdMessage";
import { Download, LayoutGrid, List, Plus, RefreshCw } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAgent } from "../../../../context/AgentContext";
Expand Down Expand Up @@ -117,6 +118,10 @@ export default function InstalledSkillsTab({
};

const handleEdit = async (skill: SkillSpec) => {
if (skill.protected) {
message.warning(t("skills.protectedHint"));
return;
}
const detail = await getDetail(skill.slug);
if (detail) {
setEditingSkill(detail);
Expand Down Expand Up @@ -180,7 +185,9 @@ export default function InstalledSkillsTab({
onMouseLeave={() => setHoverKey(null)}
onToggleEnabled={(e) => void handleToggleEnabled(skill, e)}
onDelete={
kind === "custom" ? (e) => void handleDelete(skill, e) : undefined
kind === "custom" && !skill.protected
? (e) => void handleDelete(skill, e)
: undefined
}
/>
))}
Expand All @@ -192,7 +199,12 @@ export default function InstalledSkillsTab({
onView={(skill) => void handleEdit(skill)}
onToggleEnabled={(skill) => void handleToggleEnabled(skill)}
onDelete={
kind === "custom" ? (skill) => void handleDelete(skill) : undefined
kind === "custom"
? (skill) => {
if (skill.protected) return;
void handleDelete(skill);
}
: undefined
}
/>
);
Expand Down
7 changes: 7 additions & 0 deletions dashboard/src/pages/Agent/Skills/components/SkillCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FileText,
Image,
Info,
Lock,
Presentation,
Sparkles,
Trash2,
Expand Down Expand Up @@ -251,6 +252,12 @@ export function SkillCard({
<div className={styles.cardMeta}>
<div className={styles.cardTitle}>{displayName}</div>
<div className={styles.cardBadges}>
{skill.protected && (
<span className={styles.builtinBadge}>
<Lock size={11} style={{ verticalAlign: "-1px" }} />{" "}
{t("skills.protectedTag")}
</span>
)}
{isBuiltin && (
<span className={styles.builtinBadge}>
{t("skills.kindBuiltin")}
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/pages/Agent/Skills/components/SkillDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,9 @@ export function SkillDrawer({
) : (
<>
<Button onClick={onClose}>{t("common.close")}</Button>
{editingSkill?.kind === "workspace" && viewingSkillMd ? (
{editingSkill?.kind === "workspace" &&
viewingSkillMd &&
!editingSkill?.protected ? (
<Button
type="primary"
onClick={() => {
Expand Down
20 changes: 16 additions & 4 deletions dashboard/src/pages/Agent/Skills/components/SkillsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Popconfirm, Switch, Table, Tag } from "antd";
import { Popconfirm, Switch, Table, Tag, Tooltip } from "antd";
import type { ColumnsType } from "antd/es/table";
import { Trash2 } from "lucide-react";
import { Lock, Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { SkillSpec } from "../useSkills";
import { useSkillDisplayName } from "../skillDisplayNames";
Expand Down Expand Up @@ -30,7 +30,19 @@ export default function SkillsTable({
dataIndex: "name",
width: "18%",
ellipsis: true,
render: (_name: string, row) => skillDisplayName(row),
render: (_name: string, row) => (
<span>
{skillDisplayName(row)}
{row.protected ? (
<Tooltip title={t("skills.protectedHint")}>
<Lock
size={12}
style={{ marginLeft: 6, verticalAlign: "-2px", opacity: 0.6 }}
/>
</Tooltip>
) : null}
</span>
),
},
{
title: t("skills.skillDescription"),
Expand Down Expand Up @@ -85,7 +97,7 @@ export default function SkillsTable({
>
{t("common.view")}
</button>
{kind === "custom" && onDelete ? (
{kind === "custom" && onDelete && !row.protected ? (
<Popconfirm
title={t("skills.deleteConfirmContent", { slug: row.slug })}
okText={t("common.delete")}
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/pages/Agent/Skills/useSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface SkillSpec {
emoji?: string;
/** Persisted SkillHub marketplace icon. */
iconUrl?: string;
/** True when the expert publisher disabled viewing this skill's details. */
protected?: boolean;
}

export interface SkillDetail extends SkillSpec {
Expand All @@ -54,6 +56,7 @@ interface ServerSummary {
kind?: "workspace" | "builtin" | "package";
emoji?: string;
icon_url?: string;
protected?: boolean;
}

interface ServerDetail extends ServerSummary {
Expand All @@ -77,6 +80,7 @@ const toSpec = (row: ServerSummary): SkillSpec => ({
: "workspace",
emoji: row.emoji,
iconUrl: row.icon_url,
protected: row.protected,
});

const toDetail = (row: ServerDetail): SkillDetail => ({
Expand Down
20 changes: 19 additions & 1 deletion dashboard/src/pages/Experts/components/PublishExpertDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, Button, Drawer, Form, Input, Spin } from "antd";
import { Alert, Button, Drawer, Form, Input, Spin, Switch } from "antd";
import { message } from "@/utils/antdMessage";

import { agentChatApi } from "../../../api/modules/agentChat";
Expand Down Expand Up @@ -28,6 +28,7 @@ interface PublishFormValues {
description?: string;
welcome_zh?: string;
welcome_en?: string;
allow_skill_details?: boolean;
}

export default function PublishExpertDrawer({
Expand Down Expand Up @@ -64,6 +65,10 @@ export default function PublishExpertDrawer({
description: baseDescription,
welcome_zh: pickLocale(welcome.welcome_message, "zh"),
welcome_en: pickLocale(welcome.welcome_message, "en"),
allow_skill_details:
mode === "refresh" && published
? published.allow_skill_details !== false
: true,
});
} catch {
if (cancelled) return;
Expand All @@ -72,6 +77,10 @@ export default function PublishExpertDrawer({
description: baseDescription,
welcome_zh: "",
welcome_en: "",
allow_skill_details:
mode === "refresh" && published
? published.allow_skill_details !== false
: true,
});
} finally {
if (!cancelled) setPrefillLoading(false);
Expand All @@ -91,6 +100,7 @@ export default function PublishExpertDrawer({
zh: values.welcome_zh?.trim() || "",
en: values.welcome_en?.trim() || "",
},
allow_skill_details: values.allow_skill_details !== false,
});

const handleSubmit = async () => {
Expand Down Expand Up @@ -221,6 +231,14 @@ export default function PublishExpertDrawer({
placeholder={t("experts.published.fieldWelcomePlaceholder")}
/>
</Form.Item>
<Form.Item
name="allow_skill_details"
label={t("experts.published.fieldAllowSkillDetails")}
valuePropName="checked"
extra={t("experts.published.allowSkillDetailsHint")}
>
<Switch />
</Form.Item>
</Form>
</div>
</Drawer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export const PublishedExpertCard = memo(function PublishedExpertCard({
</div>
<div className={styles.expertCardFooter}>
<div className={styles.expertCardHint}>
{t("experts.published.install")}
{expert.allow_skill_details === false
? t("experts.published.skillsProtected")
: t("experts.published.install")}
</div>
{expert.creator_username && (
<div className={styles.expertInstalledLabel}>
Expand Down
Loading
Loading