@shikeai/knowledge 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4511,8 +4511,9 @@ window.__ModuleLoader__.load({
4511
4511
  chunkSize: uploadChunk.chunk_size || "",
4512
4512
  chunkOverlap: uploadChunk.chunk_overlap || "",
4513
4513
  folderId: uploadFolderId
4514
- }).then(() => {
4514
+ }).then((data) => {
4515
4515
  toast("文档已上传,开始解析", "success");
4516
+ if (data?.warning) String(data.warning).split("\n").forEach((m) => toast(m, "warning", 6e3));
4516
4517
  setUploading(false);
4517
4518
  setPickedFile(null);
4518
4519
  setUploadTags([]);
@@ -6001,13 +6002,65 @@ window.__ModuleLoader__.load({
6001
6002
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, { text });
6002
6003
  }
6003
6004
  //#endregion
6004
- //#region src/api/system.js
6005
+ //#region src/api/modelconfig.js
6006
+ /**
6007
+ * 模型服务配置 REST(对应后端 /kb/model-config*;仅管理员)
6008
+ *
6009
+ * 覆盖向量化(embedding)/ 重排(rerank)/ OCR 视觉识别(ocr)三类模型服务的
6010
+ * 设置页覆盖配置;api_key 语义(后端约定):传空串或原样传回掩码串(含 ****)
6011
+ * = 沿用当前生效密钥,传新值 = 更新密钥
6012
+ */
6013
+ /**
6014
+ * 读取三类模型服务当前生效配置(含来源 source: db=设置页覆盖 / env=环境变量 / unset=未配置)
6015
+ * @returns {Promise<{ embedding: { source: string, enabled: boolean, config: object }, rerank: { source: string, enabled: boolean, config: object }, ocr: { source: string, enabled: boolean, config: object } }>}
6016
+ */
6017
+ function getModelConfig() {
6018
+ return request.get("/model-config");
6019
+ }
6020
+ /**
6021
+ * 保存单项配置(config=null 表示恢复默认:清除设置页覆盖并回落部署环境变量配置)
6022
+ * @param {'embedding'|'rerank'|'ocr'} item
6023
+ * @param {object|null} config
6024
+ * @returns {Promise<object>} 保存成功返回同 GET 结构的全量配置
6025
+ */
6026
+ function saveModelConfig(item, config) {
6027
+ return request.put("/model-config", {
6028
+ item,
6029
+ config
6030
+ });
6031
+ }
6032
+ /**
6033
+ * 连通性测试(用传入 config 实际请求模型服务商)
6034
+ * @param {'embedding'|'rerank'|'ocr'} item
6035
+ * @param {object} config
6036
+ * @returns {Promise<{ message: string }>} 成功 message='连通性测试通过';失败抛 ApiError(msg 为中文错误原因)
6037
+ */
6038
+ function testModelConfig(item, config) {
6039
+ return request.post("/model-config/test", {
6040
+ item,
6041
+ config
6042
+ });
6043
+ }
6044
+ //#endregion
6045
+ //#region src/api/health.js
6005
6046
  /**
6006
- * 系统级 REST(对应后端 /kb/health 等免鉴权接口)
6047
+ * 服务器健康探测:GET <base>/kb/health(免鉴权)→ { status, app, version }
6048
+ *
6049
+ * 直连 fetch(不经 request.js:request 固定使用当前激活服务器基址并附带插件 Token),
6050
+ * 设置页用它逐个探测「已添加」的每台服务器,校验其部署的应用 id 与版本和插件是否匹配
6051
+ * (app / version 由后端 main.go 注入:版本经发布脚本 --build-arg VERSION → ldflags 写入)。
6007
6052
  */
6008
- /** 健康检查:返回 { status: 'ok', version: '<后端发布版本>' }(免登录,不受平台状态门禁拦截) */
6009
- function getHealth() {
6010
- return request.get("/health", {}, { allowAnonymous: true });
6053
+ function getHealthByBase(base) {
6054
+ const ctrl = new AbortController();
6055
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
6056
+ const url = `${String(base || "").replace(/\/+$/, "")}/kb/health`;
6057
+ return fetch(url, { signal: ctrl.signal }).catch((error) => {
6058
+ if (error && error.name === "AbortError") throw new Error("请求超时");
6059
+ throw new Error("网络请求失败");
6060
+ }).then((res) => res.json()).then((body) => {
6061
+ if (!body || body.code !== 0) throw new Error(body && body.msg || "响应异常");
6062
+ return body.data || {};
6063
+ }).finally(() => clearTimeout(timer));
6011
6064
  }
6012
6065
  //#endregion
6013
6066
  //#region src/manifest.js
@@ -6022,7 +6075,7 @@ window.__ModuleLoader__.load({
6022
6075
  /** npm 作用域包名 */
6023
6076
  packageName: "@shikeai/knowledge",
6024
6077
  /** 版本:单一来源 frontend/package.json,构建时(tsdown/vite)自动内联,禁止在此手写第二份 */
6025
- version: "0.8.0",
6078
+ version: "0.8.1",
6026
6079
  /** 功能描述 */
6027
6080
  description: "时刻AI平台桌面端知识库插件:知识管理(右侧 Sidebar 标签页:首页/知识库详情/文档详情/设置)、成员权限、MCP RAG 检索服务",
6028
6081
  /** 作者 */
@@ -6214,173 +6267,1294 @@ window.__ModuleLoader__.load({
6214
6267
  });
6215
6268
  }
6216
6269
  //#endregion
6217
- //#region src/components/settings/UserManagePanel.jsx
6270
+ //#region src/components/settings/ModelServiceModal.jsx
6218
6271
  /**
6219
- * 用户管理区块(设置页;仅管理员可见可操作,非管理员仅展示状态说明)
6272
+ * 模型服务编辑弹窗(设置页「模型服务」区块):向量化 / 重排 / OCR 单项配置表单
6220
6273
  *
6221
- * - 用户列表:昵称(用户中心批量获取,仅展示)/ UID / 脱敏手机号 / 状态标签 /
6222
- * 注册时间 / 最近审核信息;状态筛选(全部/待审核/正常/禁用)+ 关键词(UID 或
6223
- * 手机号片段)+ 分页;管理员行标注「管理员」且不可被审核/禁用/删除
6224
- * - 操作:审核通过(待审核→正常)、禁用(同步吊销其全部插件 Token)、启用
6225
- * (禁用→正常)、删除(物理删除状态与授权,保留其知识库,悬空所有者由管理员
6226
- * 经「修改所有者」接管);操作均写后端审计日志
6227
- * - 新用户默认审核模式:需审核 / 无需审核(默认);切换仅影响之后首次验签的新用户
6274
+ * - 服务商预设:常用国内服务商(按服务项收录可用者:向量化=硅基流动/阿里百炼/百度千帆,
6275
+ * 重排=硅基流动/智谱,OCR=DeepSeek/火山方舟/硅基流动/阿里百炼/智谱/Kimi/腾讯混元)+
6276
+ * 本地部署 Infinity(仅向量化与重排,对应 dev/docker-compose.yml 的 bge 容器)/
6277
+ * 本地部署(ollama · glm-ocr容器)(仅 OCR 项显示)/ 自定义;选预设回填地址与协议,
6278
+ * 模型为空或仍是预设默认值时回填推荐模型,ollama 预设另在提示词为空时回填官方触发
6279
+ * 前缀 Text Recognition:(GLM-OCR 为任务专用模型),不动已填密钥;本地部署预设的
6280
+ * 地址主机默认取当前 API 地址的
6281
+ * IP/主机名(API 为 http://127.0.0.1:8081 → http://127.0.0.1:7997),未配置回退 localhost。
6282
+ * 收录约束:向量化仅 1024 维模型(库列 vector(1024) 且
6283
+ * 后端不透传 dimensions 参数);重排仅 Cohere 兼容 {base}/rerank 端点(百炼/千帆
6284
+ * 端点格式不同暂不收录)
6285
+ * - api_key 语义(后端约定):留空或原样传回掩码串(含 ****)= 沿用当前生效密钥,
6286
+ * 传新值 = 更新密钥;表单回显后端掩码串
6287
+ * - 数字字段:留空不提交该字段(后端 0/缺省 = 沿用默认,OCR max_pages 0=不限制)
6288
+ * - 测试连接:以当前表单值调后端连通性测试,成功/失败经全局 Toast 透出后端返回信息
6289
+ * - footer:恢复默认(清除设置页覆盖,回落部署环境变量配置,经确认弹窗二次确认)/ 测试连接 / 保存;
6290
+ * 保存成功回调 onSaved(data),data 为保存后同 GET /kb/model-config 结构的全量配置
6228
6291
  */
6229
- const PAGE_SIZE = 20;
6230
- /** 用户状态 → 展示文案与徽标色系(0 待审核 / 1 正常 / 2 禁用) */
6231
- const STATUS_VIEW = {
6232
- 0: {
6233
- text: "待审核",
6234
- badge: "skai-kb-badge-amber"
6235
- },
6236
- 1: {
6237
- text: "正常",
6238
- badge: "skai-kb-badge-green"
6239
- },
6240
- 2: {
6241
- text: "禁用",
6242
- badge: "skai-kb-badge-red"
6243
- }
6292
+ /** 编辑项 → 展示名(与设置页「模型服务」面板行一致) */
6293
+ const ITEM_NAMES = {
6294
+ embedding: "向量化(Embedding)",
6295
+ rerank: "重排(Rerank)",
6296
+ ocr: "OCR(视觉识别)"
6244
6297
  };
6245
- const STATUS_FILTERS = [
6246
- {
6247
- value: -1,
6248
- label: "全部"
6249
- },
6250
- {
6251
- value: 0,
6252
- label: "待审核"
6253
- },
6254
- {
6255
- value: 1,
6256
- label: "正常"
6257
- },
6258
- {
6259
- value: 2,
6260
- label: "禁用"
6298
+ /**
6299
+ * 国内服务商预设(按服务项收录,选后回填地址与推荐模型)。选型约束见文件头注释:
6300
+ * - 向量化:库列固定 vector(1024) 且后端不透传 dimensions 参数,仅收录原生 1024 维模型
6301
+ * (豆包 embedding 2048 维、智谱 embedding-3 默认 2048 维暂不收录)
6302
+ * - 重排:后端拼接 {base}/rerank(Cohere 兼容),仅收录支持该端点的服务商
6303
+ * (百炼为原生 text-rerank、千帆为 /v2/rerankers,路径不兼容暂不收录)
6304
+ * - OCR:OpenAI 兼容 /chat/completions 视觉模型(DeepSeek 无向量化/重排,仅 OCR 项)
6305
+ */
6306
+ const PROVIDER_PRESETS = {
6307
+ embedding: [
6308
+ {
6309
+ value: "siliconflow",
6310
+ label: "硅基流动 SiliconFlow(推荐)",
6311
+ base: "https://api.siliconflow.cn/v1",
6312
+ model: "BAAI/bge-large-zh-v1.5",
6313
+ hint: "推荐模型:BAAI/bge-large-zh-v1.5(1024 维)"
6314
+ },
6315
+ {
6316
+ value: "dashscope",
6317
+ label: "阿里云百炼(通义)",
6318
+ base: "https://dashscope.aliyuncs.com/compatible-mode/v1",
6319
+ model: "text-embedding-v4",
6320
+ hint: "推荐模型:text-embedding-v4(默认 1024 维,与库内向量列一致)"
6321
+ },
6322
+ {
6323
+ value: "qianfan",
6324
+ label: "百度千帆",
6325
+ base: "https://qianfan.baidubce.com/v2",
6326
+ model: "bge-large-zh",
6327
+ hint: "推荐模型:bge-large-zh(1024 维)"
6328
+ }
6329
+ ],
6330
+ rerank: [{
6331
+ value: "siliconflow",
6332
+ label: "硅基流动 SiliconFlow(推荐)",
6333
+ base: "https://api.siliconflow.cn/v1",
6334
+ model: "BAAI/bge-reranker-v2-m3",
6335
+ hint: "推荐模型:BAAI/bge-reranker-v2-m3"
6336
+ }, {
6337
+ value: "zhipu",
6338
+ label: "智谱 AI",
6339
+ base: "https://open.bigmodel.cn/api/paas/v4",
6340
+ model: "rerank",
6341
+ hint: "推荐模型:rerank(智谱 Cohere 兼容重排端点)"
6342
+ }],
6343
+ ocr: [
6344
+ {
6345
+ value: "deepseek",
6346
+ label: "DeepSeek",
6347
+ base: "https://api.deepseek.com",
6348
+ model: "deepseek-flash",
6349
+ hint: "推荐模型:deepseek-flash(V4.1 Flash 原生多模态,支持 image_url 传图;DeepSeek 无向量化/重排服务)"
6350
+ },
6351
+ {
6352
+ value: "ark",
6353
+ label: "火山方舟(字节跳动 · 豆包)",
6354
+ base: "https://ark.cn-beijing.volces.com/api/v3",
6355
+ model: "doubao-seed-2-1-pro-260915",
6356
+ hint: "推荐模型:doubao-seed-2-1-pro-260915(旗舰多模态,支持模型名直调;旧 doubao-seed-1.6 已于 2026-09 停服)"
6357
+ },
6358
+ {
6359
+ value: "siliconflow",
6360
+ label: "硅基流动 SiliconFlow",
6361
+ base: "https://api.siliconflow.cn/v1",
6362
+ model: "Qwen/Qwen2.5-VL-32B-Instruct",
6363
+ hint: "推荐模型:Qwen/Qwen2.5-VL-32B-Instruct 等视觉模型"
6364
+ },
6365
+ {
6366
+ value: "dashscope",
6367
+ label: "阿里云百炼(通义)",
6368
+ base: "https://dashscope.aliyuncs.com/compatible-mode/v1",
6369
+ model: "qwen-vl-max",
6370
+ hint: "推荐模型:qwen-vl-max(或 qwen3-vl 系列)"
6371
+ },
6372
+ {
6373
+ value: "zhipu",
6374
+ label: "智谱 AI",
6375
+ base: "https://open.bigmodel.cn/api/paas/v4",
6376
+ model: "glm-4.6v-flash",
6377
+ hint: "推荐模型:glm-4.6v-flash(免费)/ glm-4.6v / GLM-OCR"
6378
+ },
6379
+ {
6380
+ value: "moonshot",
6381
+ label: "月之暗面 Kimi",
6382
+ base: "https://api.moonshot.cn/v1",
6383
+ model: "kimi-k3.1-pro",
6384
+ hint: "推荐模型:kimi-k3.1-pro(原生视觉;kimi-latest 等旧视觉模型名已下线)"
6385
+ },
6386
+ {
6387
+ value: "hunyuan",
6388
+ label: "腾讯混元",
6389
+ base: "https://api.hunyuan.cloud.tencent.com/v1",
6390
+ model: "hunyuan-vision",
6391
+ hint: "推荐模型:hunyuan-vision"
6392
+ }
6393
+ ]
6394
+ };
6395
+ /** 按值查找服务项的服务商预设 */
6396
+ const findPreset = (item, value) => (PROVIDER_PRESETS[item] || []).find((p) => p.value === value);
6397
+ /**
6398
+ * 本地部署预设(dev/docker-compose.yml 的 bge 容器:Infinity 镜像,端口 7997,
6399
+ * 同时加载向量化与重排两个模型,无需 API 密钥)。Infinity 端点无 /v1 前缀,
6400
+ * 向量化与重排均用根路径(后端分别拼 {base}/embeddings、{base}/rerank,Cohere 兼容)。
6401
+ * 主机默认取当前 API 地址的 IP/主机名(本地模型与后端 API 同机部署,
6402
+ * 如 API 为 http://127.0.0.1:8081 则取 127.0.0.1),未配置或解析失败回退 localhost
6403
+ */
6404
+ function localModelHost() {
6405
+ try {
6406
+ const apiBase = getApiBase();
6407
+ if (!apiBase) return window.location.hostname || "localhost";
6408
+ return new URL(apiBase, window.location.origin).hostname || "localhost";
6409
+ } catch {
6410
+ return "localhost";
6261
6411
  }
6412
+ }
6413
+ /** 本地部署 bge 容器地址(Infinity 端点无 /v1 前缀,向量化与重排均用根路径) */
6414
+ const localBase = () => `http://${localModelHost()}:7997`;
6415
+ /** 本地部署标准模型名(与 compose INFINITY_MODEL_ID 一致) */
6416
+ const LOCAL_MODEL = {
6417
+ embedding: "BAAI/bge-large-zh-v1.5",
6418
+ rerank: "BAAI/bge-reranker-v2-m3"
6419
+ };
6420
+ /** 本地部署预设提示 */
6421
+ const LOCAL_HINT = {
6422
+ embedding: "本地 Infinity(bge 容器,端口 7997,向量维度 1024):无需 API 密钥;默认主机与 API 地址一致;同 Docker 网络容器内可改用 http://bge:7997",
6423
+ rerank: "本地 Infinity(bge 容器,端口 7997):无需 API 密钥;默认主机与 API 地址一致;同 Docker 网络容器内可改用 http://bge:7997"
6424
+ };
6425
+ /**
6426
+ * 本地部署 Ollama 预设(dev/docker-compose.yml 的 ollama 容器:端口 11434,启动自动拉取
6427
+ * glm-ocr:q8_0,无需 API 密钥)。GLM-OCR 为任务专用模型,识别提示词需用官方触发前缀
6428
+ * (全文 Text Recognition: / 表格 Table Recognition: / 图 Figure Recognition:),
6429
+ * 自然语言提示词在无文字图上会输出退化内容;预设回填提示词仅在为空时写入(保留自定义)
6430
+ */
6431
+ const OLLAMA_OCR = {
6432
+ model: "glm-ocr:q8_0",
6433
+ prompt: "Text Recognition:"
6434
+ };
6435
+ /** 本地部署 Ollama 预设提示 */
6436
+ const OLLAMA_HINT = "本地部署 Ollama(glm-ocr 容器,端口 11434):无需 API 密钥;GLM-OCR 为任务专用模型,已回填官方识别前缀 Text Recognition:(表格用 Table Recognition:、图用 Figure Recognition:)";
6437
+ /** 地址域名特征 → 预设值(按已有配置自动识别服务商预设,仅识别当前服务项收录的预设) */
6438
+ const BASE_MARKS = [
6439
+ ["siliconflow", "siliconflow"],
6440
+ ["volces.com", "ark"],
6441
+ ["dashscope.aliyuncs.com", "dashscope"],
6442
+ ["qianfan.baidubce.com", "qianfan"],
6443
+ ["bigmodel.cn", "zhipu"],
6444
+ ["api.moonshot.cn", "moonshot"],
6445
+ ["hunyuan.cloud.tencent.com", "hunyuan"],
6446
+ ["deepseek.com", "deepseek"]
6262
6447
  ];
6263
- function UserManagePanel({ isAdmin }) {
6264
- const [loading, setLoading] = (0, react.useState)(true);
6265
- const [users, setUsers] = (0, react.useState)([]);
6266
- const [total, setTotal] = (0, react.useState)(0);
6267
- const [pendingCount, setPendingCount] = (0, react.useState)(0);
6268
- const [status, setStatus] = (0, react.useState)(-1);
6269
- const [keyword, setKeyword] = (0, react.useState)("");
6270
- const [page, setPage] = (0, react.useState)(1);
6271
- const [mode, setMode] = (0, react.useState)(null);
6272
- const [modeBusy, setModeBusy] = (0, react.useState)(false);
6273
- const [busy, setBusy] = (0, react.useState)(false);
6274
- const [confirm, setConfirm] = (0, react.useState)(null);
6275
- const { infoMap } = useMemberInfo(users.map((u) => u.uid));
6276
- const reload = (0, react.useCallback)(() => {
6277
- if (!isAdmin) {
6278
- setLoading(false);
6448
+ /** 按已有配置自动识别服务商预设(OCR 协议 / 本地端口 / 地址域名),未命中返回 custom */
6449
+ function detectPreset(item, apiBase, protocol) {
6450
+ if (item === "ocr" && (protocol || "") === "ollama") return "ollama";
6451
+ const base = String(apiBase || "");
6452
+ if ((item === "embedding" || item === "rerank") && base.includes(":7997")) return "local";
6453
+ for (const [mark, value] of BASE_MARKS) if (base.includes(mark) && findPreset(item, value)) return value;
6454
+ return "custom";
6455
+ }
6456
+ /** 全部预设默认模型名:当前模型为空或仍是任一预设默认值时切换服务商才覆盖回填(保留自定义值) */
6457
+ const PRESET_MODELS = [.../* @__PURE__ */ new Set([
6458
+ ...Object.values(PROVIDER_PRESETS).flat().map((p) => p.model),
6459
+ ...Object.values(LOCAL_MODEL),
6460
+ OLLAMA_OCR.model
6461
+ ])];
6462
+ const isPresetModel = (m) => {
6463
+ const s = String(m ?? "").trim();
6464
+ return s === "" || PRESET_MODELS.includes(s);
6465
+ };
6466
+ /** 全部服务商预设默认地址:切换到本地部署等预设时,地址为空或仍是预设默认值才覆盖回填 */
6467
+ const PRESET_BASES = [...new Set(Object.values(PROVIDER_PRESETS).flat().map((p) => p.base))];
6468
+ /** 数字输入 → number(空串/非法值返回 undefined,不提交该字段) */
6469
+ function toNumber(value) {
6470
+ const s = String(value ?? "").trim();
6471
+ if (s === "") return void 0;
6472
+ const n = Number(s);
6473
+ return Number.isFinite(n) ? n : void 0;
6474
+ }
6475
+ function ModelServiceModal({ item, open, config, onClose, onSaved }) {
6476
+ const cfg = config || {};
6477
+ const [preset, setPreset] = (0, react.useState)(() => detectPreset(item, cfg.api_base, cfg.protocol));
6478
+ const [apiBase, setApiBase] = (0, react.useState)(cfg.api_base || "");
6479
+ const [apiKey, setApiKey] = (0, react.useState)(cfg.api_key || "");
6480
+ const [model, setModel] = (0, react.useState)(cfg.model || "");
6481
+ const [protocol, setProtocol] = (0, react.useState)(item === "ocr" ? cfg.protocol || "openai" : "");
6482
+ const [prompt, setPrompt] = (0, react.useState)(cfg.prompt || "");
6483
+ const [dimension, setDimension] = (0, react.useState)(cfg.dimension ?? "");
6484
+ const [batchSize, setBatchSize] = (0, react.useState)(cfg.batch_size ?? "");
6485
+ const [timeoutSec, setTimeoutSec] = (0, react.useState)(cfg.timeout_sec ?? "");
6486
+ const [topN, setTopN] = (0, react.useState)(cfg.top_n ?? "");
6487
+ const [maxPages, setMaxPages] = (0, react.useState)(cfg.max_pages ?? "");
6488
+ const [fieldErrors, setFieldErrors] = (0, react.useState)({});
6489
+ const [saving, setSaving] = (0, react.useState)(false);
6490
+ const [testing, setTesting] = (0, react.useState)(false);
6491
+ const [resetting, setResetting] = (0, react.useState)(false);
6492
+ const [confirmReset, setConfirmReset] = (0, react.useState)(false);
6493
+ const busy = saving || testing || resetting;
6494
+ /** 按编辑项差异组装提交体(公共 + 各项专属字段;数字留空不提交) */
6495
+ const buildPayload = () => {
6496
+ const payload = {
6497
+ api_base: apiBase.trim(),
6498
+ api_key: apiKey.trim(),
6499
+ model: model.trim()
6500
+ };
6501
+ const put = (key, value) => {
6502
+ const n = toNumber(value);
6503
+ if (n !== void 0) payload[key] = n;
6504
+ };
6505
+ if (item === "embedding") {
6506
+ put("dimension", dimension);
6507
+ put("batch_size", batchSize);
6508
+ put("timeout_sec", timeoutSec);
6509
+ } else if (item === "rerank") {
6510
+ put("top_n", topN);
6511
+ put("timeout_sec", timeoutSec);
6512
+ } else {
6513
+ payload.protocol = protocol || "openai";
6514
+ payload.prompt = prompt.trim();
6515
+ put("timeout_sec", timeoutSec);
6516
+ put("max_pages", maxPages);
6517
+ }
6518
+ return payload;
6519
+ };
6520
+ /** 必填校验:API 地址与模型名,行内错误提示 */
6521
+ const validate = () => {
6522
+ const errs = {};
6523
+ if (!apiBase.trim()) errs.api_base = "请填写 API 地址";
6524
+ if (!model.trim()) errs.model = "请填写模型名";
6525
+ setFieldErrors(errs);
6526
+ return Object.keys(errs).length === 0;
6527
+ };
6528
+ const applyPreset = (next) => {
6529
+ setPreset(next);
6530
+ if (next === "local" && (item === "embedding" || item === "rerank")) {
6531
+ setApiBase(localBase());
6532
+ if (isPresetModel(model)) setModel(LOCAL_MODEL[item]);
6279
6533
  return;
6280
6534
  }
6281
- setLoading(true);
6282
- listUsers({
6283
- status,
6284
- keyword: keyword.trim() || void 0,
6285
- page,
6286
- limit: PAGE_SIZE
6287
- }).then((res) => {
6288
- setUsers(Array.isArray(res?.list) ? res.list : []);
6289
- setTotal(Number(res?.total) || 0);
6290
- setPendingCount(Number(res?.pending_count) || 0);
6291
- }).catch(toastError).finally(() => setLoading(false));
6292
- }, [
6293
- isAdmin,
6294
- status,
6295
- keyword,
6296
- page
6297
- ]);
6298
- (0, react.useEffect)(reload, [reload]);
6299
- (0, react.useEffect)(() => {
6300
- if (!isAdmin) return;
6301
- getReviewConfig().then((res) => setMode(Number(res?.mode) === 1 ? 1 : 0)).catch(() => setMode(0));
6302
- }, [isAdmin]);
6303
- const switchMode = (next) => {
6304
- if (modeBusy || next === mode) return;
6305
- setModeBusy(true);
6306
- setReviewConfig(next).then(() => {
6307
- setMode(next);
6308
- toast(next === 1 ? "已开启需审核:新用户首次进入知识库将处于待审核状态" : "已关闭审核:新用户首次进入即为正常状态", "success");
6309
- }).catch(toastError).finally(() => setModeBusy(false));
6535
+ if (next === "ollama" && item === "ocr") {
6536
+ setProtocol("ollama");
6537
+ const cur = apiBase.trim();
6538
+ if (cur === "" || PRESET_BASES.includes(cur)) setApiBase(`http://${localModelHost()}:11434`);
6539
+ if (isPresetModel(model)) setModel(OLLAMA_OCR.model);
6540
+ if (prompt.trim() === "") setPrompt(OLLAMA_OCR.prompt);
6541
+ return;
6542
+ }
6543
+ const p = findPreset(item, next);
6544
+ if (!p) return;
6545
+ setApiBase(p.base);
6546
+ if (item === "ocr") setProtocol("openai");
6547
+ if (isPresetModel(model)) setModel(p.model);
6310
6548
  };
6311
- const runAction = () => {
6312
- const { action, user } = confirm;
6313
- setBusy(true);
6314
- action(user.uid).then(() => {
6315
- toast("操作成功", "success");
6316
- setConfirm(null);
6317
- reload();
6318
- }).catch(toastError).finally(() => setBusy(false));
6549
+ const handleTest = () => {
6550
+ if (busy || !validate()) return;
6551
+ setTesting(true);
6552
+ testModelConfig(item, buildPayload()).then((data) => toast(data && data.message || "连通性测试通过", "success")).catch(toastError).finally(() => setTesting(false));
6319
6553
  };
6320
- const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
6321
- const canPrev = page > 1;
6322
- const canNext = page < totalPages;
6323
- if (!isAdmin) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6324
- className: "skai-kb-panel",
6325
- "aria-label": "用户管理",
6326
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6327
- className: "skai-kb-panel-head",
6328
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "用户管理" })
6329
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6330
- className: "skai-kb-sub",
6331
- children: "用户管理仅管理员可用:审核新用户、禁用/启用账号、删除用户与配置新用户审核模式。"
6332
- })]
6333
- });
6334
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6335
- className: "skai-kb-panel",
6336
- "aria-label": "用户管理",
6554
+ const handleSave = () => {
6555
+ if (busy || !validate()) return;
6556
+ setSaving(true);
6557
+ saveModelConfig(item, buildPayload()).then((data) => {
6558
+ toast("已保存,配置立即生效", "success");
6559
+ onSaved?.(data);
6560
+ }).catch(toastError).finally(() => setSaving(false));
6561
+ };
6562
+ const handleReset = () => {
6563
+ if (busy) return;
6564
+ setConfirmReset(true);
6565
+ };
6566
+ /** 确认弹窗确认后执行恢复默认:清除设置页覆盖,回落部署环境变量配置 */
6567
+ const doReset = () => {
6568
+ setResetting(true);
6569
+ saveModelConfig(item, null).then((data) => {
6570
+ setConfirmReset(false);
6571
+ toast("已恢复默认配置", "success");
6572
+ onSaved?.(data);
6573
+ }).catch(toastError).finally(() => setResetting(false));
6574
+ };
6575
+ const activeHint = findPreset(item, preset)?.hint || (preset === "local" ? LOCAL_HINT[item] : preset === "ollama" ? OLLAMA_HINT : "");
6576
+ if (!open) return null;
6577
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
6578
+ title: `编辑模型服务 · ${ITEM_NAMES[item] || item}`,
6579
+ onClose,
6580
+ escClose: !confirmReset,
6581
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6582
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6583
+ type: "button",
6584
+ className: "skai-kb-btn",
6585
+ disabled: busy,
6586
+ onClick: handleReset,
6587
+ children: resetting ? "恢复中…" : "恢复默认"
6588
+ }),
6589
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6590
+ type: "button",
6591
+ className: "skai-kb-btn",
6592
+ disabled: busy,
6593
+ onClick: handleTest,
6594
+ children: testing ? "测试中…" : "测试连接"
6595
+ }),
6596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6597
+ type: "button",
6598
+ className: "skai-kb-btn skai-kb-btn-primary",
6599
+ disabled: busy,
6600
+ onClick: handleSave,
6601
+ children: saving ? "保存中…" : "保存"
6602
+ })
6603
+ ] }),
6337
6604
  children: [
6338
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6339
- className: "skai-kb-panel-head",
6340
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", { children: ["用户管理", pendingCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6341
- className: "skai-kb-badge skai-kb-badge-amber",
6342
- children: [pendingCount, " 待审核"]
6343
- }) : null] })
6605
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6606
+ className: "skai-kb-field",
6607
+ children: [
6608
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6609
+ className: "skai-kb-field-label",
6610
+ children: "服务商预设"
6611
+ }),
6612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6613
+ className: "skai-kb-select",
6614
+ value: preset,
6615
+ onChange: (e) => applyPreset(e.target.value),
6616
+ children: [
6617
+ (PROVIDER_PRESETS[item] || []).map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6618
+ value: p.value,
6619
+ children: p.label
6620
+ }, p.value)),
6621
+ item === "ocr" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6622
+ value: "ollama",
6623
+ children: "本地部署(ollama · glm-ocr容器)"
6624
+ }) : null,
6625
+ item === "embedding" || item === "rerank" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6626
+ value: "local",
6627
+ children: "本地部署(Infinity · bge 容器)"
6628
+ }) : null,
6629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6630
+ value: "custom",
6631
+ children: "自定义服务商"
6632
+ })
6633
+ ]
6634
+ }),
6635
+ activeHint ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6636
+ className: "skai-kb-sub",
6637
+ children: activeHint
6638
+ }) : null
6639
+ ]
6344
6640
  }),
