chore: 提交本地修改
This commit is contained in:
1 parent
229830ee89
commit
2862eee33b
19 files changed
+78
-218
No files matched your search
Submodule
+1
Submodule _skills_src/ai-skills added at cde1c8385e.
Submodule
+1
Submodule _skills_src/anthropics-skills added at 5754626092.
Submodule
+1
Submodule _skills_src/awesome-claude-skills added at 92568c1eda.
Submodule
+1
Submodule _skills_src/ce-kit added at d4973c4827.
Submodule
+1
Submodule _skills_src/csm added at 3fa16a94e0.
Submodule
+1
Submodule _skills_src/superpowers added at 6fd4507659.
Submodule
+1
Submodule _skills_src/tapestry-skills added at 80e1dc56df.
@@ -65,28 +65,6 @@ import { usePublishSquares } from "@/core/system-settings/hooks";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Turn the admin-authored SOUL.md markdown into a clean, readable profile blurb
|
||||
* for the narrow panel. The view mode is a profile card, not a document view —
|
||||
* so we strip markdown syntax (headings, emphasis, inline code, list bullets,
|
||||
* blockquotes) rather than rendering it, keeping the line structure intact.
|
||||
*/
|
||||
function cleanProfileText(raw: string): string {
|
||||
return raw
|
||||
.split("\n")
|
||||
.map((line) =>
|
||||
line
|
||||
.replace(/^\s{0,3}#{1,6}\s*/, "")
|
||||
.replace(/^\s{0,3}>\s?/, "")
|
||||
.replace(/^(\s*)[-*+]\s+/, "$1• ")
|
||||
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
||||
.replace(/(\*|_)(.*?)\1/g, "$2")
|
||||
.replace(/`([^`]+)`/g, "$1"),
|
||||
)
|
||||
.join("\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Local edit-mode draft — committed in one `updateAgent` call on 保存. */
|
||||
interface AgentDraft {
|
||||
@@ -232,9 +210,6 @@ export function AgentDetailPanel({
|
||||
() => normalizeAgentQuestions(agent?.common_questions),
|
||||
[agent?.common_questions],
|
||||
);
|
||||
const soulPreview = agent?.soul?.trim()
|
||||
? cleanProfileText(desensitize(agent.soul))
|
||||
: undefined;
|
||||
|
||||
function beginEdit() {
|
||||
if (!agent || !canEdit) return;
|
||||
@@ -609,31 +584,6 @@ export function AgentDetailPanel({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── SOUL / 人设 ────────────────────────────────────────────────── */}
|
||||
{(editing || soulPreview) && (
|
||||
<Section
|
||||
icon={<SparklesIcon className="size-3.5" />}
|
||||
title="人设(SOUL)"
|
||||
>
|
||||
{editing && draft ? (
|
||||
<Textarea
|
||||
value={draft.soul}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, soul: e.currentTarget.value })
|
||||
}
|
||||
placeholder="输入 SOUL.md 内容"
|
||||
className="max-h-[45vh] min-h-48 resize-y font-mono text-xs"
|
||||
/>
|
||||
) : (
|
||||
// Viewport-relative cap: the SOUL preview expands with the screen
|
||||
// (taller than the old fixed 240px box) but never swallows the
|
||||
// whole panel — long content still scrolls inside.
|
||||
<p className="text-muted-foreground max-h-[40vh] overflow-y-auto whitespace-pre-wrap text-sm leading-relaxed">
|
||||
{soulPreview}
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* ── 常用问题 ───────────────────────────────────────────────────── */}
|
||||
{(editing || questions.length > 0 || canEdit) && (
|
||||
|
||||
@@ -4,16 +4,15 @@ import {
|
||||
ArrowDownIcon,
|
||||
Code2Icon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
EyeIcon,
|
||||
FileType2Icon,
|
||||
LoaderIcon,
|
||||
PackageIcon,
|
||||
SaveIcon,
|
||||
SquareArrowOutUpRightIcon,
|
||||
UploadCloudIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
DownloadIcon,
|
||||
EyeIcon,
|
||||
FileType2Icon,
|
||||
LoaderIcon,
|
||||
PackageIcon,
|
||||
SaveIcon,
|
||||
UploadCloudIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||
@@ -38,12 +37,11 @@ import {
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||
import { publishArtifactToKnowledge } from "@/core/agents/api";
|
||||
import { useAgent } from "@/core/agents/hooks";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import {
|
||||
downloadArtifactFile,
|
||||
openArtifactInNewTab,
|
||||
} from "@/core/artifacts/download";
|
||||
import { useAgent } from "@/core/agents/hooks";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import {
|
||||
downloadArtifactFile,
|
||||
} from "@/core/artifacts/download";
|
||||
import {
|
||||
useArtifactContent,
|
||||
useUpdateArtifactContent,
|
||||
@@ -514,32 +512,7 @@ export function ArtifactFileDetail({
|
||||
}
|
||||
}, [displayContent, filepath]);
|
||||
|
||||
const handleOpenWriteFileInNewTab = useCallback(() => {
|
||||
const source = displayContent ?? "";
|
||||
if (!source.trim()) {
|
||||
toast.error("文件内容为空,暂时无法打开");
|
||||
return;
|
||||
}
|
||||
if (language === "mermaid") {
|
||||
void openMermaidSvgInNewTab(source).catch((error) => {
|
||||
console.error("Failed to open mermaid artifact:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to open artifact");
|
||||
});
|
||||
return;
|
||||
}
|
||||
const blobType =
|
||||
language === "html"
|
||||
? "text/html;charset=utf-8"
|
||||
: language === "markdown"
|
||||
? "text/markdown;charset=utf-8"
|
||||
: "text/plain;charset=utf-8";
|
||||
const blob = new Blob([source], { type: blobType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
}, [displayContent, language]);
|
||||
|
||||
const handleDownloadWriteFile = useCallback(() => {
|
||||
const handleDownloadWriteFile = useCallback(() => {
|
||||
const source = displayContent ?? "";
|
||||
if (!source.trim()) {
|
||||
toast.error("文件内容为空,暂时无法下载");
|
||||
@@ -571,7 +544,7 @@ export function ArtifactFileDetail({
|
||||
|
||||
return (
|
||||
<Artifact className={cn(className)}>
|
||||
<ArtifactHeader className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 px-4 py-3">
|
||||
<ArtifactHeader className="flex flex-wrap items-center gap-3 px-4 py-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="bg-muted/65 inline-flex min-w-0 items-center rounded-full border border-black/6 p-1 shadow-sm dark:border-white/8">
|
||||
<span className="bg-background text-muted-foreground inline-flex h-7 shrink-0 items-center rounded-full border border-black/6 px-2.5 text-[10px] font-semibold tracking-[0.16em] dark:border-white/8">
|
||||
@@ -601,7 +574,7 @@ export function ArtifactFileDetail({
|
||||
</ArtifactTitle>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex w-full items-center justify-center md:w-auto md:flex-1">
|
||||
{isSupportPreview && (
|
||||
<div className="flex items-center gap-2">
|
||||
<ToggleGroup
|
||||
@@ -644,8 +617,8 @@ export function ArtifactFileDetail({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<ArtifactActions className="bg-muted/45 rounded-full border border-black/6 p-1 dark:border-white/8">
|
||||
<div className="flex w-full items-center justify-end gap-2 md:ml-auto md:w-auto">
|
||||
<ArtifactActions className="bg-muted/45 max-w-full flex-wrap justify-end rounded-3xl border border-black/6 p-1 dark:border-white/8">
|
||||
{canPublishToKnowledge && (
|
||||
<ArtifactAction
|
||||
icon={isPublishingToKnowledge ? LoaderIcon : UploadCloudIcon}
|
||||
@@ -678,45 +651,6 @@ export function ArtifactFileDetail({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ArtifactAction
|
||||
icon={SquareArrowOutUpRightIcon}
|
||||
label={t.common.openInNewWindow}
|
||||
tooltip={t.common.openInNewWindow}
|
||||
onClick={async () => {
|
||||
if (useWriteFileStreaming) {
|
||||
handleOpenWriteFileInNewTab();
|
||||
return;
|
||||
}
|
||||
if (language === "mermaid") {
|
||||
try {
|
||||
await openMermaidSvgInNewTab(displayContent ?? "");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to open artifact",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await openArtifactInNewTab({
|
||||
filepath,
|
||||
threadId,
|
||||
isMock,
|
||||
publicShareCode,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to open artifact",
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
icon={CopyIcon}
|
||||
|
||||
@@ -1,53 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Database } from "lucide-react";
|
||||
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useKnowledgeRagEnabled } from "@/core/settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
/**
|
||||
* Sidebar toggle for knowledge-base RAG injection (phase 2).
|
||||
*
|
||||
* Session-wide and in-memory only: while enabled, every conversation sends
|
||||
* ``context.knowledge_rag_enabled`` as ``runtime.context`` so
|
||||
* ``KnowledgeRagMiddleware`` injects recalled knowledge before each model
|
||||
* call. A page reload resets the switch to off.
|
||||
*/
|
||||
export function KnowledgeRagToggle({ className }: { className?: string }) {
|
||||
const [enabled, setEnabled] = useKnowledgeRagEnabled();
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<Tooltip
|
||||
content={
|
||||
enabled
|
||||
? "知识库召回:已开启(本次会话内所有对话默认开启)"
|
||||
: "知识库召回:已关闭"
|
||||
}
|
||||
>
|
||||
<SidebarMenuButton
|
||||
isActive={enabled}
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
className={className}
|
||||
>
|
||||
<Database />
|
||||
<span>知识库召回</span>
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs",
|
||||
enabled ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
>
|
||||
{enabled ? "已开启" : "已关闭"}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</Tooltip>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
export function KnowledgeRagToggle({
|
||||
className: _className,
|
||||
}: {
|
||||
className?: string;
|
||||
}) {
|
||||
void _className;
|
||||
return null;
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
History,
|
||||
Layers,
|
||||
Layout,
|
||||
Library,
|
||||
List,
|
||||
ListChecks,
|
||||
ListTodo,
|
||||
@@ -287,12 +286,6 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
icon: BookOpen,
|
||||
path: "/page/workspace/knowledge",
|
||||
},
|
||||
{
|
||||
id: "resource-knowledge-base",
|
||||
label: "知识库资源管理",
|
||||
icon: Library,
|
||||
path: "/page/strategy/resource-management/resource-knowledge-base",
|
||||
},
|
||||
{
|
||||
id: "resource-evaluation-index",
|
||||
label: "作战资源管理",
|
||||
|
||||
@@ -131,17 +131,6 @@ export function AgentDetailSheet({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 人设(SOUL.md) */}
|
||||
{agent.soul?.trim() && (
|
||||
<section className="space-y-1.5">
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
人设
|
||||
</h4>
|
||||
<pre className="whitespace-pre-wrap break-words rounded-md border border-border bg-muted/30 p-3 text-[12px] leading-relaxed text-foreground">
|
||||
{agent.soul}
|
||||
</pre>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -20,16 +21,13 @@ import httpx
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/knowledge-ingest", tags=["knowledge-ingest"])
|
||||
|
||||
# 入库接口地址,可用环境变量覆盖(如切换 UAT/生产)。
|
||||
KNOWES_INGEST_URL = os.environ.get(
|
||||
"KNOWES_INGEST_URL",
|
||||
"https://ch1.b.uat.4.cn/knowes/api/knowledge-outer/add",
|
||||
)
|
||||
_REQUEST_TIMEOUT_SECONDS = 30.0
|
||||
|
||||
# 标题字段在目标模板里可能叫「名称」或「标题」,按此顺序逐个尝试。
|
||||
@@ -101,6 +99,21 @@ def _build_payload(template_name: str, batch_number: str, node_name: str, title_
|
||||
}
|
||||
|
||||
|
||||
def _build_node_name(title: str, timestamp: str) -> str:
|
||||
"""Build a stable node name as md5(title) + timestamp + .md."""
|
||||
title_md5 = hashlib.md5(title.encode("utf-8")).hexdigest()
|
||||
return f"{title_md5}-{timestamp}.md"
|
||||
|
||||
|
||||
def _resolve_ingest_url() -> str:
|
||||
"""Resolve the external ingest URL from config.yaml, with env fallback."""
|
||||
config_url = getattr(get_app_config().knowledge_ingest, "url", "").strip()
|
||||
return os.environ.get(
|
||||
"KNOWES_INGEST_URL",
|
||||
config_url or "https://ch1.b.uat.4.cn/knowes/api/knowledge-outer/add",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/publish", response_model=KnowledgeIngestPublishResponse, summary="Publish markdown artifact to external knowledge base")
|
||||
async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> KnowledgeIngestPublishResponse:
|
||||
title = request.title.strip()
|
||||
@@ -121,8 +134,10 @@ async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> K
|
||||
now = datetime.now()
|
||||
# nodeName 是入库记录的唯一标识:md 标题 + 时间戳。两次尝试必须共用同一个
|
||||
# nodeName,避免第一次实际已入库时第二次产生重复记录。
|
||||
node_name = f"{title}-{now.strftime('%Y%m%d%H%M%S')}"
|
||||
timestamp = now.strftime("%Y%m%d%H%M%S")
|
||||
node_name = _build_node_name(title, timestamp)
|
||||
batch_number = f"md{now.strftime('%Y%m%d')}"
|
||||
ingest_url = _resolve_ingest_url()
|
||||
|
||||
logger.info(
|
||||
"[knowledge-ingest] 开始入库: agent=%s template=%s nodeName=%s batchNumber=%s title=%r content_len=%d url=%s",
|
||||
@@ -132,7 +147,7 @@ async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> K
|
||||
batch_number,
|
||||
title,
|
||||
len(content),
|
||||
KNOWES_INGEST_URL,
|
||||
ingest_url,
|
||||
)
|
||||
|
||||
attempts: list[KnowledgeIngestAttempt] = []
|
||||
@@ -148,7 +163,7 @@ async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> K
|
||||
json.dumps({**payload, "list": [{**payload["list"][0], "data": [{"fieldName": d["fieldName"], "value": d["value"][:200]} for d in payload["list"][0]["data"]]}]}, ensure_ascii=False),
|
||||
)
|
||||
try:
|
||||
response = await client.post(KNOWES_INGEST_URL, json=payload, headers=headers)
|
||||
response = await client.post(ingest_url, json=payload, headers=headers)
|
||||
except httpx.HTTPError as exc:
|
||||
logger.error("[knowledge-ingest] 第 %d 次发送请求异常 (标题字段=%s): %s", len(attempts) + 1, title_field, exc)
|
||||
attempts.append(KnowledgeIngestAttempt(title_field=title_field, error=str(exc)))
|
||||
|
||||
@@ -15,9 +15,10 @@ from deerflow.config.checkpointer_config import CheckpointerConfig, load_checkpo
|
||||
from deerflow.config.database_config import DatabaseConfig
|
||||
from deerflow.config.extensions_config import ExtensionsConfig
|
||||
from deerflow.config.gateway_config import GatewayConfig
|
||||
from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict
|
||||
from deerflow.config.knowledge_config import KnowledgeConfig
|
||||
from deerflow.config.log_system_config import LogSystemConfig
|
||||
from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict
|
||||
from deerflow.config.knowledge_config import KnowledgeConfig
|
||||
from deerflow.config.knowledge_ingest_config import KnowledgeIngestConfig
|
||||
from deerflow.config.log_system_config import LogSystemConfig
|
||||
from deerflow.config.login_config import AuthLoginConfig
|
||||
from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_dict
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
@@ -135,8 +136,9 @@ class AppConfig(BaseModel):
|
||||
run_events: RunEventsConfig = Field(default_factory=RunEventsConfig, description="Run event storage configuration")
|
||||
checkpointer: CheckpointerConfig | None = Field(default=None, description="Checkpointer configuration")
|
||||
stream_bridge: StreamBridgeConfig | None = Field(default=None, description="Stream bridge configuration")
|
||||
browser_context: BrowserContextConfig = Field(default_factory=BrowserContextConfig, description="Optional browser page capture integration")
|
||||
knowledge: KnowledgeConfig = Field(default_factory=KnowledgeConfig, description="Product knowledge base (obsidian-wiki compatible vault) configuration")
|
||||
browser_context: BrowserContextConfig = Field(default_factory=BrowserContextConfig, description="Optional browser page capture integration")
|
||||
knowledge: KnowledgeConfig = Field(default_factory=KnowledgeConfig, description="Product knowledge base (obsidian-wiki compatible vault) configuration")
|
||||
knowledge_ingest: KnowledgeIngestConfig = Field(default_factory=KnowledgeIngestConfig, description="External knowledge-ingest endpoint configuration")
|
||||
|
||||
@classmethod
|
||||
def resolve_config_path(cls, config_path: str | None = None) -> Path:
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
"""Configuration for the external knowledge-ingest endpoint."""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class KnowledgeIngestConfig(BaseModel):
|
||||
"""External knowledge-ingest configuration (`config.yaml` -> `knowledge_ingest`)."""
|
||||
|
||||
url: str = Field(
|
||||
default="https://ch1.b.uat.4.cn/knowes/api/knowledge-outer/add",
|
||||
description="External knowledge-ingest endpoint URL.",
|
||||
)
|
||||
+1
-1
@@ -32,7 +32,7 @@ class AgentRow(Base):
|
||||
# ``is_private``. Empty string = "not assigned" → the listing layer treats
|
||||
# it as the admin-configured default square. Each agent belongs to at most
|
||||
# one square (single column), so it can never be published to two squares.
|
||||
square_id: Mapped[str] = mapped_column(String(64), nullable=False, default="", index=True)
|
||||
square_id: Mapped[str] = mapped_column(String(64), nullable=False, default="", server_default="", index=True)
|
||||
# NULL = not featured on the homepage selector. Non-NULL = admin-curated
|
||||
# featured rank (lower sorts first). Only ``published=True`` agents are
|
||||
# eligible; the router enforces that invariant on writes. BIGINT so the
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class SkillRow(Base):
|
||||
# Publish square (发布广场) this skill is filed under once published. Pure
|
||||
# organizational category, no access control. Empty string = unassigned →
|
||||
# the listing layer resolves it to the default square. One square per skill.
|
||||
square_id: Mapped[str] = mapped_column(String(64), nullable=False, default="", index=True)
|
||||
square_id: Mapped[str] = mapped_column(String(64), nullable=False, default="", server_default="", index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
@@ -108,6 +108,8 @@ vision:
|
||||
model_name: deepseek-ai/DeepSeek-OCR
|
||||
# prompt: 请详细描述这张图片的内容。
|
||||
# max_tokens: 2048
|
||||
knowledge_ingest:
|
||||
url: "https://ch1.b.uat.4.cn/knowes/api/knowledge-outer/add"
|
||||
# 上传文件识别配置。
|
||||
# - auto_convert_documents: true 时,上传的 PDF / Word / Excel / PPT 会在服务端自动转换为
|
||||
# Markdown(.md),agent 通过 read_file / grep 读取其内容 —— 即“识别”文档内容。
|
||||
|
||||
@@ -255,7 +255,6 @@ const menuItems: MenuItem[] = [
|
||||
label: "网络行为体资源管理",
|
||||
icon: Network,
|
||||
},
|
||||
{ id: "resource-knowledge-base", label: "知识库资源管理", icon: Library },
|
||||
{ id: "resource-evaluation-index", label: "作战资源管理", icon: Library },
|
||||
// {
|
||||
// id: "resource-evaluation-index",
|
||||
|
||||
Reference in new issue
Block a user