chore: 提交本地修改

This commit is contained in:
Administrator committed 2026-06-12 18:09:07 +08:00
1 parent 229830ee89
commit 2862eee33b
19 files changed
+78 -218

No files matched your search

Submodule _skills_src/ai-skills added at cde1c8385e.
Submodule _skills_src/anthropics-skills added at 5754626092.
Submodule _skills_src/awesome-claude-skills added at 92568c1eda.
Submodule _skills_src/ce-kit added at d4973c4827.
+1
Submodule _skills_src/csm added at 3fa16a94e0.
Submodule _skills_src/superpowers added at 6fd4507659.
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 { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils"; 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 保存. */ /** Local edit-mode draft — committed in one `updateAgent` call on 保存. */
interface AgentDraft { interface AgentDraft {
@@ -232,9 +210,6 @@ export function AgentDetailPanel({
() => normalizeAgentQuestions(agent?.common_questions), () => normalizeAgentQuestions(agent?.common_questions),
[agent?.common_questions], [agent?.common_questions],
); );
const soulPreview = agent?.soul?.trim()
? cleanProfileText(desensitize(agent.soul))
: undefined;
function beginEdit() { function beginEdit() {
if (!agent || !canEdit) return; if (!agent || !canEdit) return;
@@ -609,31 +584,6 @@ export function AgentDetailPanel({
)} )}
</section> </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) && ( {(editing || questions.length > 0 || canEdit) && (
@@ -4,16 +4,15 @@ import {
ArrowDownIcon, ArrowDownIcon,
Code2Icon, Code2Icon,
CopyIcon, CopyIcon,
DownloadIcon, DownloadIcon,
EyeIcon, EyeIcon,
FileType2Icon, FileType2Icon,
LoaderIcon, LoaderIcon,
PackageIcon, PackageIcon,
SaveIcon, SaveIcon,
SquareArrowOutUpRightIcon, UploadCloudIcon,
UploadCloudIcon, XIcon,
XIcon, } from "lucide-react";
} from "lucide-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react"; import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
@@ -38,12 +37,11 @@ import {
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { CodeEditor } from "@/components/workspace/code-editor"; import { CodeEditor } from "@/components/workspace/code-editor";
import { publishArtifactToKnowledge } from "@/core/agents/api"; import { publishArtifactToKnowledge } from "@/core/agents/api";
import { useAgent } from "@/core/agents/hooks"; import { useAgent } from "@/core/agents/hooks";
import { apiFetch } from "@/core/api"; import { apiFetch } from "@/core/api";
import { import {
downloadArtifactFile, downloadArtifactFile,
openArtifactInNewTab, } from "@/core/artifacts/download";
} from "@/core/artifacts/download";
import { import {
useArtifactContent, useArtifactContent,
useUpdateArtifactContent, useUpdateArtifactContent,
@@ -514,32 +512,7 @@ export function ArtifactFileDetail({
} }
}, [displayContent, filepath]); }, [displayContent, filepath]);
const handleOpenWriteFileInNewTab = useCallback(() => { const handleDownloadWriteFile = 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 source = displayContent ?? ""; const source = displayContent ?? "";
if (!source.trim()) { if (!source.trim()) {
toast.error("文件内容为空,暂时无法下载"); toast.error("文件内容为空,暂时无法下载");
@@ -571,7 +544,7 @@ export function ArtifactFileDetail({
return ( return (
<Artifact className={cn(className)}> <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="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"> <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"> <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> </ArtifactTitle>
</div> </div>
</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 && ( {isSupportPreview && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ToggleGroup <ToggleGroup
@@ -644,8 +617,8 @@ export function ArtifactFileDetail({
</div> </div>
)} )}
</div> </div>
<div className="flex items-center justify-end gap-2"> <div className="flex w-full items-center justify-end gap-2 md:ml-auto md:w-auto">
<ArtifactActions className="bg-muted/45 rounded-full border border-black/6 p-1 dark:border-white/8"> <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 && ( {canPublishToKnowledge && (
<ArtifactAction <ArtifactAction
icon={isPublishingToKnowledge ? LoaderIcon : UploadCloudIcon} icon={isPublishingToKnowledge ? LoaderIcon : UploadCloudIcon}
@@ -678,45 +651,6 @@ export function ArtifactFileDetail({
/> />
</Tooltip> </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 && ( {isCodeFile && (
<ArtifactAction <ArtifactAction
icon={CopyIcon} icon={CopyIcon}
@@ -1,53 +1,10 @@
"use client"; "use client";
import { Database } from "lucide-react"; export function KnowledgeRagToggle({
className: _className,
import { }: {
SidebarMenuButton, className?: string;
SidebarMenuItem, }) {
} from "@/components/ui/sidebar"; void _className;
import { useKnowledgeRagEnabled } from "@/core/settings"; return null;
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>
);
} }
@@ -24,7 +24,6 @@ import {
History, History,
Layers, Layers,
Layout, Layout,
Library,
List, List,
ListChecks, ListChecks,
ListTodo, ListTodo,
@@ -287,12 +286,6 @@ export const sidebarMenuItems: MenuItem[] = [
icon: BookOpen, icon: BookOpen,
path: "/page/workspace/knowledge", path: "/page/workspace/knowledge",
}, },
{
id: "resource-knowledge-base",
label: "知识库资源管理",
icon: Library,
path: "/page/strategy/resource-management/resource-knowledge-base",
},
{ {
id: "resource-evaluation-index", id: "resource-evaluation-index",
label: "作战资源管理", label: "作战资源管理",
@@ -131,17 +131,6 @@ export function AgentDetailSheet({
</section> </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> </div>
@@ -13,6 +13,7 @@ from __future__ import annotations
import json import json
import logging import logging
import os import os
import hashlib
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@@ -20,16 +21,13 @@ import httpx
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from deerflow.config import get_app_config
from deerflow.config.agents_config import load_agent_config from deerflow.config.agents_config import load_agent_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/knowledge-ingest", tags=["knowledge-ingest"]) router = APIRouter(prefix="/api/knowledge-ingest", tags=["knowledge-ingest"])
# 入库接口地址,可用环境变量覆盖(如切换 UAT/生产)。 # 入库接口地址,可用环境变量覆盖(如切换 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 _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") @router.post("/publish", response_model=KnowledgeIngestPublishResponse, summary="Publish markdown artifact to external knowledge base")
async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> KnowledgeIngestPublishResponse: async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> KnowledgeIngestPublishResponse:
title = request.title.strip() title = request.title.strip()
@@ -121,8 +134,10 @@ async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> K
now = datetime.now() now = datetime.now()
# nodeName 是入库记录的唯一标识:md 标题 + 时间戳。两次尝试必须共用同一个 # nodeName 是入库记录的唯一标识:md 标题 + 时间戳。两次尝试必须共用同一个
# nodeName,避免第一次实际已入库时第二次产生重复记录。 # 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')}" batch_number = f"md{now.strftime('%Y%m%d')}"
ingest_url = _resolve_ingest_url()
logger.info( logger.info(
"[knowledge-ingest] 开始入库: agent=%s template=%s nodeName=%s batchNumber=%s title=%r content_len=%d url=%s", "[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, batch_number,
title, title,
len(content), len(content),
KNOWES_INGEST_URL, ingest_url,
) )
attempts: list[KnowledgeIngestAttempt] = [] 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), 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: 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: except httpx.HTTPError as exc:
logger.error("[knowledge-ingest] 第 %d 次发送请求异常 (标题字段=%s): %s", len(attempts) + 1, title_field, exc) logger.error("[knowledge-ingest] 第 %d 次发送请求异常 (标题字段=%s): %s", len(attempts) + 1, title_field, exc)
attempts.append(KnowledgeIngestAttempt(title_field=title_field, error=str(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.database_config import DatabaseConfig
from deerflow.config.extensions_config import ExtensionsConfig from deerflow.config.extensions_config import ExtensionsConfig
from deerflow.config.gateway_config import GatewayConfig from deerflow.config.gateway_config import GatewayConfig
from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict
from deerflow.config.knowledge_config import KnowledgeConfig from deerflow.config.knowledge_config import KnowledgeConfig
from deerflow.config.log_system_config import LogSystemConfig 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.login_config import AuthLoginConfig
from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_dict from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_dict
from deerflow.config.model_config import ModelConfig 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") run_events: RunEventsConfig = Field(default_factory=RunEventsConfig, description="Run event storage configuration")
checkpointer: CheckpointerConfig | None = Field(default=None, description="Checkpointer configuration") checkpointer: CheckpointerConfig | None = Field(default=None, description="Checkpointer configuration")
stream_bridge: StreamBridgeConfig | None = Field(default=None, description="Stream bridge 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") 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: 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 @classmethod
def resolve_config_path(cls, config_path: str | None = None) -> Path: def resolve_config_path(cls, config_path: str | None = None) -> Path:
@@ -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.",
)
@@ -32,7 +32,7 @@ class AgentRow(Base):
# ``is_private``. Empty string = "not assigned" → the listing layer treats # ``is_private``. Empty string = "not assigned" → the listing layer treats
# it as the admin-configured default square. Each agent belongs to at most # 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. # 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 # NULL = not featured on the homepage selector. Non-NULL = admin-curated
# featured rank (lower sorts first). Only ``published=True`` agents are # featured rank (lower sorts first). Only ``published=True`` agents are
# eligible; the router enforces that invariant on writes. BIGINT so the # eligible; the router enforces that invariant on writes. BIGINT so the
@@ -36,7 +36,7 @@ class SkillRow(Base):
# Publish square (发布广场) this skill is filed under once published. Pure # Publish square (发布广场) this skill is filed under once published. Pure
# organizational category, no access control. Empty string = unassigned → # organizational category, no access control. Empty string = unassigned →
# the listing layer resolves it to the default square. One square per skill. # 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)) 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)) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
+2
View File
@@ -108,6 +108,8 @@ vision:
model_name: deepseek-ai/DeepSeek-OCR model_name: deepseek-ai/DeepSeek-OCR
# prompt: 请详细描述这张图片的内容。 # prompt: 请详细描述这张图片的内容。
# max_tokens: 2048 # 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 会在服务端自动转换为 # - auto_convert_documents: true 时,上传的 PDF / Word / Excel / PPT 会在服务端自动转换为
# Markdown(.md),agent 通过 read_file / grep 读取其内容 —— 即“识别”文档内容。 # Markdown(.md),agent 通过 read_file / grep 读取其内容 —— 即“识别”文档内容。
-1
View File
@@ -255,7 +255,6 @@ const menuItems: MenuItem[] = [
label: "网络行为体资源管理", label: "网络行为体资源管理",
icon: Network, icon: Network,
}, },
{ id: "resource-knowledge-base", label: "知识库资源管理", icon: Library },
{ id: "resource-evaluation-index", label: "作战资源管理", icon: Library }, { id: "resource-evaluation-index", label: "作战资源管理", icon: Library },
// { // {
// id: "resource-evaluation-index", // id: "resource-evaluation-index",