6345
6641
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6346
- className: "skai-kb-user-review-mode",
6347
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6348
- className: "skai-kb-sub",
6349
- style: { padding: 0 },
6350
- children: "新用户默认审核模式(仅影响之后首次进入的新用户,不影响存量用户)"
6351
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6352
- className: "skai-kb-row-actions",
6353
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6354
- type: "button",
6355
- className: `skai-kb-btn skai-kb-btn-sm ${mode === 1 ? "skai-kb-btn-primary" : ""}`,
6356
- disabled: mode === null || modeBusy,
6357
- onClick: () => switchMode(1),
6358
- children: "需审核"
6359
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6360
- type: "button",
6361
- className: `skai-kb-btn skai-kb-btn-sm ${mode === 0 ? "skai-kb-btn-primary" : ""}`,
6362
- disabled: mode === null || modeBusy,
6363
- onClick: () => switchMode(0),
6364
- children: "无需审核"
6365
- })]
6366
- })]
6642
+ className: "skai-kb-field",
6643
+ children: [
6644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6645
+ className: "skai-kb-field-label",
6646
+ children: "API 地址 *"
6647
+ }),
6648
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6649
+ className: "skai-kb-input",
6650
+ value: apiBase,
6651
+ placeholder: "https://api.siliconflow.cn/v1",
6652
+ onChange: (e) => setApiBase(e.target.value)
6653
+ }),
6654
+ fieldErrors.api_base ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6655
+ className: "skai-kb-field-error",
6656
+ children: fieldErrors.api_base
6657
+ }) : null
6658
+ ]
6367
6659
  }),
6368
6660
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6369
- className: "skai-kb-user-toolbar",
6370
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6371
- className: "skai-kb-row-actions",
6372
- children: STATUS_FILTERS.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6373
- type: "button",
6374
- className: `skai-kb-btn skai-kb-btn-sm ${status === f.value ? "skai-kb-btn-primary" : ""}`,
6375
- onClick: () => {
6376
- setStatus(f.value);
6377
- setPage(1);
6378
- },
6379
- children: f.label
6380
- }, f.value))
6381
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6382
- className: "skai-kb-input skai-kb-user-search",
6383
- placeholder: "搜索 UID 或手机号片段",
6661
+ className: "skai-kb-field",
6662
+ children: [
6663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6664
+ className: "skai-kb-field-label",
6665
+ children: "API 密钥"
6666
+ }),
6667
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6668
+ className: "skai-kb-input",
6669
+ value: apiKey,
6670
+ placeholder: "留空沿用当前密钥",
6671
+ onChange: (e) => setApiKey(e.target.value)
6672
+ }),
6673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6674
+ className: "skai-kb-sub",
6675
+ children: "密钥经后端掩码展示(如 sk-a****abcd);留空或保持掩码串均表示沿用当前生效密钥,填写新值即更新"
6676
+ })
6677
+ ]
6678
+ }),
6679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6680
+ className: "skai-kb-field",
6681
+ children: [
6682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6683
+ className: "skai-kb-field-label",
6684
+ children: "模型名 *"
6685
+ }),
6686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6687
+ className: "skai-kb-input",
6688
+ value: model,
6689
+ placeholder: item === "ocr" ? "如 Qwen/Qwen2.5-VL-32B-Instruct" : "如 BAAI/bge-large-zh-v1.5",
6690
+ onChange: (e) => setModel(e.target.value)
6691
+ }),
6692
+ fieldErrors.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6693
+ className: "skai-kb-field-error",
6694
+ children: fieldErrors.model
6695
+ }) : null
6696
+ ]
6697
+ }),
6698
+ item === "ocr" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6699
+ className: "skai-kb-field",
6700
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6701
+ className: "skai-kb-field-label",
6702
+ children: "调用协议"
6703
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6704
+ className: "skai-kb-select",
6705
+ value: protocol,
6706
+ onChange: (e) => setProtocol(e.target.value),
6707
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6708
+ value: "openai",
6709
+ children: "OpenAI 兼容公网"
6710
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6711
+ value: "ollama",
6712
+ children: "本地 Ollama"
6713
+ })]
6714
+ })]
6715
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6716
+ className: "skai-kb-field",
6717
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6718
+ className: "skai-kb-field-label",
6719
+ children: "识别提示词"
6720
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6721
+ className: "skai-kb-textarea",
6722
+ rows: 3,
6723
+ value: prompt,
6724
+ placeholder: "留空使用内置默认提示词",
6725
+ onChange: (e) => setPrompt(e.target.value)
6726
+ })]
6727
+ })] }) : null,
6728
+ item === "embedding" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6729
+ className: "skai-kb-field-row",
6730
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6731
+ className: "skai-kb-field",
6732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6733
+ className: "skai-kb-field-label",
6734
+ children: "向量维度"
6735
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6736
+ className: "skai-kb-input",
6737
+ type: "number",
6738
+ min: 0,
6739
+ value: dimension,
6740
+ placeholder: "留空沿用默认 1024",
6741
+ onChange: (e) => setDimension(e.target.value)
6742
+ })]
6743
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6744
+ className: "skai-kb-field",
6745
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6746
+ className: "skai-kb-field-label",
6747
+ children: "批量大小"
6748
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6749
+ className: "skai-kb-input",
6750
+ type: "number",
6751
+ min: 0,
6752
+ value: batchSize,
6753
+ placeholder: "留空沿用默认",
6754
+ onChange: (e) => setBatchSize(e.target.value)
6755
+ })]
6756
+ })]
6757
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6758
+ className: "skai-kb-field-row",
6759
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6760
+ className: "skai-kb-field",
6761
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6762
+ className: "skai-kb-field-label",
6763
+ children: "超时秒数"
6764
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6765
+ className: "skai-kb-input",
6766
+ type: "number",
6767
+ min: 0,
6768
+ value: timeoutSec,
6769
+ placeholder: "留空沿用默认",
6770
+ onChange: (e) => setTimeoutSec(e.target.value)
6771
+ })]
6772
+ })
6773
+ })] }) : null,
6774
+ item === "rerank" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6775
+ className: "skai-kb-field-row",
6776
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6777
+ className: "skai-kb-field",
6778
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6779
+ className: "skai-kb-field-label",
6780
+ children: "重排后保留条数 top_n"
6781
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6782
+ className: "skai-kb-input",
6783
+ type: "number",
6784
+ min: 0,
6785
+ value: topN,
6786
+ placeholder: "留空沿用默认",
6787
+ onChange: (e) => setTopN(e.target.value)
6788
+ })]
6789
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6790
+ className: "skai-kb-field",
6791
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6792
+ className: "skai-kb-field-label",
6793
+ children: "超时秒数"
6794
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6795
+ className: "skai-kb-input",
6796
+ type: "number",
6797
+ min: 0,
6798
+ value: timeoutSec,
6799
+ placeholder: "留空沿用默认",
6800
+ onChange: (e) => setTimeoutSec(e.target.value)
6801
+ })]
6802
+ })]
6803
+ }) : null,
6804
+ item === "ocr" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6805
+ className: "skai-kb-field-row",
6806
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6807
+ className: "skai-kb-field",
6808
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6809
+ className: "skai-kb-field-label",
6810
+ children: "单文档识别页数上限"
6811
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6812
+ className: "skai-kb-input",
6813
+ type: "number",
6814
+ min: 0,
6815
+ value: maxPages,
6816
+ placeholder: "0=不限制",
6817
+ onChange: (e) => setMaxPages(e.target.value)
6818
+ })]
6819
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6820
+ className: "skai-kb-field",
6821
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6822
+ className: "skai-kb-field-label",
6823
+ children: "超时秒数"
6824
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6825
+ className: "skai-kb-input",
6826
+ type: "number",
6827
+ min: 0,
6828
+ value: timeoutSec,
6829
+ placeholder: "留空沿用默认",
6830
+ onChange: (e) => setTimeoutSec(e.target.value)
6831
+ })]
6832
+ })]
6833
+ }) : null
6834
+ ]
6835
+ }), confirmReset ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
6836
+ title: "恢复默认配置",
6837
+ message: `确定恢复「${ITEM_NAMES[item] || item}」的默认配置吗?将清除设置页覆盖并回落到部署环境变量配置。`,
6838
+ confirmText: "恢复默认",
6839
+ danger: false,
6840
+ busy: resetting,
6841
+ onConfirm: doReset,
6842
+ onClose: () => setConfirmReset(false)
6843
+ }) : null] });
6844
+ }
6845
+ //#endregion
6846
+ //#region src/components/settings/ModelConfigHelpModal.jsx
6847
+ /**
6848
+ * 模型服务「配置步骤帮助」弹窗(设置页「模型服务」区块各行「配置说明」按钮入口)
6849
+ *
6850
+ * 分节内容:使用说明(预设回填机制 + 各服务项已收录名单 + 1024 维约束)→
6851
+ * 各服务商配置方法(按服务商分节覆盖全部已收录预设:硅基流动 / 阿里云百炼 /
6852
+ * 百度千帆 / 智谱 / DeepSeek / 火山方舟 / Kimi / 腾讯混元,每节含控制台入口、
6853
+ * 密钥创建步骤、API 地址与各服务项推荐模型)→ 本地部署 Infinity(向量化 / 重排,
6854
+ * bge 容器)→ 本地部署(ollama · glm-ocr容器,自动拉取 glm-ocr:q8_0)→ 密钥与生效方式;
6855
+ * 服务商能力与推荐模型与 ModelServiceModal 的 PROVIDER_PRESETS 保持一致;
6856
+ * 样式用 knowledge.css 末尾的 skai-kb-help-* 局部类
6857
+ */
6858
+ function ModelConfigHelpModal({ onClose }) {
6859
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
6860
+ title: "配置步骤帮助",
6861
+ onClose,
6862
+ width: 560,
6863
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6864
+ type: "button",
6865
+ className: "skai-kb-btn skai-kb-btn-primary",
6866
+ onClick: onClose,
6867
+ children: "关闭"
6868
+ }),
6869
+ children: [
6870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6871
+ className: "skai-kb-help-section",
6872
+ children: [
6873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
6874
+ className: "skai-kb-help-title",
6875
+ children: "使用说明"
6876
+ }),
6877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6878
+ className: "skai-kb-help-text",
6879
+ children: "编辑弹窗「服务商预设」选择服务商后自动回填 API 地址与推荐模型,一般只需粘贴密钥 → 「测试连接」→ 保存;选择「自定义服务商」可手动填写任意 OpenAI 兼容地址。"
6880
+ }),
6881
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6882
+ className: "skai-kb-help-text",
6883
+ children: "各服务项已收录:向量化——硅基流动 / 阿里云百炼 / 百度千帆 / 本地部署;重排——硅基流动 / 智谱 / 本地部署;OCR——DeepSeek / 火山方舟 / 硅基流动 / 阿里云百炼 / 智谱 / Kimi / 腾讯混元 / 本地部署。向量化所选模型须为 1024 维(与库内向量列一致),各预设已按此筛选。"
6884
+ })
6885
+ ]
6886
+ }),
6887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6888
+ className: "skai-kb-help-section",
6889
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
6890
+ className: "skai-kb-help-title",
6891
+ children: "硅基流动 SiliconFlow(向量化 / 重排 / OCR)"
6892
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
6893
+ className: "skai-kb-help-list",
6894
+ children: [
6895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
6896
+ "注册并登录",
6897
+ " ",
6898
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
6899
+ className: "skai-kb-help-link",
6900
+ href: "https://cloud.siliconflow.cn/",
6901
+ target: "_blank",
6902
+ rel: "noreferrer",
6903
+ children: "https://cloud.siliconflow.cn/"
6904
+ })
6905
+ ] }),
6906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "控制台左侧「API 密钥」→「新建 API 密钥」并复制" }),
6907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
6908
+ "API 地址统一",
6909
+ " ",
6910
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6911
+ className: "skai-kb-help-code",
6912
+ children: "https://api.siliconflow.cn/v1"
6913
+ }),
6914
+ ":向量化模型 ",
6915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6916
+ className: "skai-kb-help-code",
6917
+ children: "BAAI/bge-large-zh-v1.5"
6918
+ }),
6919
+ "(1024 维);重排模型 ",
6920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6921
+ className: "skai-kb-help-code",
6922
+ children: "BAAI/bge-reranker-v2-m3"
6923
+ }),
6924
+ ";OCR 视觉模型",
6925
+ " ",
6926
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6927
+ className: "skai-kb-help-code",
6928
+ children: "Qwen/Qwen2.5-VL-32B-Instruct"
6929
+ }),
6930
+ "(或其它支持视觉的模型)"
6931
+ ] }),
6932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "粘贴 API 密钥 → 点击「测试连接」→ 通过后保存" })
6933
+ ]
6934
+ })]
6935
+ }),
6936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6937
+ className: "skai-kb-help-section",
6938
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
6939
+ className: "skai-kb-help-title",
6940
+ children: "阿里云百炼(向量化 / OCR)"
6941
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
6942
+ className: "skai-kb-help-list",
6943
+ children: [
6944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
6945
+ "登录阿里云并开通模型服务",
6946
+ " ",
6947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
6948
+ className: "skai-kb-help-link",
6949
+ href: "https://bailian.console.aliyun.com/",
6950
+ target: "_blank",
6951
+ rel: "noreferrer",
6952
+ children: "https://bailian.console.aliyun.com/"
6953
+ })
6954
+ ] }),
6955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "控制台右上「API-KEY」→ 创建并复制 API Key" }),
6956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
6957
+ "API 地址",
6958
+ " ",
6959
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6960
+ className: "skai-kb-help-code",
6961
+ children: "https://dashscope.aliyuncs.com/compatible-mode/v1"
6962
+ }),
6963
+ ":向量化模型 ",
6964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6965
+ className: "skai-kb-help-code",
6966
+ children: "text-embedding-v4"
6967
+ }),
6968
+ "(默认 1024 维,与库内向量列一致);OCR 视觉模型",
6969
+ " ",
6970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
6971
+ className: "skai-kb-help-code",
6972
+ children: "qwen-vl-max"
6973
+ }),
6974
+ "(或 qwen3-vl 系列)"
6975
+ ] })
6976
+ ]
6977
+ })]
6978
+ }),
6979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6980
+ className: "skai-kb-help-section",
6981
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
6982
+ className: "skai-kb-help-title",
6983
+ children: "百度千帆(向量化)"
6984
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
6985
+ className: "skai-kb-help-list",
6986
+ children: [
6987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
6988
+ "登录百度智能云并开通千帆 ModelBuilder 服务",
6989
+ " ",
6990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
6991
+ className: "skai-kb-help-link",
6992
+ href: "https://console.bce.baidu.com/qianfan/",
6993
+ target: "_blank",
6994
+ rel: "noreferrer",
6995
+ children: "https://console.bce.baidu.com/qianfan/"
6996
+ })
6997
+ ] }),
6998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
6999
+ "在 IAM「API Key」页",
7000
+ " ",
7001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7002
+ className: "skai-kb-help-link",
7003
+ href: "https://console.bce.baidu.com/iam/#/iam/apikey/list",
7004
+ target: "_blank",
7005
+ rel: "noreferrer",
7006
+ children: "console.bce.baidu.com/iam/#/iam/apikey/list"
7007
+ }),
7008
+ " ",
7009
+ "创建并复制(v2 接口按 Bearer 方式认证)"
7010
+ ] }),
7011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7012
+ "API 地址 ",
7013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7014
+ className: "skai-kb-help-code",
7015
+ children: "https://qianfan.baidubce.com/v2"
7016
+ }),
7017
+ ":向量化模型 ",
7018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7019
+ className: "skai-kb-help-code",
7020
+ children: "bge-large-zh"
7021
+ }),
7022
+ "(1024 维)"
7023
+ ] })
7024
+ ]
7025
+ })]
7026
+ }),
7027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7028
+ className: "skai-kb-help-section",
7029
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7030
+ className: "skai-kb-help-title",
7031
+ children: "智谱 AI(重排 / OCR)"
7032
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
7033
+ className: "skai-kb-help-list",
7034
+ children: [
7035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7036
+ "注册并登录",
7037
+ " ",
7038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7039
+ className: "skai-kb-help-link",
7040
+ href: "https://open.bigmodel.cn/",
7041
+ target: "_blank",
7042
+ rel: "noreferrer",
7043
+ children: "https://open.bigmodel.cn/"
7044
+ })
7045
+ ] }),
7046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "控制台「API Keys」页 → 创建并复制" }),
7047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7048
+ "API 地址",
7049
+ " ",
7050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7051
+ className: "skai-kb-help-code",
7052
+ children: "https://open.bigmodel.cn/api/paas/v4"
7053
+ }),
7054
+ ":重排模型 ",
7055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7056
+ className: "skai-kb-help-code",
7057
+ children: "rerank"
7058
+ }),
7059
+ "(Cohere 兼容 /rerank 端点);OCR 视觉模型",
7060
+ " ",
7061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7062
+ className: "skai-kb-help-code",
7063
+ children: "glm-4.6v-flash"
7064
+ }),
7065
+ "(免费)/",
7066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7067
+ className: "skai-kb-help-code",
7068
+ children: "glm-4.6v"
7069
+ }),
7070
+ " /",
7071
+ " ",
7072
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7073
+ className: "skai-kb-help-code",
7074
+ children: "GLM-OCR"
7075
+ })
7076
+ ] })
7077
+ ]
7078
+ })]
7079
+ }),
7080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7081
+ className: "skai-kb-help-section",
7082
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7083
+ className: "skai-kb-help-title",
7084
+ children: "DeepSeek(OCR)"
7085
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
7086
+ className: "skai-kb-help-list",
7087
+ children: [
7088
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7089
+ "注册并登录",
7090
+ " ",
7091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7092
+ className: "skai-kb-help-link",
7093
+ href: "https://platform.deepseek.com/",
7094
+ target: "_blank",
7095
+ rel: "noreferrer",
7096
+ children: "https://platform.deepseek.com/"
7097
+ })
7098
+ ] }),
7099
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "左侧「API Keys」→ 创建并复制(需先充值,按量计费)" }),
7100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7101
+ "API 地址 ",
7102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7103
+ className: "skai-kb-help-code",
7104
+ children: "https://api.deepseek.com"
7105
+ }),
7106
+ ":模型 ",
7107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7108
+ className: "skai-kb-help-code",
7109
+ children: "deepseek-flash"
7110
+ }),
7111
+ "(原生多模态,支持 image_url 传图;DeepSeek 不提供向量化 / 重排服务)"
7112
+ ] })
7113
+ ]
7114
+ })]
7115
+ }),
7116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7117
+ className: "skai-kb-help-section",
7118
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7119
+ className: "skai-kb-help-title",
7120
+ children: "火山方舟(OCR)"
7121
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
7122
+ className: "skai-kb-help-list",
7123
+ children: [
7124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7125
+ "登录火山引擎(需实名认证)并进入方舟控制台",
7126
+ " ",
7127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7128
+ className: "skai-kb-help-link",
7129
+ href: "https://console.volcengine.com/ark/",
7130
+ target: "_blank",
7131
+ rel: "noreferrer",
7132
+ children: "https://console.volcengine.com/ark/"
7133
+ })
7134
+ ] }),
7135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "左侧「API Key 管理」→「创建 API Key」并复制" }),
7136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7137
+ "模型名直调(无需创建推理接入点):API 地址",
7138
+ " ",
7139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7140
+ className: "skai-kb-help-code",
7141
+ children: "https://ark.cn-beijing.volces.com/api/v3"
7142
+ }),
7143
+ ",OCR 模型 ",
7144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7145
+ className: "skai-kb-help-code",
7146
+ children: "doubao-seed-2-1-pro-260915"
7147
+ }),
7148
+ "(旗舰多模态)"
7149
+ ] })
7150
+ ]
7151
+ })]
7152
+ }),
7153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7154
+ className: "skai-kb-help-section",
7155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7156
+ className: "skai-kb-help-title",
7157
+ children: "月之暗面 Kimi(OCR)"
7158
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
7159
+ className: "skai-kb-help-list",
7160
+ children: [
7161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7162
+ "注册并登录",
7163
+ " ",
7164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7165
+ className: "skai-kb-help-link",
7166
+ href: "https://platform.moonshot.cn/",
7167
+ target: "_blank",
7168
+ rel: "noreferrer",
7169
+ children: "https://platform.moonshot.cn/"
7170
+ })
7171
+ ] }),
7172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "左侧「API Key 管理」→「新建」并复制" }),
7173
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7174
+ "API 地址 ",
7175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7176
+ className: "skai-kb-help-code",
7177
+ children: "https://api.moonshot.cn/v1"
7178
+ }),
7179
+ ":OCR 模型 ",
7180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7181
+ className: "skai-kb-help-code",
7182
+ children: "kimi-k3.1-pro"
7183
+ }),
7184
+ "(原生视觉)"
7185
+ ] })
7186
+ ]
7187
+ })]
7188
+ }),
7189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7190
+ className: "skai-kb-help-section",
7191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7192
+ className: "skai-kb-help-title",
7193
+ children: "腾讯混元(OCR)"
7194
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
7195
+ className: "skai-kb-help-list",
7196
+ children: [
7197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7198
+ "登录腾讯云控制台",
7199
+ " ",
7200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7201
+ className: "skai-kb-help-link",
7202
+ href: "https://console.cloud.tencent.com/hunyuan",
7203
+ target: "_blank",
7204
+ rel: "noreferrer",
7205
+ children: "https://console.cloud.tencent.com/hunyuan"
7206
+ }),
7207
+ " ",
7208
+ "并开通混元模型服务"
7209
+ ] }),
7210
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "左侧「API 密钥」→「创建 API Key」并复制" }),
7211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
7212
+ "API 地址",
7213
+ " ",
7214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7215
+ className: "skai-kb-help-code",
7216
+ children: "https://api.hunyuan.cloud.tencent.com/v1"
7217
+ }),
7218
+ ":OCR 模型 ",
7219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7220
+ className: "skai-kb-help-code",
7221
+ children: "hunyuan-vision"
7222
+ })
7223
+ ] })
7224
+ ]
7225
+ })]
7226
+ }),
7227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7228
+ className: "skai-kb-help-section",
7229
+ children: [
7230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7231
+ className: "skai-kb-help-title",
7232
+ children: "本地部署(向量化 / 重排)"
7233
+ }),
7234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7235
+ className: "skai-kb-help-text",
7236
+ children: [
7237
+ "部署栈自带 Infinity 服务(dev/docker-compose.yml 的",
7238
+ " ",
7239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7240
+ className: "skai-kb-help-code",
7241
+ children: "bge"
7242
+ }),
7243
+ " 容器,端口",
7244
+ " ",
7245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7246
+ className: "skai-kb-help-code",
7247
+ children: "7997"
7248
+ }),
7249
+ ",同时加载向量化与重排两个模型),无需 API 密钥;编辑弹窗选择预设",
7250
+ " ",
7251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7252
+ className: "skai-kb-help-code",
7253
+ children: "本地部署(Infinity · bge 容器)"
7254
+ }),
7255
+ "即可自动回填,默认主机与当前 API 地址一致(未配置服务器时为 localhost)。"
7256
+ ]
7257
+ }),
7258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7259
+ className: "skai-kb-help-text",
7260
+ children: [
7261
+ "向量化:API 地址 ",
7262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7263
+ className: "skai-kb-help-code",
7264
+ children: "http://localhost:7997"
7265
+ }),
7266
+ ",模型",
7267
+ " ",
7268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7269
+ className: "skai-kb-help-code",
7270
+ children: "BAAI/bge-large-zh-v1.5"
7271
+ }),
7272
+ "(1024 维);重排:API 地址",
7273
+ " ",
7274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7275
+ className: "skai-kb-help-code",
7276
+ children: "http://localhost:7997"
7277
+ }),
7278
+ ",模型",
7279
+ " ",
7280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7281
+ className: "skai-kb-help-code",
7282
+ children: "BAAI/bge-reranker-v2-m3"
7283
+ }),
7284
+ ";同 Docker 网络容器内调用时地址改用",
7285
+ " ",
7286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7287
+ className: "skai-kb-help-code",
7288
+ children: "http://bge:7997"
7289
+ }),
7290
+ "。"
7291
+ ]
7292
+ })
7293
+ ]
7294
+ }),
7295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7296
+ className: "skai-kb-help-section",
7297
+ children: [
7298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7299
+ className: "skai-kb-help-title",
7300
+ children: "本地部署(ollama · glm-ocr容器,OCR)"
7301
+ }),
7302
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7303
+ className: "skai-kb-help-text",
7304
+ children: [
7305
+ "部署栈自带 ",
7306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7307
+ className: "skai-kb-help-code",
7308
+ children: "ollama"
7309
+ }),
7310
+ " 容器(dev/docker-compose.yml, 端口 ",
7311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7312
+ className: "skai-kb-help-code",
7313
+ children: "11434"
7314
+ }),
7315
+ ",启动时自动拉取 ",
7316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7317
+ className: "skai-kb-help-code",
7318
+ children: "glm-ocr:q8_0"
7319
+ }),
7320
+ ");编辑弹窗选择预设 「本地部署(ollama · glm-ocr容器)」自动回填地址(主机与当前 API 地址一致)与协议",
7321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7322
+ className: "skai-kb-help-code",
7323
+ children: "ollama"
7324
+ }),
7325
+ ",OCR 模型填 ",
7326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7327
+ className: "skai-kb-help-code",
7328
+ children: "glm-ocr:q8_0"
7329
+ }),
7330
+ ";手动填写时 API 地址格式为 ",
7331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7332
+ className: "skai-kb-help-code",
7333
+ children: "http://主机:11434"
7334
+ }),
7335
+ "。"
7336
+ ]
7337
+ }),
7338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7339
+ className: "skai-kb-help-text",
7340
+ children: [
7341
+ "GLM-OCR 为任务专用模型,识别提示词需用官方触发前缀:全文",
7342
+ " ",
7343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7344
+ className: "skai-kb-help-code",
7345
+ children: "Text Recognition:"
7346
+ }),
7347
+ "、表格",
7348
+ " ",
7349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7350
+ className: "skai-kb-help-code",
7351
+ children: "Table Recognition:"
7352
+ }),
7353
+ "、图",
7354
+ " ",
7355
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7356
+ className: "skai-kb-help-code",
7357
+ children: "Figure Recognition:"
7358
+ }),
7359
+ ";选择预设时已在提示词为空的情况下回填默认前缀",
7360
+ " ",
7361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7362
+ className: "skai-kb-help-code",
7363
+ children: "Text Recognition:"
7364
+ }),
7365
+ ",自定义提示词不受影响。"
7366
+ ]
7367
+ })
7368
+ ]
7369
+ }),
7370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7371
+ className: "skai-kb-help-section",
7372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
7373
+ className: "skai-kb-help-title",
7374
+ children: "密钥与生效方式"
7375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7376
+ className: "skai-kb-help-text",
7377
+ children: [
7378
+ "编辑时密钥留空或保留掩码串(含 ",
7379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7380
+ className: "skai-kb-help-code",
7381
+ children: "****"
7382
+ }),
7383
+ ")表示沿用当前生效密钥,填入新值即更新;保存后立即生效,无需重启服务;「恢复默认」清除设置页覆盖并回落到部署环境变量配置。"
7384
+ ]
7385
+ })]
7386
+ })
7387
+ ]
7388
+ });
7389
+ }
7390
+ //#endregion
7391
+ //#region src/components/settings/UserManagePanel.jsx
7392
+ /**
7393
+ * 用户管理区块(设置页;仅管理员可见可操作,非管理员仅展示状态说明)
7394
+ *
7395
+ * - 用户列表:昵称(用户中心批量获取,仅展示)/ UID / 脱敏手机号 / 状态标签 /
7396
+ * 注册时间 / 最近审核信息;状态筛选(全部/待审核/正常/禁用)+ 关键词(UID 或
7397
+ * 手机号片段)+ 分页;管理员行标注「管理员」且不可被审核/禁用/删除
7398
+ * - 操作:审核通过(待审核→正常)、禁用(同步吊销其全部插件 Token)、启用
7399
+ * (禁用→正常)、删除(物理删除状态与授权,保留其知识库,悬空所有者由管理员
7400
+ * 经「修改所有者」接管);操作均写后端审计日志
7401
+ * - 新用户默认审核模式:需审核 / 无需审核(默认);切换仅影响之后首次验签的新用户
7402
+ */
7403
+ const PAGE_SIZE = 20;
7404
+ /** 用户状态 → 展示文案与徽标色系(0 待审核 / 1 正常 / 2 禁用) */
7405
+ const STATUS_VIEW = {
7406
+ 0: {
7407
+ text: "待审核",
7408
+ badge: "skai-kb-badge-amber"
7409
+ },
7410
+ 1: {
7411
+ text: "正常",
7412
+ badge: "skai-kb-badge-green"
7413
+ },
7414
+ 2: {
7415
+ text: "禁用",
7416
+ badge: "skai-kb-badge-red"
7417
+ }
7418
+ };
7419
+ const STATUS_FILTERS = [
7420
+ {
7421
+ value: -1,
7422
+ label: "全部"
7423
+ },
7424
+ {
7425
+ value: 0,
7426
+ label: "待审核"
7427
+ },
7428
+ {
7429
+ value: 1,
7430
+ label: "正常"
7431
+ },
7432
+ {
7433
+ value: 2,
7434
+ label: "禁用"
7435
+ }
7436
+ ];
7437
+ function UserManagePanel({ isAdmin }) {
7438
+ const [loading, setLoading] = (0, react.useState)(true);
7439
+ const [users, setUsers] = (0, react.useState)([]);
7440
+ const [total, setTotal] = (0, react.useState)(0);
7441
+ const [pendingCount, setPendingCount] = (0, react.useState)(0);
7442
+ const [status, setStatus] = (0, react.useState)(-1);
7443
+ const [keyword, setKeyword] = (0, react.useState)("");
7444
+ const [page, setPage] = (0, react.useState)(1);
7445
+ const [mode, setMode] = (0, react.useState)(null);
7446
+ const [modeBusy, setModeBusy] = (0, react.useState)(false);
7447
+ const [busy, setBusy] = (0, react.useState)(false);
7448
+ const [confirm, setConfirm] = (0, react.useState)(null);
7449
+ const { infoMap } = useMemberInfo(users.map((u) => u.uid));
7450
+ const reload = (0, react.useCallback)(() => {
7451
+ if (!isAdmin) {
7452
+ setLoading(false);
7453
+ return;
7454
+ }
7455
+ setLoading(true);
7456
+ listUsers({
7457
+ status,
7458
+ keyword: keyword.trim() || void 0,
7459
+ page,
7460
+ limit: PAGE_SIZE
7461
+ }).then((res) => {
7462
+ setUsers(Array.isArray(res?.list) ? res.list : []);
7463
+ setTotal(Number(res?.total) || 0);
7464
+ setPendingCount(Number(res?.pending_count) || 0);
7465
+ }).catch(toastError).finally(() => setLoading(false));
7466
+ }, [
7467
+ isAdmin,
7468
+ status,
7469
+ keyword,
7470
+ page
7471
+ ]);
7472
+ (0, react.useEffect)(reload, [reload]);
7473
+ (0, react.useEffect)(() => {
7474
+ if (!isAdmin) return;
7475
+ getReviewConfig().then((res) => setMode(Number(res?.mode) === 1 ? 1 : 0)).catch(() => setMode(0));
7476
+ }, [isAdmin]);
7477
+ const switchMode = (next) => {
7478
+ if (modeBusy || next === mode) return;
7479
+ setModeBusy(true);
7480
+ setReviewConfig(next).then(() => {
7481
+ setMode(next);
7482
+ toast(next === 1 ? "已开启需审核:新用户首次进入知识库将处于待审核状态" : "已关闭审核:新用户首次进入即为正常状态", "success");
7483
+ }).catch(toastError).finally(() => setModeBusy(false));
7484
+ };
7485
+ const runAction = () => {
7486
+ const { action, user } = confirm;
7487
+ setBusy(true);
7488
+ action(user.uid).then(() => {
7489
+ toast("操作成功", "success");
7490
+ setConfirm(null);
7491
+ reload();
7492
+ }).catch(toastError).finally(() => setBusy(false));
7493
+ };
7494
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
7495
+ const canPrev = page > 1;
7496
+ const canNext = page < totalPages;
7497
+ if (!isAdmin) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7498
+ className: "skai-kb-panel",
7499
+ "aria-label": "用户管理",
7500
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7501
+ className: "skai-kb-panel-head",
7502
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "用户管理" })
7503
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7504
+ className: "skai-kb-sub",
7505
+ children: "用户管理仅管理员可用:审核新用户、禁用/启用账号、删除用户与配置新用户审核模式。"
7506
+ })]
7507
+ });
7508
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7509
+ className: "skai-kb-panel",
7510
+ "aria-label": "用户管理",
7511
+ children: [
7512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7513
+ className: "skai-kb-panel-head",
7514
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", { children: ["用户管理", pendingCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7515
+ className: "skai-kb-badge skai-kb-badge-amber",
7516
+ children: [pendingCount, " 待审核"]
7517
+ }) : null] })
7518
+ }),
7519
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7520
+ className: "skai-kb-user-review-mode",
7521
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7522
+ className: "skai-kb-sub",
7523
+ style: { padding: 0 },
7524
+ children: "新用户默认审核模式(仅影响之后首次进入的新用户,不影响存量用户)"
7525
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7526
+ className: "skai-kb-row-actions",
7527
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7528
+ type: "button",
7529
+ className: `skai-kb-btn skai-kb-btn-sm ${mode === 1 ? "skai-kb-btn-primary" : ""}`,
7530
+ disabled: mode === null || modeBusy,
7531
+ onClick: () => switchMode(1),
7532
+ children: "需审核"
7533
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7534
+ type: "button",
7535
+ className: `skai-kb-btn skai-kb-btn-sm ${mode === 0 ? "skai-kb-btn-primary" : ""}`,
7536
+ disabled: mode === null || modeBusy,
7537
+ onClick: () => switchMode(0),
7538
+ children: "无需审核"
7539
+ })]
7540
+ })]
7541
+ }),
7542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7543
+ className: "skai-kb-user-toolbar",
7544
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7545
+ className: "skai-kb-row-actions",
7546
+ children: STATUS_FILTERS.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7547
+ type: "button",
7548
+ className: `skai-kb-btn skai-kb-btn-sm ${status === f.value ? "skai-kb-btn-primary" : ""}`,
7549
+ onClick: () => {
7550
+ setStatus(f.value);
7551
+ setPage(1);
7552
+ },
7553
+ children: f.label
7554
+ }, f.value))
7555
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7556
+ className: "skai-kb-input skai-kb-user-search",
7557
+ placeholder: "搜索 UID 或手机号片段",
6384
7558
  value: keyword,
6385
7559
  onChange: (e) => {
6386
7560
  setKeyword(e.target.value);
@@ -6563,10 +7737,14 @@ window.__ModuleLoader__.load({
6563
7737
  * - 页头:标题「设置」+ 右上角「添加服务器」主操作
6564
7738
  * - 服务器配置:多服务器增删改切(云持久化 knowledge.servers / knowledge.serverActive),
6565
7739
  * 当前服务器即 REST 基址与知识库 MCP 接入点,变更经 auth.subscribe 链路自动同步宿主
7740
+ * - 服务器探测:逐个探测已添加服务器(GET /kb/health 免鉴权),校验部署的应用id与版本
7741
+ * 和插件是否匹配,不匹配在该服务器条目下方红色提示(发布机制三处同号)
6566
7742
  * - 管理员:全局角色(昵称/uid/脱敏手机号/添加时间,资料经用户中心批量获取仅展示);
6567
7743
  * 管理员可添加、删除其他管理员(最后一个管理员不可删除);尚无任何管理员时可将
6568
7744
  * 当前登录用户初始化为管理员(首次部署「添加管理员」流程)
6569
7745
  * - 智能体(MCP)同步状态:宿主配置自检 + 手动重新自检 + 接入点展示
7746
+ * - 模型服务(仅管理员):向量化 / 重排 / OCR 的公网模型服务配置(优先推荐硅基流动),
7747
+ * 各行展示配置来源与模型名,支持编辑(含连通性测试、恢复默认)与配置步骤帮助
6570
7748
  * - 账号信息:当前用户角色与 UID(仅展示)
6571
7749
  */
6572
7750
  /** MCP 自检状态 → 展示文案与色系 */
@@ -6611,6 +7789,36 @@ window.__ModuleLoader__.load({
6611
7789
  danger: "skai-kb-badge-red",
6612
7790
  neutral: "skai-kb-badge-gray"
6613
7791
  };
7792
+ /** 模型服务三类配置项(面板行展示名与编辑 key,对应后端 /kb/model-config 的 item) */
7793
+ const MODEL_ITEMS = [
7794
+ {
7795
+ key: "embedding",
7796
+ name: "向量化(Embedding)"
7797
+ },
7798
+ {
7799
+ key: "rerank",
7800
+ name: "重排(Rerank)"
7801
+ },
7802
+ {
7803
+ key: "ocr",
7804
+ name: "OCR(视觉识别)"
7805
+ }
7806
+ ];
7807
+ /** 配置来源 → 展示文案与徽标色系(db=设置页覆盖 / env=环境变量 / unset=未配置) */
7808
+ const MODEL_SOURCE_VIEW = {
7809
+ db: {
7810
+ text: "设置页配置",
7811
+ badge: "skai-kb-badge-green"
7812
+ },
7813
+ env: {
7814
+ text: "环境变量配置",
7815
+ badge: "skai-kb-badge-blue"
7816
+ },
7817
+ unset: {
7818
+ text: "未配置",
7819
+ badge: "skai-kb-badge-gray"
7820
+ }
7821
+ };
6614
7822
  function SettingsPage() {
6615
7823
  const [, forceUpdate] = (0, react.useState)(0);
6616
7824
  const [editing, setEditing] = (0, react.useState)(null);
@@ -6629,48 +7837,7 @@ window.__ModuleLoader__.load({
6629
7837
  const [busy, setBusy] = (0, react.useState)(false);
6630
7838
  const servers = getServers();
6631
7839
  const active = getActiveServer();
6632
- const activeBase = active ? active.api_base : "";
6633
7840
  const cloudOk = isCloudPersistAvailable();
6634
- const pluginVersion = String(manifest.version || "");
6635
- const [backendVersion, setBackendVersion] = (0, react.useState)({
6636
- checking: false,
6637
- version: "",
6638
- error: ""
6639
- });
6640
- (0, react.useEffect)(() => {
6641
- if (!activeBase) {
6642
- setBackendVersion({
6643
- checking: false,
6644
- version: "",
6645
- error: ""
6646
- });
6647
- return;
6648
- }
6649
- let cancelled = false;
6650
- setBackendVersion({
6651
- checking: true,
6652
- version: "",
6653
- error: ""
6654
- });
6655
- getHealth().then((health) => {
6656
- if (!cancelled) setBackendVersion({
6657
- checking: false,
6658
- version: String(health?.version || ""),
6659
- error: ""
6660
- });
6661
- }).catch((error) => {
6662
- if (!cancelled) setBackendVersion({
6663
- checking: false,
6664
- version: "",
6665
- error: error && error.msg || "无法获取后端版本"
6666
- });
6667
- });
6668
- return () => {
6669
- cancelled = true;
6670
- };
6671
- }, [activeBase]);
6672
- const backendVer = backendVersion.version;
6673
- const versionMismatch = Boolean(backendVer) && backendVer !== "dev" && backendVer.replace(/^v/i, "") !== pluginVersion.replace(/^v/i, "");
6674
7841
  const { infoMap } = useMemberInfo(adminInfo.list.map((m) => m.uid));
6675
7842
  (0, react.useEffect)(() => {
6676
7843
  return subscribe(() => forceUpdate((n) => n + 1));
@@ -6702,6 +7869,70 @@ window.__ModuleLoader__.load({
6702
7869
  if (typeof off === "function") off();
6703
7870
  };
6704
7871
  }, [runMcpCheck]);
7872
+ const [healthMap, setHealthMap] = (0, react.useState)({});
7873
+ const serverSig = servers.map((s) => `${s.id}:${s.api_base}`).join("|");
7874
+ (0, react.useEffect)(() => {
7875
+ let cancelled = false;
7876
+ for (const s of servers) {
7877
+ setHealthMap((m) => m[s.id] && m[s.id].state === "checking" ? m : {
7878
+ ...m,
7879
+ [s.id]: { state: "checking" }
7880
+ });
7881
+ getHealthByBase(s.api_base).then((data) => {
7882
+ if (!cancelled) setHealthMap((m) => ({
7883
+ ...m,
7884
+ [s.id]: {
7885
+ state: "ok",
7886
+ app: String(data?.app || ""),
7887
+ version: String(data?.version || "")
7888
+ }
7889
+ }));
7890
+ }).catch((error) => {
7891
+ if (!cancelled) setHealthMap((m) => ({
7892
+ ...m,
7893
+ [s.id]: {
7894
+ state: "error",
7895
+ error: error && (error.msg || error.message) || "请求失败"
7896
+ }
7897
+ }));
7898
+ });
7899
+ }
7900
+ return () => {
7901
+ cancelled = true;
7902
+ };
7903
+ }, [serverSig]);
7904
+ const healthView = (h) => {
7905
+ if (!h || h.state === "checking") return {
7906
+ cls: "is-muted",
7907
+ text: "应用与版本检测中…"
7908
+ };
7909
+ if (h.state === "error") return {
7910
+ cls: "is-danger",
7911
+ text: `服务器不可达:${h.error}`
7912
+ };
7913
+ if (h.app && h.app !== manifest.id) return {
7914
+ cls: "is-danger",
7915
+ text: `应用不匹配:该服务器部署的是「${h.app}」,本插件为 ${manifest.id}`
7916
+ };
7917
+ if (!h.version) return {
7918
+ cls: "is-danger",
7919
+ text: "后端未上报版本(版本过旧),请升级后端",
7920
+ tips: [`升级后端对应版本:在「时刻AI后台」将该应用升级到 v${manifest.version}`]
7921
+ };
7922
+ if (h.version === "dev") return {
7923
+ cls: "is-muted",
7924
+ text: "后端 dev(未注入版本,本地开发构建)"
7925
+ };
7926
+ if (h.version !== manifest.version) return {
7927
+ cls: "is-danger",
7928
+ text: `前后端版本不一致:后端 v${h.version} / 插件 ${manifest.version}`,
7929
+ tips: [`部署前端对应版本:dsh plugin --profile web add ${manifest.packageName}@${h.version}`, `升级后端对应版本:在「时刻AI后台」将该应用升级到 v${manifest.version}`]
7930
+ };
7931
+ return {
7932
+ cls: "is-ok",
7933
+ text: `应用与版本一致 v${h.version}`
7934
+ };
7935
+ };
6705
7936
  const reloadAdmin = () => {
6706
7937
  setAdminInfo((s) => ({
6707
7938
  ...s,
@@ -6722,6 +7953,16 @@ window.__ModuleLoader__.load({
6722
7953
  }));
6723
7954
  };
6724
7955
  (0, react.useEffect)(reloadAdmin, []);
7956
+ const [modelConfig, setModelConfig] = (0, react.useState)(null);
7957
+ const [modelLoading, setModelLoading] = (0, react.useState)(false);
7958
+ const [showModelHelp, setShowModelHelp] = (0, react.useState)(false);
7959
+ const [modelEditItem, setModelEditItem] = (0, react.useState)(null);
7960
+ const reloadModelConfig = (0, react.useCallback)(() => {
7961
+ if (!adminInfo.isAdmin) return;
7962
+ setModelLoading(true);
7963
+ getModelConfig().then((res) => setModelConfig(res || null)).catch(toastError).finally(() => setModelLoading(false));
7964
+ }, [adminInfo.isAdmin]);
7965
+ (0, react.useEffect)(reloadModelConfig, [reloadModelConfig]);
6725
7966
  const [myStatus, setMyStatus] = (0, react.useState)(null);
6726
7967
  (0, react.useEffect)(() => {
6727
7968
  let cancelled = false;
@@ -6861,20 +8102,32 @@ window.__ModuleLoader__.load({
6861
8102
  className: "skai-kb-server-list",
6862
8103
  children: servers.map((s) => {
6863
8104
  const isActive = Boolean(active && active.id === s.id);
8105
+ const health = healthView(healthMap[s.id]);
6864
8106
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
6865
8107
  className: `skai-kb-server-item ${isActive ? "is-active" : ""}`,
6866
8108
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6867
8109
  className: "skai-kb-server-info",
6868
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6869
- className: "skai-kb-server-name",
6870
- children: [s.name, isActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6871
- className: "skai-kb-badge skai-kb-badge-green",
6872
- children: "当前"
6873
- }) : null]
6874
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6875
- className: "skai-kb-server-url",
6876
- children: s.api_base
6877
- })]
8110
+ children: [
8111
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8112
+ className: "skai-kb-server-name",
8113
+ children: [s.name, isActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8114
+ className: "skai-kb-badge skai-kb-badge-green",
8115
+ children: "当前"
8116
+ }) : null]
8117
+ }),
8118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8119
+ className: "skai-kb-server-url",
8120
+ children: s.api_base
8121
+ }),
8122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8123
+ className: `skai-kb-server-health ${health.cls}`,
8124
+ children: health.text
8125
+ }),
8126
+ (health.tips || []).map((tip) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8127
+ className: "skai-kb-server-health is-muted",
8128
+ children: ["· ", tip]
8129
+ }, tip))
8130
+ ]
6878
8131
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6879
8132
  className: "skai-kb-row-actions",
6880
8133
  children: [
@@ -6898,36 +8151,115 @@ window.__ModuleLoader__.load({
6898
8151
  })
6899
8152
  ]
6900
8153
  })]
6901
- }, s.id);
6902
- })
6903
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6904
- className: "skai-kb-empty skai-kb-empty-sub",
6905
- children: "尚未配置服务器。添加后即可使用知识库全部功能,并自动注册 mcp__knowledge__* 智能体工具。"
8154
+ }, s.id);
8155
+ })
8156
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8157
+ className: "skai-kb-empty skai-kb-empty-sub",
8158
+ children: "尚未配置服务器。添加后即可使用知识库全部功能,并自动注册 mcp__knowledge__* 智能体工具。"
8159
+ }),
8160
+ !cloudOk ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8161
+ className: "skai-kb-sub skai-kb-cloud-hint",
8162
+ children: "当前为游客态或用户中心配置接口未就绪:服务器配置仅本次会话内有效,登录后自动云同步。"
8163
+ }) : null
8164
+ ]
8165
+ }),
8166
+ adminInfo.isAdmin ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8167
+ className: "skai-kb-panel",
8168
+ "aria-label": "模型服务",
8169
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8170
+ className: "skai-kb-panel-head",
8171
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "模型服务" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8172
+ className: "skai-kb-muted",
8173
+ children: "向量化 / 重排 / OCR 支持调用互联网公网模型服务,优先推荐硅基流动"
8174
+ })]
8175
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
8176
+ className: "skai-kb-server-list",
8177
+ children: modelLoading && !modelConfig ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8178
+ className: "skai-kb-sub",
8179
+ children: "配置加载中…"
8180
+ }) : MODEL_ITEMS.map(({ key, name }) => {
8181
+ const entry = (modelConfig || {})[key] || {};
8182
+ const sourceView = MODEL_SOURCE_VIEW[entry.source] || MODEL_SOURCE_VIEW.unset;
8183
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
8184
+ className: "skai-kb-server-item",
8185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8186
+ className: "skai-kb-server-info",
8187
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8188
+ className: "skai-kb-server-name",
8189
+ children: [
8190
+ name,
8191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8192
+ className: `skai-kb-badge ${sourceView.badge}`,
8193
+ children: sourceView.text
8194
+ }),
8195
+ entry.enabled === false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8196
+ className: "skai-kb-badge skai-kb-badge-red",
8197
+ children: "未启用"
8198
+ }) : null
8199
+ ]
8200
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8201
+ className: "skai-kb-server-url",
8202
+ children: ["模型:", entry.config?.model || "—"]
8203
+ })]
8204
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8205
+ className: "skai-kb-row-actions",
8206
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8207
+ type: "button",
8208
+ className: "skai-kb-btn skai-kb-btn-sm",
8209
+ onClick: () => setShowModelHelp(true),
8210
+ children: "配置说明"
8211
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8212
+ type: "button",
8213
+ className: "skai-kb-btn skai-kb-btn-sm",
8214
+ onClick: () => setModelEditItem(key),
8215
+ children: "编辑"
8216
+ })]
8217
+ })]
8218
+ }, key);
8219
+ })
8220
+ })]
8221
+ }) : null,
8222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8223
+ className: "skai-kb-panel",
8224
+ "aria-label": "MCP 同步状态",
8225
+ children: [
8226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8227
+ className: "skai-kb-panel-head",
8228
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "智能体(MCP)同步状态" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8229
+ type: "button",
8230
+ className: "skai-kb-btn skai-kb-btn-sm",
8231
+ disabled: mcpStatus.checking,
8232
+ onClick: runMcpCheck,
8233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
8234
+ size: 12,
8235
+ d: P.refresh
8236
+ }), "重新自检"]
8237
+ })]
8238
+ }),
8239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8240
+ className: "skai-kb-mcp-status",
8241
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8242
+ className: `skai-kb-badge ${TONE_BADGE[mcpView.tone] || "skai-kb-badge-gray"}`,
8243
+ children: mcpStatus.checking ? "检查中…" : mcpView.tone === "success" ? "就绪" : "未就绪"
8244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8245
+ className: "skai-kb-mcp-text",
8246
+ children: [mcpView.text, mcpStatus.detail && mcpStatus.detail !== mcpView.text ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8247
+ className: "skai-kb-muted",
8248
+ children: [
8249
+ "(",
8250
+ mcpStatus.detail,
8251
+ ")"
8252
+ ]
8253
+ }) : null]
8254
+ })]
6906
8255
  }),
6907
- activeBase ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8256
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6908
8257
  className: "skai-kb-sub",
6909
- style: {
6910
- display: "flex",
6911
- alignItems: "center",
6912
- gap: 8,
6913
- flexWrap: "wrap",
6914
- marginTop: 8
6915
- },
6916
8258
  children: [
6917
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["后端版本:", backendVersion.checking ? "检测中…" : backendVersion.error || backendVer || "未知"] }),
6918
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6919
- className: "skai-kb-muted",
6920
- children: ["插件版本:v", pluginVersion]
6921
- }),
6922
- versionMismatch ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6923
- className: "skai-kb-badge skai-kb-badge-amber",
6924
- children: "前后端版本不一致,请将镜像与插件升级到同一版本"
6925
- }) : null
8259
+ "接入点:",
8260
+ active.api_base,
8261
+ "/kb/mcp/v1 · 智能体经 mcp__knowledge__ 前缀工具检索知识库"
6926
8262
  ]
6927
- }) : null,
6928
- !cloudOk ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6929
- className: "skai-kb-sub skai-kb-cloud-hint",
6930
- children: "当前为游客态或用户中心配置接口未就绪:服务器配置仅本次会话内有效,登录后自动云同步。"
6931
8263
  }) : null
6932
8264
  ]
6933
8265
  }),
@@ -7097,50 +8429,6 @@ window.__ModuleLoader__.load({
7097
8429
  ]
7098
8430
  }) : null,
7099
8431
  !statusRestricted && adminInfo.isAdmin ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserManagePanel, { isAdmin: true }) : null,
7100
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7101
- className: "skai-kb-panel",
7102
- "aria-label": "MCP 同步状态",
7103
- children: [
7104
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7105
- className: "skai-kb-panel-head",
7106
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "智能体(MCP)同步状态" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7107
- type: "button",
7108
- className: "skai-kb-btn skai-kb-btn-sm",
7109
- disabled: mcpStatus.checking,
7110
- onClick: runMcpCheck,
7111
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
7112
- size: 12,
7113
- d: P.refresh
7114
- }), "重新自检"]
7115
- })]
7116
- }),
7117
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7118
- className: "skai-kb-mcp-status",
7119
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7120
- className: `skai-kb-badge ${TONE_BADGE[mcpView.tone] || "skai-kb-badge-gray"}`,
7121
- children: mcpStatus.checking ? "检查中…" : mcpView.tone === "success" ? "就绪" : "未就绪"
7122
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7123
- className: "skai-kb-mcp-text",
7124
- children: [mcpView.text, mcpStatus.detail && mcpStatus.detail !== mcpView.text ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7125
- className: "skai-kb-muted",
7126
- children: [
7127
- "(",
7128
- mcpStatus.detail,
7129
- ")"
7130
- ]
7131
- }) : null]
7132
- })]
7133
- }),
7134
- active ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7135
- className: "skai-kb-sub",
7136
- children: [
7137
- "接入点:",
7138
- active.api_base,
7139
- "/kb/mcp/v1 · 智能体经 mcp__knowledge__ 前缀工具检索知识库"
7140
- ]
7141
- }) : null
7142
- ]
7143
- }),
7144
8432
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7145
8433
  className: "skai-kb-panel",
7146
8434
  "aria-label": "账号信息",
@@ -7208,6 +8496,17 @@ window.__ModuleLoader__.load({
7208
8496
  onConfirm: handleRemovePending,
7209
8497
  onClose: () => setRemovingPending(null)
7210
8498
  }) : null,
8499
+ modelEditItem ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelServiceModal, {
8500
+ item: modelEditItem,
8501
+ open: true,
8502
+ config: modelConfig?.[modelEditItem]?.config || null,
8503
+ onClose: () => setModelEditItem(null),
8504
+ onSaved: () => {
8505
+ setModelEditItem(null);
8506
+ reloadModelConfig();
8507
+ }
8508
+ }) : null,
8509
+ showModelHelp ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelConfigHelpModal, { onClose: () => setShowModelHelp(false) }) : null,
7211
8510
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toast, {})
7212
8511
  ]
7213
8512
  });
@@ -7224,7 +8523,7 @@ window.__ModuleLoader__.load({
7224
8523
  */
7225
8524
  //#endregion
7226
8525
  //#region src/styles/knowledge.css?raw
7227
- var knowledge_default = "/* ============================================================\n 时刻AI知识库插件样式(前缀 skai-kb-,不覆盖宿主样式)\n - 0.8.0 起渲染于 DSH 右侧 Sidebar 标签页正文(首页 / 知识库详情 /\n 文档详情 / 设置 各为一个标签页)与 standalone 调试台:\n .skai-kb-page 声明 inline-size 容器查询(与 .skai-kb-app 同名 skai-kb),\n 布局按容器宽度自适应,不依赖视口宽度(面板开合、拖宽均正确响应)\n - 颜色全部取 DSH 官方主题令牌(--dsw-alias-*),随宿主\n 浅色/深色主题自动切换;var() 兜底值仅供 standalone 预览\n (无宿主令牌)使用\n - z-index 使用适中值(≤35),禁止覆盖宿主设置菜单\n ============================================================ */\n\n/* ---------- 主题令牌别名层(统一映射到 DSH 官方令牌) ----------\n DSH 强调色语义(与常见设计系统相反,务必注意):\n - --dsw-alias-brand-primary:亮色主题为深色(bluish-1000)、暗色主题为浅色\n (bluish-50),即「反色强调」——主按钮填充随主题黑白互换\n - --dsw-alias-label-primary-foreground:强调填充上的前景文字,恒与填充反相\n (亮色=白、暗色=黑),是主按钮等强调填充上文字的正确取值。\n (DSH 无 --dsw-alias-brand-text;此前误用导致暗色模式白底白字)\n - --dsw-alias-link:正文链接色(亮 deepseek-500 / 暗 deepseek-400,恒为蓝) */\n.skai-kb-root,\n.skai-kb-modal-mask,\n.skai-kb-toast-layer {\n --skai-kb-bg: var(--dsw-alias-bg-base, #ffffff);\n --skai-kb-bg-card: var(--dsw-alias-bg-layer-1, #ffffff);\n --skai-kb-bg-soft: var(--dsw-alias-bg-layer-2, #f6f7f9);\n --skai-kb-bg-hover: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04));\n --skai-kb-mask: var(--dsw-alias-bg-mask-1, rgba(17, 19, 23, 0.5));\n --skai-kb-border: var(--dsw-alias-border-l1, #e4e6eb);\n --skai-kb-border-strong: var(--dsw-alias-border-l2, #d0d3d9);\n --skai-kb-text: var(--dsw-alias-label-primary, #1f2329);\n --skai-kb-text-sub: var(--dsw-alias-label-secondary, #8a8f99);\n --skai-kb-text-caption: var(--dsw-alias-label-caption, #9ca1ab);\n /* 知识库品牌青色:与 DSH 侧边栏标签页/引导页图标同色(#06b6d4) */\n --skai-kb-primary: #06b6d4;\n /* 强调填充上的文字(官方前景令牌:亮=白 / 暗=黑;无 token 环境兜底白色) */\n --skai-kb-on-primary: var(--dsw-alias-label-primary-foreground, #ffffff);\n /* 主按钮:品牌青色实心 + 白字(青色为高亮度色相,不适用深底反色公式),hover 提亮一档(与 CRM 一致) */\n --skai-kb-btn-primary: var(--skai-kb-primary);\n --skai-kb-primary-weak: color-mix(in srgb, var(--skai-kb-primary) 12%, transparent);\n --skai-kb-danger: var(--dsw-alias-state-error-primary, #d83931);\n --skai-kb-danger-weak: color-mix(in srgb, var(--skai-kb-danger) 12%, transparent);\n --skai-kb-ok: var(--dsw-alias-state-success-primary, #2ea44f);\n --skai-kb-ok-weak: color-mix(in srgb, var(--skai-kb-ok) 14%, transparent);\n --skai-kb-warn: var(--dsw-alias-state-warn-primary, #d97706);\n --skai-kb-warn-weak: color-mix(in srgb, var(--skai-kb-warn) 14%, transparent);\n --skai-kb-link: var(--skai-kb-primary);\n --skai-kb-shadow: 0 8px 24px rgba(31, 35, 41, 0.12);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--skai-kb-text);\n background: var(--skai-kb-bg);\n scrollbar-width: thin;\n}\n\n/* 盒模型统一(与 CRM 一致):root 作用域内全部 border-box */\n.skai-kb-root *,\n.skai-kb-root *::before,\n.skai-kb-root *::after,\n.skai-kb-modal-mask *,\n.skai-kb-modal-mask *::before,\n.skai-kb-modal-mask *::after {\n box-sizing: border-box;\n}\n\n/* 原生 button 不继承字体,与 CRM 一致显式继承宿主字体 */\n.skai-kb-root button,\n.skai-kb-modal-mask button {\n font-family: inherit;\n}\n\n/* 深色模式(跟随系统 prefers-color-scheme,与 CRM 插件策略一致):\n 宿主令牌存在时仍优先取宿主值(var 包裹),此处仅提供 standalone 兜底 */\n@media (prefers-color-scheme: dark) {\n .skai-kb-root,\n .skai-kb-modal-mask,\n .skai-kb-toast-layer {\n --skai-kb-bg: var(--dsw-alias-bg-base, #1e1f22);\n --skai-kb-bg-card: var(--dsw-alias-bg-layer-1, #1e1f22);\n --skai-kb-bg-soft: var(--dsw-alias-bg-layer-2, #2a2b2e);\n --skai-kb-bg-hover: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08));\n --skai-kb-mask: rgba(0, 0, 0, 0.55);\n --skai-kb-border: var(--dsw-alias-border-l1, #2c2d2f);\n --skai-kb-border-strong: var(--dsw-alias-border-l2, #3a3d42);\n --skai-kb-text: var(--dsw-alias-label-primary, #e8eaed);\n --skai-kb-text-sub: var(--dsw-alias-label-secondary, #8b8f96);\n --skai-kb-text-caption: var(--dsw-alias-label-caption, #6f747d);\n --skai-kb-primary: #22d3ee;\n --skai-kb-on-primary: var(--dsw-alias-label-primary-foreground, #10131a);\n --skai-kb-btn-primary: #06b6d4;\n --skai-kb-danger: var(--dsw-alias-state-error-primary, #f5564e);\n --skai-kb-ok: var(--dsw-alias-state-success-primary, #46c26a);\n --skai-kb-warn: var(--dsw-alias-state-warn-primary, #e8a23c);\n --skai-kb-link: var(--skai-kb-primary);\n --skai-kb-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);\n }\n}\n\n/* ---------- 布局骨架(容器查询基准) ---------- */\n.skai-kb-app {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n position: relative;\n container-type: inline-size;\n container-name: skai-kb;\n}\n\n/* standalone 调试台挂载容器(模拟抽屉内 100% 高度) */\n.skai-kb-standalone-inner {\n height: 100%;\n min-height: 0;\n}\n\n/* ---------- DSH 标签页正文骨架 ----------\n sidebar.right.pane.tab 正文:不滚动定位容器(弹窗/Toast 绝对定位收敛于此),\n 内容滚动由 .skai-kb-page-scroll 承担(文档详情为 .skai-kb-md-wrap) */\n.skai-kb-tab-root {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.skai-kb-page {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 12px;\n height: 100%;\n min-height: 0;\n padding: 14px 16px;\n overflow: hidden;\n container-type: inline-size;\n container-name: skai-kb;\n}\n.skai-kb-tab-root > .skai-kb-page {\n flex: 1;\n}\n.skai-kb-page-scroll {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.skai-kb-header {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 14px 16px 0;\n flex: none;\n}\n.skai-kb-header-title {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 15px;\n font-weight: 600;\n color: var(--skai-kb-text);\n}\n\n/* 分区导航:窄容器横向滑动(不换行挤压),宽容器自然展开 */\n.skai-kb-nav {\n display: flex;\n gap: 4px;\n overflow-x: auto;\n scrollbar-width: none;\n padding-bottom: 8px;\n border-bottom: 1px solid var(--skai-kb-border);\n align-items: center;\n}\n.skai-kb-nav::-webkit-scrollbar {\n display: none;\n}\n.skai-kb-nav-btn {\n flex: none;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-sub);\n padding: 4px 10px;\n border-radius: 999px;\n cursor: pointer;\n font-size: 12px;\n transition: background 0.15s ease, color 0.15s ease;\n}\n.skai-kb-nav-btn:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-text);\n}\n.skai-kb-nav-btn.is-active {\n background: var(--skai-kb-primary-weak);\n color: var(--skai-kb-primary);\n font-weight: 500;\n}\n.skai-kb-nav-btn:focus-visible {\n outline: 2px solid var(--skai-kb-primary);\n outline-offset: 1px;\n}\n/* standalone 页签关闭钮 */\n.skai-kb-tab-close {\n flex: none;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-caption);\n cursor: pointer;\n font-size: 10px;\n padding: 2px 4px;\n border-radius: 6px;\n}\n.skai-kb-tab-close:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-danger);\n}\n\n.skai-kb-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 14px 16px;\n}\n\n/* ---------- 详情页头(知识库详情 / 文档详情标签页) ---------- */\n.skai-kb-detail-head {\n flex: none;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-bottom: 8px;\n}\n.skai-kb-detail-title {\n font-weight: 600;\n font-size: 15px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-back {\n flex: none;\n width: auto;\n min-width: 0;\n}\n\n/* ---------- 首页页头(左侧说明介绍 + 右上角设置入口) ---------- */\n.skai-kb-home-head {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 10px;\n padding-bottom: 10px;\n}\n.skai-kb-home-head-main {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n.skai-kb-home-desc {\n font-size: 12px;\n line-height: 1.6;\n color: var(--skai-kb-text-sub);\n /* 介绍信息完整展示:不截断,自动换行铺满全量文字 */\n overflow-wrap: anywhere;\n}\n.skai-kb-home-head > .skai-kb-btn {\n flex: none;\n width: auto;\n margin-top: 1px;\n}\n\n/* ---------- 通用元素 ---------- */\n.skai-kb-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n width: auto;\n height: 30px;\n padding: 0 12px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n background: var(--skai-kb-bg-card);\n color: var(--skai-kb-text);\n cursor: pointer;\n font-size: 12.5px;\n white-space: nowrap;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n.skai-kb-btn:hover:not(:disabled) {\n background: var(--skai-kb-bg-soft);\n}\n.skai-kb-btn:focus-visible {\n outline: 2px solid var(--skai-kb-primary);\n outline-offset: 1px;\n}\n.skai-kb-btn:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n/* 等待转圈动画(SpinnerGlyph 专用) */\n@keyframes skai-kb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.skai-kb-spin {\n animation: skai-kb-spin 0.8s linear infinite;\n flex: none;\n}\n.skai-kb-btn-primary {\n background: var(--skai-kb-btn-primary);\n border-color: transparent;\n color: #fff;\n}\n.skai-kb-btn-primary:hover:not(:disabled) {\n background: var(--skai-kb-btn-primary);\n filter: brightness(1.08);\n}\n.skai-kb-btn-danger {\n background: var(--skai-kb-danger);\n border-color: var(--skai-kb-danger);\n color: #fff;\n}\n.skai-kb-btn-danger:hover:not(:disabled) {\n background: var(--skai-kb-danger);\n filter: brightness(1.05);\n}\n.skai-kb-btn-ghost-danger {\n border-color: transparent;\n background: transparent;\n color: var(--skai-kb-danger);\n}\n.skai-kb-btn-ghost-danger:hover:not(:disabled) {\n background: var(--skai-kb-danger-weak);\n}\n.skai-kb-btn-sm {\n width: auto;\n height: 26px;\n padding: 0 9px;\n font-size: 12px;\n border-radius: 8px;\n}\n\n.skai-kb-icon-btn {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-sub);\n cursor: pointer;\n border-radius: 8px;\n font-size: 12px;\n}\n.skai-kb-icon-btn:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-text);\n}\n/* 悬停文字提示(CSS 实现,即时显示;按钮右上角对齐避免溢出弹窗) */\n.skai-kb-icon-btn[data-tip]:hover::after {\n content: attr(data-tip);\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 30;\n padding: 4px 8px;\n border-radius: 6px;\n background: var(--skai-kb-bg);\n color: var(--skai-kb-text);\n font-size: 12px;\n line-height: 1.4;\n white-space: nowrap;\n pointer-events: none;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n}\n\n.skai-kb-input,\n.skai-kb-select,\n.skai-kb-textarea {\n width: 100%;\n padding: 5px 10px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n background: var(--skai-kb-bg);\n color: var(--skai-kb-text);\n /* 原生表单控件不继承字体,与 CRM 一致显式继承 */\n font: inherit;\n font-size: 12.5px;\n box-sizing: border-box;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n}\n/* 下拉选择与 CRM 一致使用手型光标 */\n.skai-kb-select {\n cursor: pointer;\n}\n/* 单行输入控件与按钮(30px)同高 */\n.skai-kb-input,\n.skai-kb-select {\n height: 30px;\n}\n.skai-kb-textarea {\n min-height: 56px;\n resize: vertical;\n line-height: 1.6;\n}\n.skai-kb-input:focus,\n.skai-kb-select:focus,\n.skai-kb-textarea:focus {\n outline: none;\n border-color: var(--skai-kb-primary);\n box-shadow: 0 0 0 2px var(--skai-kb-primary-weak);\n}\n.skai-kb-input::placeholder,\n.skai-kb-textarea::placeholder {\n color: var(--skai-kb-text-caption);\n}\n.skai-kb-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n}\n.skai-kb-field-label {\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n}\n.skai-kb-field-error {\n font-size: 12px;\n color: var(--skai-kb-danger);\n}\n.skai-kb-field-row {\n display: flex;\n gap: 8px;\n}\n.skai-kb-field-row > * {\n flex: 1;\n}\n\n.skai-kb-toolbar {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n flex-wrap: wrap;\n flex: none;\n}\n.skai-kb-toolbar .skai-kb-search {\n flex: 1;\n min-width: 120px;\n}\n\n.skai-kb-card {\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n padding: 10px 12px;\n margin-bottom: 8px;\n transition: border-color 0.15s ease, background 0.15s ease;\n}\n.skai-kb-card:hover {\n border-color: var(--skai-kb-border-strong);\n}\n\n/* 卡片列表:每行一个知识库卡片(纵向堆叠,占满整行宽度) */\n.skai-kb-card-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.skai-kb-card-list > .skai-kb-card {\n margin-bottom: 0;\n}\n\n.skai-kb-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.skai-kb-row-between {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.skai-kb-title {\n font-weight: 600;\n font-size: 13px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-sub {\n padding: 7px 0 0 0;\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n /* 说明/介绍类文本完整展示:不截断省略,自动换行铺满全量文字 */\n overflow-wrap: anywhere;\n}\n\n.skai-kb-badge {\n display: inline-flex;\n align-items: center;\n padding: 0 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 18px;\n white-space: nowrap;\n}\n.skai-kb-badge-gray { background: var(--skai-kb-bg-soft); color: var(--skai-kb-text-sub); }\n.skai-kb-badge-blue { background: var(--skai-kb-primary-weak); color: var(--skai-kb-primary); }\n.skai-kb-badge-green { background: var(--skai-kb-ok-weak); color: var(--skai-kb-ok); }\n.skai-kb-badge-amber { background: var(--skai-kb-warn-weak); color: var(--skai-kb-warn); }\n.skai-kb-badge-red { background: var(--skai-kb-danger-weak); color: var(--skai-kb-danger); }\n\n.skai-kb-tag {\n display: inline-block;\n padding: 0 6px;\n border-radius: 4px;\n font-size: 11px;\n line-height: 18px;\n background: var(--skai-kb-bg-soft);\n color: var(--skai-kb-text-sub);\n margin-right: 4px;\n}\n\n.skai-kb-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n text-align: center;\n padding: 28px 16px;\n color: var(--skai-kb-text-sub);\n font-size: 13px;\n}\n.skai-kb-empty-sub {\n font-size: 12px;\n}\n\n.skai-kb-pagination {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 8px 0;\n color: var(--skai-kb-text-sub);\n font-size: 12px;\n flex: none;\n}\n.skai-kb-pagination-btns {\n display: flex;\n gap: 8px;\n}\n\n/* ---------- 设置页(对齐 CRM:页头 + panel 区块 + 行操作 + 详情列表) ---------- */\n.skai-kb-page-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n flex: none;\n}\n.skai-kb-page-title {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n}\n.skai-kb-page-title svg {\n color: var(--skai-kb-primary);\n}\n.skai-kb-page-actions {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.skai-kb-panel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n}\n.skai-kb-panel-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n}\n.skai-kb-panel-head h3 {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n.skai-kb-panel-head .skai-kb-btn {\n flex: none;\n width: auto;\n}\n.skai-kb-muted {\n color: var(--skai-kb-text-sub);\n font-size: 12px;\n}\n.skai-kb-row-actions {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n.skai-kb-server-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n.skai-kb-server-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n background: var(--skai-kb-bg-card);\n padding: 10px 12px;\n transition: border-color 0.15s ease;\n}\n.skai-kb-server-item.is-active {\n border-color: var(--skai-kb-primary);\n background: var(--skai-kb-primary-weak);\n}\n.skai-kb-server-info {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n.skai-kb-server-name {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 500;\n font-size: 13px;\n}\n.skai-kb-server-url {\n margin-top: 2px;\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n word-break: break-all;\n}\n.skai-kb-mcp-status {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n.skai-kb-mcp-text {\n font-size: 12.5px;\n line-height: 1.6;\n}\n.skai-kb-cloud-hint {\n padding: 8px 10px;\n border: 1px dashed var(--skai-kb-border);\n border-radius: 8px;\n}\n.skai-kb-detail-list {\n display: flex;\n flex-direction: column;\n}\n.skai-kb-detail-row {\n display: flex;\n gap: 12px;\n padding: 7px 0;\n border-bottom: 1px dashed var(--skai-kb-border);\n font-size: 12.5px;\n}\n.skai-kb-detail-row:last-child {\n border-bottom: none;\n}\n.skai-kb-detail-row .skai-kb-field-label {\n width: 92px;\n flex-shrink: 0;\n padding-top: 1px;\n}\n.skai-kb-detail-text {\n flex: 1;\n min-width: 0;\n word-break: break-all;\n white-space: pre-wrap;\n}\n\n/* ---------- 用户管理(设置页区块:审核模式开关 / 筛选工具条 / 状态横幅) ---------- */\n.skai-kb-user-review-mode {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n padding: 8px 10px;\n border: 1px dashed var(--skai-kb-border);\n border-radius: 8px;\n margin-bottom: 10px;\n}\n.skai-kb-user-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n margin-bottom: 8px;\n}\n\n/* 添加成员弹窗「从用户列表选择」候选列表(弹窗内滚动,选中高亮) */\n.skai-kb-user-pick-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 200px;\n overflow-y: auto;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 8px;\n padding: 4px;\n margin-bottom: 8px;\n}\n.skai-kb-user-pick-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n padding: 6px 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n}\n.skai-kb-user-pick-item:hover {\n background: var(--skai-kb-hover, rgba(0, 0, 0, 0.04));\n}\n.skai-kb-user-pick-item-active,\n.skai-kb-user-pick-item-active:hover {\n background: var(--skai-kb-primary-soft, rgba(49, 92, 255, 0.1));\n}\n.skai-kb-user-pick-nickname {\n display: block;\n font-weight: 500;\n}\n/* 多选复选标记(行首圆角方框,选中品牌色) */\n.skai-kb-pick-check {\n flex: none;\n width: 16px;\n height: 16px;\n border: 1px solid var(--skai-kb-border, #d1d5db);\n border-radius: 4px;\n font-size: 11px;\n line-height: 14px;\n text-align: center;\n color: #fff;\n}\n.skai-kb-pick-check-on {\n background: var(--skai-kb-primary, #17b0c8);\n border-color: var(--skai-kb-primary, #17b0c8);\n}\n.skai-kb-user-pick-item-disabled,\n.skai-kb-user-pick-item-disabled:hover {\n opacity: 0.55;\n cursor: not-allowed;\n background: transparent;\n}\n/* 已选用户 chips(头像 + 昵称 + 移除) */\n.skai-kb-picked-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 8px;\n}\n.skai-kb-picked-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 3px 6px 3px 4px;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 999px;\n background: var(--skai-kb-primary-soft, rgba(23, 176, 200, 0.08));\n font-size: 12px;\n}\n.skai-kb-picked-chip-avatar {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n object-fit: cover;\n}\n.skai-kb-picked-chip-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-picked-chip-remove {\n flex: none;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.08);\n color: inherit;\n font-size: 12px;\n line-height: 16px;\n cursor: pointer;\n}\n.skai-kb-picked-chip-remove:hover {\n background: rgba(0, 0, 0, 0.16);\n}\n.skai-kb-user-search {\n width: 200px;\n height: 26px;\n flex: none;\n}\n\n/* ---------- 弹窗(Portal 挂 body,遮罩 fixed 铺满整个窗口并居中,与 CRM 同款;\n 弹层根已纳入文件头部主题令牌作用域;z-index ≤35 不覆盖宿主设置菜单) ---------- */\n.skai-kb-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 30;\n background: var(--skai-kb-mask);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n animation: skai-kb-fade-in 0.15s ease;\n}\n.skai-kb-modal {\n width: 100%;\n max-width: 520px;\n max-height: 86%;\n display: flex;\n flex-direction: column;\n background: var(--skai-kb-bg-card);\n border-radius: 10px;\n box-shadow: var(--skai-kb-shadow);\n animation: skai-kb-pop-in 0.16s ease;\n}\n.skai-kb-modal.is-fullscreen {\n max-width: none;\n width: 100%;\n max-height: 96%;\n height: 96%;\n}\n.skai-kb-modal-head-actions {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n flex: none;\n}\n.skai-kb-modal-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--skai-kb-border);\n flex: none;\n}\n.skai-kb-modal-title {\n font-size: 14px;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-modal-body {\n padding: 14px 16px;\n overflow-y: auto;\n}\n.skai-kb-modal-foot {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 10px 16px;\n border-top: 1px solid var(--skai-kb-border);\n flex: none;\n}\n.skai-kb-modal-foot .skai-kb-btn {\n width: auto;\n min-width: 72px;\n}\n.skai-kb-confirm-text {\n margin: 0;\n font-size: 13px;\n line-height: 1.7;\n}\n\n/* ---------- Toast(面板底 + 彩色描边样式,与 CRM 一致;z-index 34 宿主菜单避让约定内) ---------- */\n.skai-kb-toast-layer {\n /* createPortal 到 body + fixed:相对整个 DSH 窗口顶部居中(非插件标签页区域);\n z-index 34 宿主菜单避让约定内 */\n position: fixed;\n left: 50%;\n top: 18px;\n transform: translateX(-50%);\n z-index: 34;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: center;\n pointer-events: none;\n}\n.skai-kb-toast {\n pointer-events: auto;\n max-width: 90%;\n padding: 7px 14px;\n border-radius: 999px;\n font-size: 12.5px;\n background: var(--skai-kb-bg-card);\n border: 1px solid var(--skai-kb-border);\n color: var(--skai-kb-text);\n box-shadow: var(--skai-kb-shadow);\n cursor: pointer;\n word-break: break-all;\n animation: skai-kb-toast-in 0.18s ease;\n}\n.skai-kb-toast-info { border-color: var(--skai-kb-primary); color: var(--skai-kb-primary); }\n.skai-kb-toast-error { border-color: var(--skai-kb-danger); color: var(--skai-kb-danger); }\n.skai-kb-toast-success { border-color: var(--skai-kb-ok); color: var(--skai-kb-ok); }\n\n/* ---------- 动画(与 CRM 同款节奏) ---------- */\n@keyframes skai-kb-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes skai-kb-pop-in {\n from { opacity: 0; transform: translateY(8px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n@keyframes skai-kb-toast-in {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ---------- 概览统计(首页) ---------- */\n.skai-kb-stat-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n margin-bottom: 4px;\n}\n.skai-kb-stat-card {\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n padding: 10px 12px;\n}\n.skai-kb-stat-num {\n font-size: 18px;\n font-weight: 600;\n}\n/* 统计卡内气泡(首页「用户数」待审核提示) */\n.skai-kb-stat-badge {\n margin-left: 8px;\n vertical-align: 2px;\n cursor: default;\n}\n.skai-kb-stat-label {\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n margin-top: 2px;\n}\n.skai-kb-quick-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n}\n\n/* ---------- 知识库卡片 ---------- */\n.skai-kb-kb-meta {\n display: flex;\n gap: 12px;\n margin-top: 6px;\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n flex-wrap: wrap;\n}\n.skai-kb-kb-actions {\n display: flex;\n gap: 6px;\n margin-top: 8px;\n flex-wrap: wrap;\n}\n.skai-kb-kb-actions .skai-kb-btn-sm {\n flex: 1;\n}\n\n/* ---------- 文档列表 ---------- */\n.skai-kb-doc-item {\n cursor: pointer;\n}\n\n/* 文档卡片右上角「操作」下拉菜单(遮罩点击关闭;z-index ≤35 不覆盖宿主) */\n.skai-kb-doc-topbar {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: none;\n}\n.skai-kb-doc-menu-anchor {\n position: relative;\n display: inline-flex;\n}\n.skai-kb-doc-menu-trigger {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n line-height: 1;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-sub);\n font-size: 12px;\n padding: 2px 6px;\n border-radius: 6px;\n cursor: pointer;\n}\n/* CSS 三角下箭头:随 flex 垂直居中,字体字形对齐不可控故不用 ⌄ */\n.skai-kb-caret {\n width: 0;\n height: 0;\n border-left: 4px solid transparent;\n border-right: 4px solid transparent;\n border-top: 5px solid currentColor;\n}\n.skai-kb-doc-menu-trigger:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-text);\n}\n.skai-kb-menu-mask {\n position: fixed;\n inset: 0;\n z-index: 34;\n}\n.skai-kb-menu {\n position: absolute;\n top: 22px;\n right: 0;\n z-index: 35;\n min-width: 104px;\n padding: 4px;\n background: var(--skai-kb-bg-card);\n border: 1px solid var(--skai-kb-border-strong);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);\n}\n.skai-kb-menu-item {\n display: block;\n width: 100%;\n text-align: left;\n border: none;\n background: transparent;\n color: var(--skai-kb-text);\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 6px;\n cursor: pointer;\n white-space: nowrap;\n}\n.skai-kb-menu-item:hover {\n background: var(--skai-kb-bg-hover);\n}\n.skai-kb-menu-item.is-danger {\n color: var(--skai-kb-danger);\n}\n.skai-kb-menu-item.is-danger:hover {\n background: var(--skai-kb-danger-weak);\n}\n\n.skai-kb-pre {\n white-space: pre-wrap;\n word-break: break-word;\n background: var(--skai-kb-bg-soft);\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n padding: 8px 10px;\n max-height: 260px;\n overflow-y: auto;\n font-size: 12px;\n margin: 0;\n}\n\n/* ---------- 文档详情正文 ---------- */\n.skai-kb-doc-detail {\n gap: 8px;\n}\n.skai-kb-md-wrap {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n padding: 14px 16px;\n}\n\n/* ---------- Markdown 渲染(文档详情正文;.skai-kb-md 作用域,不外溢) ---------- */\n.skai-kb-md {\n font-size: 13px;\n line-height: 1.7;\n word-break: break-word;\n}\n.skai-kb-md .skai-kb-md-h {\n font-weight: 600;\n line-height: 1.4;\n margin: 14px 0 6px;\n}\n.skai-kb-md .skai-kb-md-h:first-child {\n margin-top: 0;\n}\n.skai-kb-md .skai-kb-md-h1 { font-size: 18px; }\n.skai-kb-md .skai-kb-md-h2 { font-size: 16px; }\n.skai-kb-md .skai-kb-md-h3 { font-size: 15px; }\n.skai-kb-md .skai-kb-md-h4,\n.skai-kb-md .skai-kb-md-h5,\n.skai-kb-md .skai-kb-md-h6 { font-size: 14px; }\n.skai-kb-md .skai-kb-md-p {\n margin: 6px 0;\n}\n.skai-kb-md .skai-kb-md-code {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n background: var(--skai-kb-bg-soft);\n border: 1px solid var(--skai-kb-border);\n border-radius: 4px;\n padding: 0 4px;\n}\n.skai-kb-md .skai-kb-md-pre {\n background: var(--skai-kb-bg-soft);\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n padding: 10px 12px;\n overflow-x: auto;\n font-size: 12px;\n line-height: 1.6;\n margin: 8px 0;\n white-space: pre;\n}\n.skai-kb-md .skai-kb-md-pre code {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n background: transparent;\n border: none;\n padding: 0;\n}\n.skai-kb-md .skai-kb-md-quote {\n margin: 8px 0;\n padding: 4px 12px;\n border-left: 3px solid var(--skai-kb-border-strong);\n color: var(--skai-kb-text-sub);\n background: var(--skai-kb-bg-soft);\n border-radius: 0 8px 8px 0;\n}\n.skai-kb-md .skai-kb-md-ul,\n.skai-kb-md .skai-kb-md-ol {\n margin: 6px 0;\n padding-left: 22px;\n}\n.skai-kb-md .skai-kb-md-li {\n margin: 2px 0;\n}\n.skai-kb-md .skai-kb-md-link {\n color: var(--skai-kb-link);\n text-decoration: none;\n}\n.skai-kb-md .skai-kb-md-link:hover {\n text-decoration: underline;\n}\n.skai-kb-md .skai-kb-md-img {\n max-width: 100%;\n border-radius: 8px;\n border: 1px solid var(--skai-kb-border);\n}\n.skai-kb-md .skai-kb-md-hr {\n border: none;\n border-top: 1px solid var(--skai-kb-border);\n margin: 12px 0;\n}\n.skai-kb-md .skai-kb-md-table-wrap {\n overflow-x: auto;\n margin: 8px 0;\n}\n.skai-kb-md .skai-kb-md-table {\n border-collapse: collapse;\n font-size: 12px;\n min-width: 60%;\n}\n.skai-kb-md .skai-kb-md-table th,\n.skai-kb-md .skai-kb-md-table td {\n border: 1px solid var(--skai-kb-border);\n padding: 4px 10px;\n text-align: left;\n}\n.skai-kb-md .skai-kb-md-table th {\n background: var(--skai-kb-bg-soft);\n font-weight: 600;\n}\n\n/* ---------- 时间线(文档版本) ---------- */\n.skai-kb-timeline {\n list-style: none;\n margin: 0;\n padding: 0 0 0 14px;\n border-left: 2px solid var(--skai-kb-border);\n}\n.skai-kb-timeline li {\n position: relative;\n padding: 0 0 12px 10px;\n}\n.skai-kb-timeline li::before {\n content: '';\n position: absolute;\n left: -20px;\n top: 4px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--skai-kb-primary);\n}\n\n/* ---------- 文档上传 ---------- */\n.skai-kb-dropzone {\n border: 1px dashed var(--skai-kb-border-strong);\n border-radius: 8px;\n padding: 12px;\n}\n\n/* ---------- 成员权限 ---------- */\n.skai-kb-member-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 0;\n border-bottom: 1px solid var(--skai-kb-border);\n}\n.skai-kb-member-row:last-child {\n border-bottom: none;\n}\n.skai-kb-member-avatar {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n flex: none;\n object-fit: cover;\n background: var(--skai-kb-bg-soft);\n}\n.skai-kb-member-avatar-fallback {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 500;\n color: var(--skai-kb-primary);\n background: var(--skai-kb-primary-weak);\n}\n.skai-kb-member-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n.skai-kb-member-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-member-mobile {\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ============================================================\n 宽容器布局(DSH 标签页 / standalone 宽屏):按容器宽度自适应,\n 与视口无关(面板开合、拖宽均正确响应)\n ============================================================ */\n@container skai-kb (min-width: 620px) {\n /* 概览:统计四列 */\n .skai-kb-stat-grid {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n }\n .skai-kb-quick-grid {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n@container skai-kb (min-width: 980px) {\n /* 超宽容器:正文限宽保持可读性(卡片分列由基础网格自适应) */\n .skai-kb-page-scroll,\n .skai-kb-md-wrap {\n max-width: 1080px;\n }\n}\n\n/* =======================================================================\n 目录(文件夹)管理:目录树 + 文档列表布局 + 拖拽指示\n ======================================================================= */\n\n/* 详情页两栏布局:左目录树 + 右文档区(窄容器降级为上下布局) */\n.skai-kb-doc-layout {\n display: flex;\n gap: 10px;\n align-items: stretch;\n min-height: 0;\n}\n\n.skai-kb-doc-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* 目录树面板 */\n.skai-kb-folder-tree {\n flex: none;\n width: 168px;\n display: flex;\n flex-direction: column;\n background: var(--skai-kb-bg-card, #fff);\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 10px;\n padding: 6px;\n align-self: flex-start;\n position: sticky;\n top: 0;\n}\n\n.skai-kb-folder-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 6px 6px;\n border-bottom: 1px solid var(--skai-kb-bg-soft, #f0f1f3);\n margin-bottom: 4px;\n}\n\n.skai-kb-folder-head-title {\n font-size: 12px;\n font-weight: 600;\n color: var(--skai-kb-text-sub, #6b7280);\n}\n\n.skai-kb-folder-body {\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow-y: auto;\n max-height: 320px;\n}\n\n.skai-kb-folder-item {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 6px;\n border-radius: 7px;\n cursor: pointer;\n font-size: 12.5px;\n color: var(--skai-kb-text, #374151);\n border: 1px dashed transparent;\n user-select: none;\n min-width: 0;\n}\n\n.skai-kb-folder-item:hover {\n background: var(--skai-kb-bg-hover, #f5f6f8);\n}\n\n.skai-kb-folder-item.is-active {\n background: var(--skai-kb-primary-weak, #eef4ff);\n color: var(--skai-kb-primary, #2f6bff);\n font-weight: 500;\n}\n\n.skai-kb-folder-arrow {\n flex: none;\n width: 12px;\n font-size: 10px;\n color: var(--skai-kb-text-caption, #9ca3af);\n text-align: center;\n}\n\n.skai-kb-folder-arrow.is-leaf {\n visibility: hidden;\n}\n\n.skai-kb-folder-icon {\n flex: none;\n font-size: 12px;\n}\n\n.skai-kb-folder-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.skai-kb-folder-actions {\n flex: none;\n display: none;\n gap: 2px;\n}\n\n.skai-kb-folder-item:hover .skai-kb-folder-actions {\n display: inline-flex;\n}\n\n.skai-kb-folder-act {\n border: 0;\n background: transparent;\n padding: 0 3px;\n font-size: 11px;\n line-height: 16px;\n color: var(--skai-kb-text-caption, #9ca3af);\n border-radius: 4px;\n cursor: pointer;\n}\n\n.skai-kb-folder-act:hover {\n background: var(--skai-kb-bg-soft, #f0f1f3);\n color: var(--skai-kb-primary, #2f6bff);\n}\n\n.skai-kb-folder-act.is-danger:hover {\n color: var(--skai-kb-danger, #e5484d);\n}\n\n/* 目录树节点:文档拖入高亮(in)与同级排序插入线(before/after) */\n.skai-kb-folder-item.skai-kb-drop-in {\n border: 1px solid var(--skai-kb-primary, #2f6bff);\n background: var(--skai-kb-primary-weak, #eef4ff);\n}\n\n.skai-kb-folder-item.skai-kb-drop-before {\n box-shadow: inset 0 2px 0 0 var(--skai-kb-primary, #2f6bff);\n}\n\n.skai-kb-folder-item.skai-kb-drop-after {\n box-shadow: inset 0 -2px 0 0 var(--skai-kb-primary, #2f6bff);\n}\n\n/* 文档列表项:作为同目录排序 drop 目标时高亮 */\n.skai-kb-doc-item.skai-kb-drop-in {\n outline: 1.5px dashed var(--skai-kb-primary, #2f6bff);\n outline-offset: -2px;\n}\n\n/* 移动文档弹窗:目录选择列表 */\n.skai-kb-move-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 240px;\n overflow-y: auto;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 8px;\n padding: 4px;\n}\n\n.skai-kb-move-item {\n border: 0;\n background: transparent;\n text-align: left;\n padding: 6px 8px;\n font-size: 12.5px;\n border-radius: 6px;\n cursor: pointer;\n color: var(--skai-kb-text, #374151);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.skai-kb-move-item:hover {\n background: var(--skai-kb-bg-hover, #f5f6f8);\n}\n\n.skai-kb-move-item.is-active {\n background: var(--skai-kb-primary-weak, #eef4ff);\n color: var(--skai-kb-primary, #2f6bff);\n font-weight: 500;\n}\n\n/* =======================================================================\n 文档在线编辑:Markdown 可视化编辑器(工具栏 + 双栏分屏)\n ======================================================================= */\n\n.skai-kb-editor {\n display: flex;\n flex-direction: column;\n gap: 8px;\n height: 100%;\n min-height: 320px;\n}\n\n.skai-kb-editor-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.skai-kb-editor-tools {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-wrap: wrap;\n}\n\n.skai-kb-editor-tool {\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n background: var(--skai-kb-bg-card, #fff);\n border-radius: 6px;\n padding: 3px 7px;\n font-size: 11.5px;\n line-height: 16px;\n color: var(--skai-kb-text, #374151);\n cursor: pointer;\n}\n\n.skai-kb-editor-tool:hover:not(:disabled) {\n border-color: var(--skai-kb-primary, #2f6bff);\n color: var(--skai-kb-primary, #2f6bff);\n}\n\n.skai-kb-editor-tool:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.skai-kb-editor-modes {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.skai-kb-editor-mode {\n border: 1px solid transparent;\n background: transparent;\n padding: 3px 8px;\n font-size: 12px;\n border-radius: 6px;\n cursor: pointer;\n color: var(--skai-kb-text-sub, #6b7280);\n}\n\n.skai-kb-editor-mode.is-active {\n background: var(--skai-kb-primary-weak, #eef4ff);\n color: var(--skai-kb-primary, #2f6bff);\n font-weight: 500;\n}\n\n.skai-kb-editor-dirty {\n font-size: 11px;\n color: var(--skai-kb-warn, #d97706);\n}\n\n/* 双栏分屏:编辑区 | 预览区(仅预览/仅编辑时单栏占满) */\n.skai-kb-editor-panes {\n display: flex;\n gap: 8px;\n flex: 1;\n min-height: 0;\n}\n\n.skai-kb-editor-input {\n flex: 1;\n min-width: 0;\n min-height: 300px;\n resize: vertical;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 10px;\n padding: 10px 12px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--skai-kb-text, #374151);\n background: var(--skai-kb-bg-card, #fff);\n}\n\n.skai-kb-editor-input:focus {\n outline: none;\n border-color: var(--skai-kb-primary, #2f6bff);\n}\n\n/* 仅编辑模式:编辑框撑满父容器高度(取消最小高度下限与手动拉伸) */\n.skai-kb-editor.edit .skai-kb-editor-input {\n min-height: 0;\n resize: none;\n}\n\n.skai-kb-editor-preview {\n flex: 1;\n min-width: 0;\n min-height: 300px;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 10px;\n padding: 10px 12px;\n overflow-y: auto;\n background: var(--skai-kb-bg-soft, #fafafa);\n}\n\n/* 窄容器:编辑器分屏改为上下堆叠 */\n@container skai-kb (max-width: 560px) {\n .skai-kb-doc-layout {\n flex-direction: column;\n }\n\n .skai-kb-folder-tree {\n width: auto;\n position: static;\n }\n\n .skai-kb-folder-body {\n flex-direction: row;\n flex-wrap: wrap;\n max-height: none;\n }\n\n .skai-kb-editor-panes.split {\n flex-direction: column;\n }\n\n .skai-kb-editor-panes.split .skai-kb-editor-input,\n .skai-kb-editor-panes.split .skai-kb-editor-preview {\n min-height: 180px;\n }\n}\n\n/* ---------- 对话消息操作区「存入知识库」(conversation.chat.assistant-actions) ----------\n 按钮渲染于宿主 AI 消息操作行(官方复制/点赞同款 28px 图标钮),颜色直接取\n DSH 官方令牌(--dsw-alias-*,全局可达,随宿主浅/深色主题切换);\n --skai-kb-* 别名仅作 standalone 兜底 */\n.skai-kb-chat-save-btn {\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px;\n color: var(--dsw-alias-label-tertiary, var(--skai-kb-text-caption, #9ca1ab));\n background: none;\n border: none;\n border-radius: 28px;\n cursor: pointer;\n transition: color 0.15s, background 0.15s;\n}\n\n.skai-kb-chat-save-btn:hover {\n color: var(--dsw-alias-label-secondary, var(--skai-kb-text-sub, #8a8f99));\n background: var(--dsw-alias-interactive-bg-hover, var(--skai-kb-bg-hover, rgba(0, 0, 0, 0.04)));\n}\n\n.skai-kb-chat-save-preview {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.skai-kb-chat-save-preview-label {\n font-size: 12px;\n color: var(--skai-kb-text-caption);\n}\n\n/* 保存方式切换(新建文档 / 覆盖已有文档):与弹窗按钮风格一致 */\n.skai-kb-chat-save-modes {\n display: flex;\n gap: 8px;\n}\n.skai-kb-chat-save-hint {\n display: block;\n margin-top: 4px;\n font-size: 12px;\n color: var(--skai-kb-text-caption);\n}\n\n.skai-kb-chat-save-preview pre {\n max-height: 140px;\n overflow: auto;\n margin: 0;\n padding: 8px 10px;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-wrap;\n word-break: break-word;\n background: var(--skai-kb-bg-soft);\n border-radius: 6px;\n}\n\n";
8526
+ var knowledge_default = "/* ============================================================\n 时刻AI知识库插件样式(前缀 skai-kb-,不覆盖宿主样式)\n - 0.8.0 起渲染于 DSH 右侧 Sidebar 标签页正文(首页 / 知识库详情 /\n 文档详情 / 设置 各为一个标签页)与 standalone 调试台:\n .skai-kb-page 声明 inline-size 容器查询(与 .skai-kb-app 同名 skai-kb),\n 布局按容器宽度自适应,不依赖视口宽度(面板开合、拖宽均正确响应)\n - 颜色全部取 DSH 官方主题令牌(--dsw-alias-*),随宿主\n 浅色/深色主题自动切换;var() 兜底值仅供 standalone 预览\n (无宿主令牌)使用\n - z-index 使用适中值(≤35),禁止覆盖宿主设置菜单\n ============================================================ */\n\n/* ---------- 主题令牌别名层(统一映射到 DSH 官方令牌) ----------\n DSH 强调色语义(与常见设计系统相反,务必注意):\n - --dsw-alias-brand-primary:亮色主题为深色(bluish-1000)、暗色主题为浅色\n (bluish-50),即「反色强调」——主按钮填充随主题黑白互换\n - --dsw-alias-label-primary-foreground:强调填充上的前景文字,恒与填充反相\n (亮色=白、暗色=黑),是主按钮等强调填充上文字的正确取值。\n (DSH 无 --dsw-alias-brand-text;此前误用导致暗色模式白底白字)\n - --dsw-alias-link:正文链接色(亮 deepseek-500 / 暗 deepseek-400,恒为蓝) */\n.skai-kb-root,\n.skai-kb-modal-mask,\n.skai-kb-toast-layer {\n --skai-kb-bg: var(--dsw-alias-bg-base, #ffffff);\n --skai-kb-bg-card: var(--dsw-alias-bg-layer-1, #ffffff);\n --skai-kb-bg-soft: var(--dsw-alias-bg-layer-2, #f6f7f9);\n --skai-kb-bg-hover: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04));\n --skai-kb-mask: var(--dsw-alias-bg-mask-1, rgba(17, 19, 23, 0.5));\n --skai-kb-border: var(--dsw-alias-border-l1, #e4e6eb);\n --skai-kb-border-strong: var(--dsw-alias-border-l2, #d0d3d9);\n --skai-kb-text: var(--dsw-alias-label-primary, #1f2329);\n --skai-kb-text-sub: var(--dsw-alias-label-secondary, #8a8f99);\n --skai-kb-text-caption: var(--dsw-alias-label-caption, #9ca1ab);\n /* 知识库品牌青色:与 DSH 侧边栏标签页/引导页图标同色(#06b6d4) */\n --skai-kb-primary: #06b6d4;\n /* 强调填充上的文字(官方前景令牌:亮=白 / 暗=黑;无 token 环境兜底白色) */\n --skai-kb-on-primary: var(--dsw-alias-label-primary-foreground, #ffffff);\n /* 主按钮:品牌青色实心 + 白字(青色为高亮度色相,不适用深底反色公式),hover 提亮一档(与 CRM 一致) */\n --skai-kb-btn-primary: var(--skai-kb-primary);\n --skai-kb-primary-weak: color-mix(in srgb, var(--skai-kb-primary) 12%, transparent);\n --skai-kb-danger: var(--dsw-alias-state-error-primary, #d83931);\n --skai-kb-danger-weak: color-mix(in srgb, var(--skai-kb-danger) 12%, transparent);\n --skai-kb-ok: var(--dsw-alias-state-success-primary, #2ea44f);\n --skai-kb-ok-weak: color-mix(in srgb, var(--skai-kb-ok) 14%, transparent);\n --skai-kb-warn: var(--dsw-alias-state-warn-primary, #d97706);\n --skai-kb-warn-weak: color-mix(in srgb, var(--skai-kb-warn) 14%, transparent);\n --skai-kb-link: var(--skai-kb-primary);\n --skai-kb-shadow: 0 8px 24px rgba(31, 35, 41, 0.12);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--skai-kb-text);\n background: var(--skai-kb-bg);\n scrollbar-width: thin;\n}\n\n/* 盒模型统一(与 CRM 一致):root 作用域内全部 border-box */\n.skai-kb-root *,\n.skai-kb-root *::before,\n.skai-kb-root *::after,\n.skai-kb-modal-mask *,\n.skai-kb-modal-mask *::before,\n.skai-kb-modal-mask *::after {\n box-sizing: border-box;\n}\n\n/* 原生 button 不继承字体,与 CRM 一致显式继承宿主字体 */\n.skai-kb-root button,\n.skai-kb-modal-mask button {\n font-family: inherit;\n}\n\n/* 深色模式(跟随系统 prefers-color-scheme,与 CRM 插件策略一致):\n 宿主令牌存在时仍优先取宿主值(var 包裹),此处仅提供 standalone 兜底 */\n@media (prefers-color-scheme: dark) {\n .skai-kb-root,\n .skai-kb-modal-mask,\n .skai-kb-toast-layer {\n --skai-kb-bg: var(--dsw-alias-bg-base, #1e1f22);\n --skai-kb-bg-card: var(--dsw-alias-bg-layer-1, #1e1f22);\n --skai-kb-bg-soft: var(--dsw-alias-bg-layer-2, #2a2b2e);\n --skai-kb-bg-hover: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08));\n --skai-kb-mask: rgba(0, 0, 0, 0.55);\n --skai-kb-border: var(--dsw-alias-border-l1, #2c2d2f);\n --skai-kb-border-strong: var(--dsw-alias-border-l2, #3a3d42);\n --skai-kb-text: var(--dsw-alias-label-primary, #e8eaed);\n --skai-kb-text-sub: var(--dsw-alias-label-secondary, #8b8f96);\n --skai-kb-text-caption: var(--dsw-alias-label-caption, #6f747d);\n --skai-kb-primary: #22d3ee;\n --skai-kb-on-primary: var(--dsw-alias-label-primary-foreground, #10131a);\n --skai-kb-btn-primary: #06b6d4;\n --skai-kb-danger: var(--dsw-alias-state-error-primary, #f5564e);\n --skai-kb-ok: var(--dsw-alias-state-success-primary, #46c26a);\n --skai-kb-warn: var(--dsw-alias-state-warn-primary, #e8a23c);\n --skai-kb-link: var(--skai-kb-primary);\n --skai-kb-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);\n }\n}\n\n/* ---------- 布局骨架(容器查询基准) ---------- */\n.skai-kb-app {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n position: relative;\n container-type: inline-size;\n container-name: skai-kb;\n}\n\n/* standalone 调试台挂载容器(模拟抽屉内 100% 高度) */\n.skai-kb-standalone-inner {\n height: 100%;\n min-height: 0;\n}\n\n/* ---------- DSH 标签页正文骨架 ----------\n sidebar.right.pane.tab 正文:不滚动定位容器(弹窗/Toast 绝对定位收敛于此),\n 内容滚动由 .skai-kb-page-scroll 承担(文档详情为 .skai-kb-md-wrap) */\n.skai-kb-tab-root {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.skai-kb-page {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 12px;\n height: 100%;\n min-height: 0;\n padding: 14px 16px;\n overflow: hidden;\n container-type: inline-size;\n container-name: skai-kb;\n}\n.skai-kb-tab-root > .skai-kb-page {\n flex: 1;\n}\n.skai-kb-page-scroll {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.skai-kb-header {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 14px 16px 0;\n flex: none;\n}\n.skai-kb-header-title {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 15px;\n font-weight: 600;\n color: var(--skai-kb-text);\n}\n\n/* 分区导航:窄容器横向滑动(不换行挤压),宽容器自然展开 */\n.skai-kb-nav {\n display: flex;\n gap: 4px;\n overflow-x: auto;\n scrollbar-width: none;\n padding-bottom: 8px;\n border-bottom: 1px solid var(--skai-kb-border);\n align-items: center;\n}\n.skai-kb-nav::-webkit-scrollbar {\n display: none;\n}\n.skai-kb-nav-btn {\n flex: none;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-sub);\n padding: 4px 10px;\n border-radius: 999px;\n cursor: pointer;\n font-size: 12px;\n transition: background 0.15s ease, color 0.15s ease;\n}\n.skai-kb-nav-btn:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-text);\n}\n.skai-kb-nav-btn.is-active {\n background: var(--skai-kb-primary-weak);\n color: var(--skai-kb-primary);\n font-weight: 500;\n}\n.skai-kb-nav-btn:focus-visible {\n outline: 2px solid var(--skai-kb-primary);\n outline-offset: 1px;\n}\n/* standalone 页签关闭钮 */\n.skai-kb-tab-close {\n flex: none;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-caption);\n cursor: pointer;\n font-size: 10px;\n padding: 2px 4px;\n border-radius: 6px;\n}\n.skai-kb-tab-close:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-danger);\n}\n\n.skai-kb-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 14px 16px;\n}\n\n/* ---------- 详情页头(知识库详情 / 文档详情标签页) ---------- */\n.skai-kb-detail-head {\n flex: none;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-bottom: 8px;\n}\n.skai-kb-detail-title {\n font-weight: 600;\n font-size: 15px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-back {\n flex: none;\n width: auto;\n min-width: 0;\n}\n\n/* ---------- 首页页头(左侧说明介绍 + 右上角设置入口) ---------- */\n.skai-kb-home-head {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 10px;\n padding-bottom: 10px;\n}\n.skai-kb-home-head-main {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n.skai-kb-home-desc {\n font-size: 12px;\n line-height: 1.6;\n color: var(--skai-kb-text-sub);\n /* 介绍信息完整展示:不截断,自动换行铺满全量文字 */\n overflow-wrap: anywhere;\n}\n.skai-kb-home-head > .skai-kb-btn {\n flex: none;\n width: auto;\n margin-top: 1px;\n}\n\n/* ---------- 通用元素 ---------- */\n.skai-kb-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n width: auto;\n height: 30px;\n padding: 0 12px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n background: var(--skai-kb-bg-card);\n color: var(--skai-kb-text);\n cursor: pointer;\n font-size: 12.5px;\n white-space: nowrap;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n.skai-kb-btn:hover:not(:disabled) {\n background: var(--skai-kb-bg-soft);\n}\n.skai-kb-btn:focus-visible {\n outline: 2px solid var(--skai-kb-primary);\n outline-offset: 1px;\n}\n.skai-kb-btn:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n/* 等待转圈动画(SpinnerGlyph 专用) */\n@keyframes skai-kb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.skai-kb-spin {\n animation: skai-kb-spin 0.8s linear infinite;\n flex: none;\n}\n.skai-kb-btn-primary {\n background: var(--skai-kb-btn-primary);\n border-color: transparent;\n color: #fff;\n}\n.skai-kb-btn-primary:hover:not(:disabled) {\n background: var(--skai-kb-btn-primary);\n filter: brightness(1.08);\n}\n.skai-kb-btn-danger {\n background: var(--skai-kb-danger);\n border-color: var(--skai-kb-danger);\n color: #fff;\n}\n.skai-kb-btn-danger:hover:not(:disabled) {\n background: var(--skai-kb-danger);\n filter: brightness(1.05);\n}\n.skai-kb-btn-ghost-danger {\n border-color: transparent;\n background: transparent;\n color: var(--skai-kb-danger);\n}\n.skai-kb-btn-ghost-danger:hover:not(:disabled) {\n background: var(--skai-kb-danger-weak);\n}\n.skai-kb-btn-sm {\n width: auto;\n height: 26px;\n padding: 0 9px;\n font-size: 12px;\n border-radius: 8px;\n}\n\n.skai-kb-icon-btn {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-sub);\n cursor: pointer;\n border-radius: 8px;\n font-size: 12px;\n}\n.skai-kb-icon-btn:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-text);\n}\n/* 悬停文字提示(CSS 实现,即时显示;按钮右上角对齐避免溢出弹窗) */\n.skai-kb-icon-btn[data-tip]:hover::after {\n content: attr(data-tip);\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 30;\n padding: 4px 8px;\n border-radius: 6px;\n background: var(--skai-kb-bg);\n color: var(--skai-kb-text);\n font-size: 12px;\n line-height: 1.4;\n white-space: nowrap;\n pointer-events: none;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n}\n\n.skai-kb-input,\n.skai-kb-select,\n.skai-kb-textarea {\n width: 100%;\n padding: 5px 10px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n background: var(--skai-kb-bg);\n color: var(--skai-kb-text);\n /* 原生表单控件不继承字体,与 CRM 一致显式继承 */\n font: inherit;\n font-size: 12.5px;\n box-sizing: border-box;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n}\n/* 下拉选择与 CRM 一致使用手型光标 */\n.skai-kb-select {\n cursor: pointer;\n}\n/* 单行输入控件与按钮(30px)同高 */\n.skai-kb-input,\n.skai-kb-select {\n height: 30px;\n}\n.skai-kb-textarea {\n min-height: 56px;\n resize: vertical;\n line-height: 1.6;\n}\n.skai-kb-input:focus,\n.skai-kb-select:focus,\n.skai-kb-textarea:focus {\n outline: none;\n border-color: var(--skai-kb-primary);\n box-shadow: 0 0 0 2px var(--skai-kb-primary-weak);\n}\n.skai-kb-input::placeholder,\n.skai-kb-textarea::placeholder {\n color: var(--skai-kb-text-caption);\n}\n.skai-kb-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n}\n.skai-kb-field-label {\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n}\n.skai-kb-field-error {\n font-size: 12px;\n color: var(--skai-kb-danger);\n}\n.skai-kb-field-row {\n display: flex;\n gap: 8px;\n}\n.skai-kb-field-row > * {\n flex: 1;\n}\n\n.skai-kb-toolbar {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n flex-wrap: wrap;\n flex: none;\n}\n.skai-kb-toolbar .skai-kb-search {\n flex: 1;\n min-width: 120px;\n}\n\n.skai-kb-card {\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n padding: 10px 12px;\n margin-bottom: 8px;\n transition: border-color 0.15s ease, background 0.15s ease;\n}\n.skai-kb-card:hover {\n border-color: var(--skai-kb-border-strong);\n}\n\n/* 卡片列表:每行一个知识库卡片(纵向堆叠,占满整行宽度) */\n.skai-kb-card-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.skai-kb-card-list > .skai-kb-card {\n margin-bottom: 0;\n}\n\n.skai-kb-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.skai-kb-row-between {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.skai-kb-title {\n font-weight: 600;\n font-size: 13px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-sub {\n padding: 7px 0 0 0;\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n /* 说明/介绍类文本完整展示:不截断省略,自动换行铺满全量文字 */\n overflow-wrap: anywhere;\n}\n\n.skai-kb-badge {\n display: inline-flex;\n align-items: center;\n padding: 0 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 18px;\n white-space: nowrap;\n}\n.skai-kb-badge-gray { background: var(--skai-kb-bg-soft); color: var(--skai-kb-text-sub); }\n.skai-kb-badge-blue { background: var(--skai-kb-primary-weak); color: var(--skai-kb-primary); }\n.skai-kb-badge-green { background: var(--skai-kb-ok-weak); color: var(--skai-kb-ok); }\n.skai-kb-badge-amber { background: var(--skai-kb-warn-weak); color: var(--skai-kb-warn); }\n.skai-kb-badge-red { background: var(--skai-kb-danger-weak); color: var(--skai-kb-danger); }\n\n.skai-kb-tag {\n display: inline-block;\n padding: 0 6px;\n border-radius: 4px;\n font-size: 11px;\n line-height: 18px;\n background: var(--skai-kb-bg-soft);\n color: var(--skai-kb-text-sub);\n margin-right: 4px;\n}\n\n.skai-kb-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n text-align: center;\n padding: 28px 16px;\n color: var(--skai-kb-text-sub);\n font-size: 13px;\n}\n.skai-kb-empty-sub {\n font-size: 12px;\n}\n\n.skai-kb-pagination {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 8px 0;\n color: var(--skai-kb-text-sub);\n font-size: 12px;\n flex: none;\n}\n.skai-kb-pagination-btns {\n display: flex;\n gap: 8px;\n}\n\n/* ---------- 设置页(对齐 CRM:页头 + panel 区块 + 行操作 + 详情列表) ---------- */\n.skai-kb-page-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n flex: none;\n}\n.skai-kb-page-title {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n}\n.skai-kb-page-title svg {\n color: var(--skai-kb-primary);\n}\n.skai-kb-page-actions {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.skai-kb-panel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n}\n.skai-kb-panel-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n}\n.skai-kb-panel-head h3 {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n.skai-kb-panel-head .skai-kb-btn {\n flex: none;\n width: auto;\n}\n.skai-kb-muted {\n color: var(--skai-kb-text-sub);\n font-size: 12px;\n}\n.skai-kb-row-actions {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n.skai-kb-server-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n.skai-kb-server-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n background: var(--skai-kb-bg-card);\n padding: 10px 12px;\n transition: border-color 0.15s ease;\n}\n.skai-kb-server-item.is-active {\n border-color: var(--skai-kb-primary);\n background: var(--skai-kb-primary-weak);\n}\n.skai-kb-server-info {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n/* 服务器条目下方健康提示(应用id / 版本匹配状态,探测 /kb/health) */\n.skai-kb-server-health {\n font-size: 12px;\n line-height: 1.5;\n}\n\n.skai-kb-server-health.is-ok {\n color: var(--skai-kb-ok);\n}\n\n.skai-kb-server-health.is-danger {\n color: var(--skai-kb-danger);\n}\n\n.skai-kb-server-health.is-muted {\n color: var(--skai-kb-text-sub);\n}\n\n.skai-kb-server-name {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 500;\n font-size: 13px;\n}\n.skai-kb-server-url {\n margin-top: 2px;\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n word-break: break-all;\n}\n.skai-kb-mcp-status {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n.skai-kb-mcp-text {\n font-size: 12.5px;\n line-height: 1.6;\n}\n.skai-kb-cloud-hint {\n padding: 8px 10px;\n border: 1px dashed var(--skai-kb-border);\n border-radius: 8px;\n}\n.skai-kb-detail-list {\n display: flex;\n flex-direction: column;\n}\n.skai-kb-detail-row {\n display: flex;\n gap: 12px;\n padding: 7px 0;\n border-bottom: 1px dashed var(--skai-kb-border);\n font-size: 12.5px;\n}\n.skai-kb-detail-row:last-child {\n border-bottom: none;\n}\n.skai-kb-detail-row .skai-kb-field-label {\n width: 92px;\n flex-shrink: 0;\n padding-top: 1px;\n}\n.skai-kb-detail-text {\n flex: 1;\n min-width: 0;\n word-break: break-all;\n white-space: pre-wrap;\n}\n\n/* ---------- 用户管理(设置页区块:审核模式开关 / 筛选工具条 / 状态横幅) ---------- */\n.skai-kb-user-review-mode {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n padding: 8px 10px;\n border: 1px dashed var(--skai-kb-border);\n border-radius: 8px;\n margin-bottom: 10px;\n}\n.skai-kb-user-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n margin-bottom: 8px;\n}\n\n/* 添加成员弹窗「从用户列表选择」候选列表(弹窗内滚动,选中高亮) */\n.skai-kb-user-pick-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 200px;\n overflow-y: auto;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 8px;\n padding: 4px;\n margin-bottom: 8px;\n}\n.skai-kb-user-pick-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n padding: 6px 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n}\n.skai-kb-user-pick-item:hover {\n background: var(--skai-kb-hover, rgba(0, 0, 0, 0.04));\n}\n.skai-kb-user-pick-item-active,\n.skai-kb-user-pick-item-active:hover {\n background: var(--skai-kb-primary-soft, rgba(49, 92, 255, 0.1));\n}\n.skai-kb-user-pick-nickname {\n display: block;\n font-weight: 500;\n}\n/* 多选复选标记(行首圆角方框,选中品牌色) */\n.skai-kb-pick-check {\n flex: none;\n width: 16px;\n height: 16px;\n border: 1px solid var(--skai-kb-border, #d1d5db);\n border-radius: 4px;\n font-size: 11px;\n line-height: 14px;\n text-align: center;\n color: #fff;\n}\n.skai-kb-pick-check-on {\n background: var(--skai-kb-primary, #17b0c8);\n border-color: var(--skai-kb-primary, #17b0c8);\n}\n.skai-kb-user-pick-item-disabled,\n.skai-kb-user-pick-item-disabled:hover {\n opacity: 0.55;\n cursor: not-allowed;\n background: transparent;\n}\n/* 已选用户 chips(头像 + 昵称 + 移除) */\n.skai-kb-picked-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 8px;\n}\n.skai-kb-picked-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 3px 6px 3px 4px;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 999px;\n background: var(--skai-kb-primary-soft, rgba(23, 176, 200, 0.08));\n font-size: 12px;\n}\n.skai-kb-picked-chip-avatar {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n object-fit: cover;\n}\n.skai-kb-picked-chip-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-picked-chip-remove {\n flex: none;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.08);\n color: inherit;\n font-size: 12px;\n line-height: 16px;\n cursor: pointer;\n}\n.skai-kb-picked-chip-remove:hover {\n background: rgba(0, 0, 0, 0.16);\n}\n.skai-kb-user-search {\n width: 200px;\n height: 26px;\n flex: none;\n}\n\n/* ---------- 弹窗(Portal 挂 body,遮罩 fixed 铺满整个窗口并居中,与 CRM 同款;\n 弹层根已纳入文件头部主题令牌作用域;z-index ≤35 不覆盖宿主设置菜单) ---------- */\n.skai-kb-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 30;\n background: var(--skai-kb-mask);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n animation: skai-kb-fade-in 0.15s ease;\n}\n.skai-kb-modal {\n width: 100%;\n max-width: 520px;\n max-height: 86%;\n display: flex;\n flex-direction: column;\n background: var(--skai-kb-bg-card);\n border-radius: 10px;\n box-shadow: var(--skai-kb-shadow);\n animation: skai-kb-pop-in 0.16s ease;\n}\n.skai-kb-modal.is-fullscreen {\n max-width: none;\n width: 100%;\n max-height: 96%;\n height: 96%;\n}\n.skai-kb-modal-head-actions {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n flex: none;\n}\n.skai-kb-modal-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--skai-kb-border);\n flex: none;\n}\n.skai-kb-modal-title {\n font-size: 14px;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-modal-body {\n padding: 14px 16px;\n overflow-y: auto;\n}\n.skai-kb-modal-foot {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 10px 16px;\n border-top: 1px solid var(--skai-kb-border);\n flex: none;\n}\n.skai-kb-modal-foot .skai-kb-btn {\n width: auto;\n min-width: 72px;\n}\n.skai-kb-confirm-text {\n margin: 0;\n font-size: 13px;\n line-height: 1.7;\n}\n\n/* ---------- Toast(面板底 + 彩色描边样式,与 CRM 一致;z-index 34 宿主菜单避让约定内) ---------- */\n.skai-kb-toast-layer {\n /* createPortal 到 body + fixed:相对整个 DSH 窗口顶部居中(非插件标签页区域);\n z-index 34 宿主菜单避让约定内 */\n position: fixed;\n left: 50%;\n top: 18px;\n transform: translateX(-50%);\n z-index: 34;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: center;\n pointer-events: none;\n}\n.skai-kb-toast {\n pointer-events: auto;\n max-width: 90%;\n padding: 7px 14px;\n border-radius: 999px;\n font-size: 12.5px;\n background: var(--skai-kb-bg-card);\n border: 1px solid var(--skai-kb-border);\n color: var(--skai-kb-text);\n box-shadow: var(--skai-kb-shadow);\n cursor: pointer;\n word-break: break-all;\n animation: skai-kb-toast-in 0.18s ease;\n}\n.skai-kb-toast-info { border-color: var(--skai-kb-primary); color: var(--skai-kb-primary); }\n.skai-kb-toast-error { border-color: var(--skai-kb-danger); color: var(--skai-kb-danger); }\n.skai-kb-toast-success { border-color: var(--skai-kb-ok); color: var(--skai-kb-ok); }\n.skai-kb-toast-warning { border-color: var(--skai-kb-warn); color: var(--skai-kb-warn); white-space: pre-line; }\n\n/* ---------- 动画(与 CRM 同款节奏) ---------- */\n@keyframes skai-kb-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes skai-kb-pop-in {\n from { opacity: 0; transform: translateY(8px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n@keyframes skai-kb-toast-in {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ---------- 概览统计(首页) ---------- */\n.skai-kb-stat-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n margin-bottom: 4px;\n}\n.skai-kb-stat-card {\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n padding: 10px 12px;\n}\n.skai-kb-stat-num {\n font-size: 18px;\n font-weight: 600;\n}\n/* 统计卡内气泡(首页「用户数」待审核提示) */\n.skai-kb-stat-badge {\n margin-left: 8px;\n vertical-align: 2px;\n cursor: default;\n}\n.skai-kb-stat-label {\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n margin-top: 2px;\n}\n.skai-kb-quick-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n}\n\n/* ---------- 知识库卡片 ---------- */\n.skai-kb-kb-meta {\n display: flex;\n gap: 12px;\n margin-top: 6px;\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n flex-wrap: wrap;\n}\n.skai-kb-kb-actions {\n display: flex;\n gap: 6px;\n margin-top: 8px;\n flex-wrap: wrap;\n}\n.skai-kb-kb-actions .skai-kb-btn-sm {\n flex: 1;\n}\n\n/* ---------- 文档列表 ---------- */\n.skai-kb-doc-item {\n cursor: pointer;\n}\n\n/* 文档卡片右上角「操作」下拉菜单(遮罩点击关闭;z-index ≤35 不覆盖宿主) */\n.skai-kb-doc-topbar {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: none;\n}\n.skai-kb-doc-menu-anchor {\n position: relative;\n display: inline-flex;\n}\n.skai-kb-doc-menu-trigger {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n line-height: 1;\n border: none;\n background: transparent;\n color: var(--skai-kb-text-sub);\n font-size: 12px;\n padding: 2px 6px;\n border-radius: 6px;\n cursor: pointer;\n}\n/* CSS 三角下箭头:随 flex 垂直居中,字体字形对齐不可控故不用 ⌄ */\n.skai-kb-caret {\n width: 0;\n height: 0;\n border-left: 4px solid transparent;\n border-right: 4px solid transparent;\n border-top: 5px solid currentColor;\n}\n.skai-kb-doc-menu-trigger:hover {\n background: var(--skai-kb-bg-hover);\n color: var(--skai-kb-text);\n}\n.skai-kb-menu-mask {\n position: fixed;\n inset: 0;\n z-index: 34;\n}\n.skai-kb-menu {\n position: absolute;\n top: 22px;\n right: 0;\n z-index: 35;\n min-width: 104px;\n padding: 4px;\n background: var(--skai-kb-bg-card);\n border: 1px solid var(--skai-kb-border-strong);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);\n}\n.skai-kb-menu-item {\n display: block;\n width: 100%;\n text-align: left;\n border: none;\n background: transparent;\n color: var(--skai-kb-text);\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 6px;\n cursor: pointer;\n white-space: nowrap;\n}\n.skai-kb-menu-item:hover {\n background: var(--skai-kb-bg-hover);\n}\n.skai-kb-menu-item.is-danger {\n color: var(--skai-kb-danger);\n}\n.skai-kb-menu-item.is-danger:hover {\n background: var(--skai-kb-danger-weak);\n}\n\n.skai-kb-pre {\n white-space: pre-wrap;\n word-break: break-word;\n background: var(--skai-kb-bg-soft);\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n padding: 8px 10px;\n max-height: 260px;\n overflow-y: auto;\n font-size: 12px;\n margin: 0;\n}\n\n/* ---------- 文档详情正文 ---------- */\n.skai-kb-doc-detail {\n gap: 8px;\n}\n.skai-kb-md-wrap {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--skai-kb-border);\n border-radius: 10px;\n background: var(--skai-kb-bg-card);\n padding: 14px 16px;\n}\n\n/* ---------- Markdown 渲染(文档详情正文;.skai-kb-md 作用域,不外溢) ---------- */\n.skai-kb-md {\n font-size: 13px;\n line-height: 1.7;\n word-break: break-word;\n}\n.skai-kb-md .skai-kb-md-h {\n font-weight: 600;\n line-height: 1.4;\n margin: 14px 0 6px;\n}\n.skai-kb-md .skai-kb-md-h:first-child {\n margin-top: 0;\n}\n.skai-kb-md .skai-kb-md-h1 { font-size: 18px; }\n.skai-kb-md .skai-kb-md-h2 { font-size: 16px; }\n.skai-kb-md .skai-kb-md-h3 { font-size: 15px; }\n.skai-kb-md .skai-kb-md-h4,\n.skai-kb-md .skai-kb-md-h5,\n.skai-kb-md .skai-kb-md-h6 { font-size: 14px; }\n.skai-kb-md .skai-kb-md-p {\n margin: 6px 0;\n}\n.skai-kb-md .skai-kb-md-code {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n background: var(--skai-kb-bg-soft);\n border: 1px solid var(--skai-kb-border);\n border-radius: 4px;\n padding: 0 4px;\n}\n.skai-kb-md .skai-kb-md-pre {\n background: var(--skai-kb-bg-soft);\n border: 1px solid var(--skai-kb-border);\n border-radius: 8px;\n padding: 10px 12px;\n overflow-x: auto;\n font-size: 12px;\n line-height: 1.6;\n margin: 8px 0;\n white-space: pre;\n}\n.skai-kb-md .skai-kb-md-pre code {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n background: transparent;\n border: none;\n padding: 0;\n}\n.skai-kb-md .skai-kb-md-quote {\n margin: 8px 0;\n padding: 4px 12px;\n border-left: 3px solid var(--skai-kb-border-strong);\n color: var(--skai-kb-text-sub);\n background: var(--skai-kb-bg-soft);\n border-radius: 0 8px 8px 0;\n}\n.skai-kb-md .skai-kb-md-ul,\n.skai-kb-md .skai-kb-md-ol {\n margin: 6px 0;\n padding-left: 22px;\n}\n.skai-kb-md .skai-kb-md-li {\n margin: 2px 0;\n}\n.skai-kb-md .skai-kb-md-link {\n color: var(--skai-kb-link);\n text-decoration: none;\n}\n.skai-kb-md .skai-kb-md-link:hover {\n text-decoration: underline;\n}\n.skai-kb-md .skai-kb-md-img {\n max-width: 100%;\n border-radius: 8px;\n border: 1px solid var(--skai-kb-border);\n}\n.skai-kb-md .skai-kb-md-hr {\n border: none;\n border-top: 1px solid var(--skai-kb-border);\n margin: 12px 0;\n}\n.skai-kb-md .skai-kb-md-table-wrap {\n overflow-x: auto;\n margin: 8px 0;\n}\n.skai-kb-md .skai-kb-md-table {\n border-collapse: collapse;\n font-size: 12px;\n min-width: 60%;\n}\n.skai-kb-md .skai-kb-md-table th,\n.skai-kb-md .skai-kb-md-table td {\n border: 1px solid var(--skai-kb-border);\n padding: 4px 10px;\n text-align: left;\n}\n.skai-kb-md .skai-kb-md-table th {\n background: var(--skai-kb-bg-soft);\n font-weight: 600;\n}\n\n/* ---------- 时间线(文档版本) ---------- */\n.skai-kb-timeline {\n list-style: none;\n margin: 0;\n padding: 0 0 0 14px;\n border-left: 2px solid var(--skai-kb-border);\n}\n.skai-kb-timeline li {\n position: relative;\n padding: 0 0 12px 10px;\n}\n.skai-kb-timeline li::before {\n content: '';\n position: absolute;\n left: -20px;\n top: 4px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--skai-kb-primary);\n}\n\n/* ---------- 文档上传 ---------- */\n.skai-kb-dropzone {\n border: 1px dashed var(--skai-kb-border-strong);\n border-radius: 8px;\n padding: 12px;\n}\n\n/* ---------- 成员权限 ---------- */\n.skai-kb-member-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 0;\n border-bottom: 1px solid var(--skai-kb-border);\n}\n.skai-kb-member-row:last-child {\n border-bottom: none;\n}\n.skai-kb-member-avatar {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n flex: none;\n object-fit: cover;\n background: var(--skai-kb-bg-soft);\n}\n.skai-kb-member-avatar-fallback {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 500;\n color: var(--skai-kb-primary);\n background: var(--skai-kb-primary-weak);\n}\n.skai-kb-member-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n.skai-kb-member-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.skai-kb-member-mobile {\n font-size: 12px;\n color: var(--skai-kb-text-sub);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ============================================================\n 宽容器布局(DSH 标签页 / standalone 宽屏):按容器宽度自适应,\n 与视口无关(面板开合、拖宽均正确响应)\n ============================================================ */\n@container skai-kb (min-width: 620px) {\n /* 概览:统计四列 */\n .skai-kb-stat-grid {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n }\n .skai-kb-quick-grid {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n@container skai-kb (min-width: 980px) {\n /* 超宽容器:正文限宽保持可读性(卡片分列由基础网格自适应) */\n .skai-kb-page-scroll,\n .skai-kb-md-wrap {\n max-width: 1080px;\n }\n}\n\n/* =======================================================================\n 目录(文件夹)管理:目录树 + 文档列表布局 + 拖拽指示\n ======================================================================= */\n\n/* 详情页两栏布局:左目录树 + 右文档区(窄容器降级为上下布局) */\n.skai-kb-doc-layout {\n display: flex;\n gap: 10px;\n align-items: stretch;\n min-height: 0;\n}\n\n.skai-kb-doc-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* 目录树面板 */\n.skai-kb-folder-tree {\n flex: none;\n width: 168px;\n display: flex;\n flex-direction: column;\n background: var(--skai-kb-bg-card, #fff);\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 10px;\n padding: 6px;\n align-self: flex-start;\n position: sticky;\n top: 0;\n}\n\n.skai-kb-folder-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 6px 6px;\n border-bottom: 1px solid var(--skai-kb-bg-soft, #f0f1f3);\n margin-bottom: 4px;\n}\n\n.skai-kb-folder-head-title {\n font-size: 12px;\n font-weight: 600;\n color: var(--skai-kb-text-sub, #6b7280);\n}\n\n.skai-kb-folder-body {\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow-y: auto;\n max-height: 320px;\n}\n\n.skai-kb-folder-item {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 6px;\n border-radius: 7px;\n cursor: pointer;\n font-size: 12.5px;\n color: var(--skai-kb-text, #374151);\n border: 1px dashed transparent;\n user-select: none;\n min-width: 0;\n}\n\n.skai-kb-folder-item:hover {\n background: var(--skai-kb-bg-hover, #f5f6f8);\n}\n\n.skai-kb-folder-item.is-active {\n background: var(--skai-kb-primary-weak, #eef4ff);\n color: var(--skai-kb-primary, #2f6bff);\n font-weight: 500;\n}\n\n.skai-kb-folder-arrow {\n flex: none;\n width: 12px;\n font-size: 10px;\n color: var(--skai-kb-text-caption, #9ca3af);\n text-align: center;\n}\n\n.skai-kb-folder-arrow.is-leaf {\n visibility: hidden;\n}\n\n.skai-kb-folder-icon {\n flex: none;\n font-size: 12px;\n}\n\n.skai-kb-folder-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.skai-kb-folder-actions {\n flex: none;\n display: none;\n gap: 2px;\n}\n\n.skai-kb-folder-item:hover .skai-kb-folder-actions {\n display: inline-flex;\n}\n\n.skai-kb-folder-act {\n border: 0;\n background: transparent;\n padding: 0 3px;\n font-size: 11px;\n line-height: 16px;\n color: var(--skai-kb-text-caption, #9ca3af);\n border-radius: 4px;\n cursor: pointer;\n}\n\n.skai-kb-folder-act:hover {\n background: var(--skai-kb-bg-soft, #f0f1f3);\n color: var(--skai-kb-primary, #2f6bff);\n}\n\n.skai-kb-folder-act.is-danger:hover {\n color: var(--skai-kb-danger, #e5484d);\n}\n\n/* 目录树节点:文档拖入高亮(in)与同级排序插入线(before/after) */\n.skai-kb-folder-item.skai-kb-drop-in {\n border: 1px solid var(--skai-kb-primary, #2f6bff);\n background: var(--skai-kb-primary-weak, #eef4ff);\n}\n\n.skai-kb-folder-item.skai-kb-drop-before {\n box-shadow: inset 0 2px 0 0 var(--skai-kb-primary, #2f6bff);\n}\n\n.skai-kb-folder-item.skai-kb-drop-after {\n box-shadow: inset 0 -2px 0 0 var(--skai-kb-primary, #2f6bff);\n}\n\n/* 文档列表项:作为同目录排序 drop 目标时高亮 */\n.skai-kb-doc-item.skai-kb-drop-in {\n outline: 1.5px dashed var(--skai-kb-primary, #2f6bff);\n outline-offset: -2px;\n}\n\n/* 移动文档弹窗:目录选择列表 */\n.skai-kb-move-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 240px;\n overflow-y: auto;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 8px;\n padding: 4px;\n}\n\n.skai-kb-move-item {\n border: 0;\n background: transparent;\n text-align: left;\n padding: 6px 8px;\n font-size: 12.5px;\n border-radius: 6px;\n cursor: pointer;\n color: var(--skai-kb-text, #374151);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.skai-kb-move-item:hover {\n background: var(--skai-kb-bg-hover, #f5f6f8);\n}\n\n.skai-kb-move-item.is-active {\n background: var(--skai-kb-primary-weak, #eef4ff);\n color: var(--skai-kb-primary, #2f6bff);\n font-weight: 500;\n}\n\n/* =======================================================================\n 文档在线编辑:Markdown 可视化编辑器(工具栏 + 双栏分屏)\n ======================================================================= */\n\n.skai-kb-editor {\n display: flex;\n flex-direction: column;\n gap: 8px;\n height: 100%;\n min-height: 320px;\n}\n\n.skai-kb-editor-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.skai-kb-editor-tools {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-wrap: wrap;\n}\n\n.skai-kb-editor-tool {\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n background: var(--skai-kb-bg-card, #fff);\n border-radius: 6px;\n padding: 3px 7px;\n font-size: 11.5px;\n line-height: 16px;\n color: var(--skai-kb-text, #374151);\n cursor: pointer;\n}\n\n.skai-kb-editor-tool:hover:not(:disabled) {\n border-color: var(--skai-kb-primary, #2f6bff);\n color: var(--skai-kb-primary, #2f6bff);\n}\n\n.skai-kb-editor-tool:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.skai-kb-editor-modes {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.skai-kb-editor-mode {\n border: 1px solid transparent;\n background: transparent;\n padding: 3px 8px;\n font-size: 12px;\n border-radius: 6px;\n cursor: pointer;\n color: var(--skai-kb-text-sub, #6b7280);\n}\n\n.skai-kb-editor-mode.is-active {\n background: var(--skai-kb-primary-weak, #eef4ff);\n color: var(--skai-kb-primary, #2f6bff);\n font-weight: 500;\n}\n\n.skai-kb-editor-dirty {\n font-size: 11px;\n color: var(--skai-kb-warn, #d97706);\n}\n\n/* 双栏分屏:编辑区 | 预览区(仅预览/仅编辑时单栏占满) */\n.skai-kb-editor-panes {\n display: flex;\n gap: 8px;\n flex: 1;\n min-height: 0;\n}\n\n.skai-kb-editor-input {\n flex: 1;\n min-width: 0;\n min-height: 300px;\n resize: vertical;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 10px;\n padding: 10px 12px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--skai-kb-text, #374151);\n background: var(--skai-kb-bg-card, #fff);\n}\n\n.skai-kb-editor-input:focus {\n outline: none;\n border-color: var(--skai-kb-primary, #2f6bff);\n}\n\n/* 仅编辑模式:编辑框撑满父容器高度(取消最小高度下限与手动拉伸) */\n.skai-kb-editor.edit .skai-kb-editor-input {\n min-height: 0;\n resize: none;\n}\n\n.skai-kb-editor-preview {\n flex: 1;\n min-width: 0;\n min-height: 300px;\n border: 1px solid var(--skai-kb-border, #e5e7eb);\n border-radius: 10px;\n padding: 10px 12px;\n overflow-y: auto;\n background: var(--skai-kb-bg-soft, #fafafa);\n}\n\n/* 窄容器:编辑器分屏改为上下堆叠 */\n@container skai-kb (max-width: 560px) {\n .skai-kb-doc-layout {\n flex-direction: column;\n }\n\n .skai-kb-folder-tree {\n width: auto;\n position: static;\n }\n\n .skai-kb-folder-body {\n flex-direction: row;\n flex-wrap: wrap;\n max-height: none;\n }\n\n .skai-kb-editor-panes.split {\n flex-direction: column;\n }\n\n .skai-kb-editor-panes.split .skai-kb-editor-input,\n .skai-kb-editor-panes.split .skai-kb-editor-preview {\n min-height: 180px;\n }\n}\n\n/* ---------- 对话消息操作区「存入知识库」(conversation.chat.assistant-actions) ----------\n 按钮渲染于宿主 AI 消息操作行(官方复制/点赞同款 28px 图标钮),颜色直接取\n DSH 官方令牌(--dsw-alias-*,全局可达,随宿主浅/深色主题切换);\n --skai-kb-* 别名仅作 standalone 兜底 */\n.skai-kb-chat-save-btn {\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px;\n color: var(--dsw-alias-label-tertiary, var(--skai-kb-text-caption, #9ca1ab));\n background: none;\n border: none;\n border-radius: 28px;\n cursor: pointer;\n transition: color 0.15s, background 0.15s;\n}\n\n.skai-kb-chat-save-btn:hover {\n color: var(--dsw-alias-label-secondary, var(--skai-kb-text-sub, #8a8f99));\n background: var(--dsw-alias-interactive-bg-hover, var(--skai-kb-bg-hover, rgba(0, 0, 0, 0.04)));\n}\n\n.skai-kb-chat-save-preview {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.skai-kb-chat-save-preview-label {\n font-size: 12px;\n color: var(--skai-kb-text-caption);\n}\n\n/* 保存方式切换(新建文档 / 覆盖已有文档):与弹窗按钮风格一致 */\n.skai-kb-chat-save-modes {\n display: flex;\n gap: 8px;\n}\n.skai-kb-chat-save-hint {\n display: block;\n margin-top: 4px;\n font-size: 12px;\n color: var(--skai-kb-text-caption);\n}\n\n.skai-kb-chat-save-preview pre {\n max-height: 140px;\n overflow: auto;\n margin: 0;\n padding: 8px 10px;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-wrap;\n word-break: break-word;\n background: var(--skai-kb-bg-soft);\n border-radius: 6px;\n}\n\n/* ---------- 模型服务帮助弹窗(设置页「模型服务」区块「配置说明」入口) ---------- */\n.skai-kb-help-section {\n margin-bottom: 14px;\n}\n.skai-kb-help-section:last-child {\n margin-bottom: 0;\n}\n.skai-kb-help-title {\n margin: 0 0 6px;\n font-size: 13px;\n font-weight: 600;\n}\n.skai-kb-help-list {\n margin: 0;\n padding-left: 18px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.skai-kb-help-list li {\n font-size: 12.5px;\n line-height: 1.7;\n}\n.skai-kb-help-text {\n margin: 0;\n font-size: 12.5px;\n line-height: 1.7;\n}\n.skai-kb-help-link {\n color: var(--skai-kb-primary);\n text-decoration: none;\n word-break: break-all;\n}\n.skai-kb-help-link:hover {\n text-decoration: underline;\n}\n.skai-kb-help-code {\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--skai-kb-bg-soft);\n font-size: 12px;\n word-break: break-all;\n}\n\n";
7228
8527
  //#endregion
7229
8528
  //#region src/index.js
7230
8529
  /**
@@ -7784,6 +9083,8 @@ window.__ModuleLoader__.load({
7784
9083
  const [docs, setDocs] = (0, react.useState)([]);
7785
9084
  const [docsLoaded, setDocsLoaded] = (0, react.useState)(false);
7786
9085
  const [docId, setDocId] = (0, react.useState)(0);
9086
+ const [confirmForce, setConfirmForce] = (0, react.useState)(false);
9087
+ const [forcing, setForcing] = (0, react.useState)(false);
7787
9088
  const chat = typeof useChat === "function" ? useChat((s) => s) : null;
7788
9089
  const chatText = (0, react.useMemo)(() => extractTextFromChat(chat, messageId), [chat, messageId]);
7789
9090
  const kbDocRef = (0, react.useMemo)(() => extractKbDocRef(chat), [chat]);
@@ -7906,172 +9207,195 @@ window.__ModuleLoader__.load({
7906
9207
  toast(`已覆盖文档「${target?.name || docId}」,重新解析与向量化中`, "success", 4e3);
7907
9208
  setOpen(false);
7908
9209
  } catch (e) {
7909
- if ((e?.code === 409 || /冲突/.test(e?.msg || e?.message || "")) && typeof window !== "undefined" && window.confirm("该文档在此期间已被修改,是否强制覆盖?")) try {
7910
- await updateDocContent(kbId, docId, {
7911
- content,
7912
- baseUpdatedAt: target?.update_time || 0,
7913
- force: true
7914
- });
7915
- toast(`已强制覆盖文档「${target?.name || docId}」,重新解析与向量化中`, "success", 4e3);
7916
- setOpen(false);
7917
- return;
7918
- } catch (e2) {
7919
- setError(e2 && (e2.msg || e2.message) || "覆盖失败,请稍后重试");
9210
+ if (e?.code === 409 || /冲突/.test(e?.msg || e?.message || "")) {
9211
+ setConfirmForce(true);
7920
9212
  return;
7921
9213
  }
7922
9214
  setError(e && (e.msg || e.message) || "覆盖失败,请稍后重试");
7923
9215
  }
7924
9216
  }
7925
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7926
- ref: btnRef,
7927
- type: "button",
7928
- className: "skai-kb-chat-save-btn",
7929
- title: BTN_LABEL$1,
7930
- "aria-label": BTN_LABEL$1,
7931
- onClick: openModal,
7932
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BookPlusGlyph, { size: 15 })
7933
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
7934
- title: "存入知识库",
7935
- width: 480,
7936
- onClose: () => {
7937
- if (!busy) setOpen(false);
7938
- },
7939
- footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7940
- type: "button",
7941
- className: "skai-kb-btn",
7942
- onClick: () => setOpen(false),
7943
- disabled: busy,
7944
- children: "取消"
7945
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9217
+ /** 冲突确认弹窗确认后强制覆盖(force=true 跳过乐观锁) */
9218
+ async function doForceOverwrite() {
9219
+ const target = docs.find((d) => Number(d.id) === Number(docId));
9220
+ setForcing(true);
9221
+ try {
9222
+ await updateDocContent(kbId, docId, {
9223
+ content,
9224
+ baseUpdatedAt: target?.update_time || 0,
9225
+ force: true
9226
+ });
9227
+ toast(`已强制覆盖文档「${target?.name || docId}」,重新解析与向量化中`, "success", 4e3);
9228
+ setConfirmForce(false);
9229
+ setOpen(false);
9230
+ } catch (e2) {
9231
+ setConfirmForce(false);
9232
+ setError(e2 && (e2.msg || e2.message) || "覆盖失败,请稍后重试");
9233
+ } finally {
9234
+ setForcing(false);
9235
+ }
9236
+ }
9237
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9239
+ ref: btnRef,
7946
9240
  type: "button",
7947
- className: "skai-kb-btn skai-kb-btn-primary",
7948
- onClick: submit,
7949
- disabled: busy,
7950
- children: busy ? "提交中…" : mode === "overwrite" ? "覆盖" : "保存"
7951
- })] }),
7952
- children: [
7953
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7954
- className: "skai-kb-field",
7955
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7956
- className: "skai-kb-field-label",
7957
- children: "保存方式"
7958
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7959
- className: "skai-kb-chat-save-modes",
7960
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7961
- type: "button",
7962
- className: `skai-kb-btn${mode === "create" ? " skai-kb-btn-primary" : ""}`,
7963
- onClick: () => setMode("create"),
7964
- disabled: busy,
7965
- children: "新建文档"
7966
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7967
- type: "button",
7968
- className: `skai-kb-btn${mode === "overwrite" ? " skai-kb-btn-primary" : ""}`,
7969
- onClick: () => {
7970
- setMode("overwrite");
7971
- setError("");
7972
- },
7973
- disabled: busy,
7974
- children: "覆盖已有文档"
9241
+ className: "skai-kb-chat-save-btn",
9242
+ title: BTN_LABEL$1,
9243
+ "aria-label": BTN_LABEL$1,
9244
+ onClick: openModal,
9245
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BookPlusGlyph, { size: 15 })
9246
+ }),
9247
+ open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
9248
+ title: "存入知识库",
9249
+ width: 480,
9250
+ escClose: !confirmForce,
9251
+ onClose: () => {
9252
+ if (!busy && !confirmForce) setOpen(false);
9253
+ },
9254
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9255
+ type: "button",
9256
+ className: "skai-kb-btn",
9257
+ onClick: () => setOpen(false),
9258
+ disabled: busy,
9259
+ children: "取消"
9260
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9261
+ type: "button",
9262
+ className: "skai-kb-btn skai-kb-btn-primary",
9263
+ onClick: submit,
9264
+ disabled: busy,
9265
+ children: busy ? "提交中…" : mode === "overwrite" ? "覆盖" : "保存"
9266
+ })] }),
9267
+ children: [
9268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9269
+ className: "skai-kb-field",
9270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9271
+ className: "skai-kb-field-label",
9272
+ children: "保存方式"
9273
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9274
+ className: "skai-kb-chat-save-modes",
9275
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9276
+ type: "button",
9277
+ className: `skai-kb-btn${mode === "create" ? " skai-kb-btn-primary" : ""}`,
9278
+ onClick: () => setMode("create"),
9279
+ disabled: busy,
9280
+ children: "新建文档"
9281
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9282
+ type: "button",
9283
+ className: `skai-kb-btn${mode === "overwrite" ? " skai-kb-btn-primary" : ""}`,
9284
+ onClick: () => {
9285
+ setMode("overwrite");
9286
+ setError("");
9287
+ },
9288
+ disabled: busy,
9289
+ children: "覆盖已有文档"
9290
+ })]
7975
9291
  })]
7976
- })]
7977
- }),
7978
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7979
- className: "skai-kb-field",
7980
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7981
- className: "skai-kb-field-label",
7982
- children: "目标知识库 *"
7983
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
7984
- className: "skai-kb-select",
7985
- value: kbId,
7986
- onChange: (e) => setKbId(Number(e.target.value) || 0),
7987
- disabled: busy,
7988
- children: [
7989
- !kbLoaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7990
- value: 0,
7991
- children: "加载中…"
7992
- }) : null,
7993
- kbLoaded && kbs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7994
- value: 0,
7995
- children: "(无可用知识库,请先创建)"
7996
- }) : null,
7997
- kbs.map((kb) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7998
- value: kb.id,
7999
- children: kb.name
8000
- }, kb.id))
8001
- ]
8002
- })]
8003
- }),
8004
- mode === "overwrite" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8005
- className: "skai-kb-field",
8006
- children: [
8007
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9292
+ }),
9293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9294
+ className: "skai-kb-field",
9295
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8008
9296
  className: "skai-kb-field-label",
8009
- children: "选择要覆盖的文档 *"
8010
- }),
8011
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
9297
+ children: "目标知识库 *"
9298
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
8012
9299
  className: "skai-kb-select",
8013
- value: docId,
8014
- onChange: (e) => setDocId(Number(e.target.value) || 0),
8015
- disabled: busy || !docsLoaded || docs.length === 0,
9300
+ value: kbId,
9301
+ onChange: (e) => setKbId(Number(e.target.value) || 0),
9302
+ disabled: busy,
8016
9303
  children: [
8017
- !docsLoaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
9304
+ !kbLoaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
8018
9305
  value: 0,
8019
9306
  children: "加载中…"
8020
9307
  }) : null,
8021
- docsLoaded && docs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
9308
+ kbLoaded && kbs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
8022
9309
  value: 0,
8023
- children: "(该知识库暂无文档)"
9310
+ children: "(无可用知识库,请先创建)"
8024
9311
  }) : null,
8025
- docs.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
8026
- value: d.id,
8027
- children: d.name
8028
- }, d.id))
9312
+ kbs.map((kb) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
9313
+ value: kb.id,
9314
+ children: kb.name
9315
+ }, kb.id))
8029
9316
  ]
8030
- }),
8031
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8032
- className: "skai-kb-chat-save-hint",
8033
- children: "覆盖将替换文档全部内容,原内容自动保留为历史版本"
8034
- })
8035
- ]
8036
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8037
- className: "skai-kb-field",
8038
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8039
- className: "skai-kb-field-label",
8040
- children: "文档名称 *"
8041
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8042
- className: "skai-kb-input",
8043
- value: docName,
8044
- maxLength: 120,
8045
- onChange: (e) => setDocName(e.target.value),
8046
- placeholder: "如:接口设计要点-20260101-1530.md",
8047
- disabled: busy
8048
- })]
8049
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8050
- className: "skai-kb-field",
8051
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8052
- className: "skai-kb-field-label",
8053
- children: "业务标签(逗号分隔,可选)"
8054
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8055
- className: "skai-kb-input",
8056
- value: tags,
8057
- onChange: (e) => setTags(e.target.value),
8058
- placeholder: "如:对话纪要, 方案",
8059
- disabled: busy
8060
- })]
8061
- })] }),
8062
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8063
- className: "skai-kb-chat-save-preview",
8064
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8065
- className: "skai-kb-chat-save-preview-label",
8066
- children: "内容预览(前 200 字)"
8067
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("pre", { children: [content.slice(0, 200), content.length > 200 ? "…" : ""] })]
8068
- }),
8069
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8070
- className: "skai-kb-field-error",
8071
- children: error
8072
- }) : null
8073
- ]
8074
- }) : null] });
9317
+ })]
9318
+ }),
9319
+ mode === "overwrite" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9320
+ className: "skai-kb-field",
9321
+ children: [
9322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9323
+ className: "skai-kb-field-label",
9324
+ children: "选择要覆盖的文档 *"
9325
+ }),
9326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
9327
+ className: "skai-kb-select",
9328
+ value: docId,
9329
+ onChange: (e) => setDocId(Number(e.target.value) || 0),
9330
+ disabled: busy || !docsLoaded || docs.length === 0,
9331
+ children: [
9332
+ !docsLoaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
9333
+ value: 0,
9334
+ children: "加载中…"
9335
+ }) : null,
9336
+ docsLoaded && docs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
9337
+ value: 0,
9338
+ children: "(该知识库暂无文档)"
9339
+ }) : null,
9340
+ docs.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
9341
+ value: d.id,
9342
+ children: d.name
9343
+ }, d.id))
9344
+ ]
9345
+ }),
9346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9347
+ className: "skai-kb-chat-save-hint",
9348
+ children: "覆盖将替换文档全部内容,原内容自动保留为历史版本"
9349
+ })
9350
+ ]
9351
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9352
+ className: "skai-kb-field",
9353
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9354
+ className: "skai-kb-field-label",
9355
+ children: "文档名称 *"
9356
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
9357
+ className: "skai-kb-input",
9358
+ value: docName,
9359
+ maxLength: 120,
9360
+ onChange: (e) => setDocName(e.target.value),
9361
+ placeholder: "如:接口设计要点-20260101-1530.md",
9362
+ disabled: busy
9363
+ })]
9364
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9365
+ className: "skai-kb-field",
9366
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9367
+ className: "skai-kb-field-label",
9368
+ children: "业务标签(逗号分隔,可选)"
9369
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
9370
+ className: "skai-kb-input",
9371
+ value: tags,
9372
+ onChange: (e) => setTags(e.target.value),
9373
+ placeholder: "如:对话纪要, 方案",
9374
+ disabled: busy
9375
+ })]
9376
+ })] }),
9377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9378
+ className: "skai-kb-chat-save-preview",
9379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9380
+ className: "skai-kb-chat-save-preview-label",
9381
+ children: "内容预览(前 200 字)"
9382
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("pre", { children: [content.slice(0, 200), content.length > 200 ? "…" : ""] })]
9383
+ }),
9384
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9385
+ className: "skai-kb-field-error",
9386
+ children: error
9387
+ }) : null
9388
+ ]
9389
+ }) : null,
9390
+ confirmForce ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
9391
+ title: "文档已被修改",
9392
+ message: "该文档在此期间已被其他人修改,是否强制覆盖?覆盖将替换文档全部内容,原内容自动保留为历史版本。",
9393
+ confirmText: "强制覆盖",
9394
+ busy: forcing,
9395
+ onConfirm: doForceOverwrite,
9396
+ onClose: () => setConfirmForce(false)
9397
+ }) : null
9398
+ ] });
8075
9399
  }
8076
9400
  //#endregion
8077
9401
  //#region src/components/chat/ReferenceKbButton.jsx