Compare commits
4 Commits
0cd5b07124
...
e03a1ee7eb
| Author | SHA1 | Date |
|---|---|---|
|
|
e03a1ee7eb | |
|
|
0c74a44224 | |
|
|
0ceb4c5bd9 | |
|
|
e0aa8130af |
|
|
@ -5,6 +5,12 @@
|
|||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||
|
||||
## 0.59.4 — 2026-07-24
|
||||
|
||||
- 对话里直接说「把这份资料放进我的知识库」现在可靠了:即使你还没建过任何库,助手也会帮你建库并入库(此前会被误存成「记忆」);同时更明确了分工——成篇资料进知识库,关于你本人的偏好等短事实才记进记忆。
|
||||
- 知识库支持在线查看原件:已入库条目、单篇正文页、待入库文件都新增「查看」入口,点开即弹预览窗(PDF / Word / Excel / 图片直接看,PPT 自动转 PDF 预览),不用先下载。
|
||||
- 知识库已入库列表更清爽:每条精简为标题 + 一行摘要,完整摘要和关键词悬停即可查看。
|
||||
|
||||
## 0.59.3 — 2026-07-23
|
||||
|
||||
- 知识库支持下载文件:已入库的每篇文档可下载原始文件(⬇ 按钮),打开单篇正文后还可下载整理好的 Markdown 文字版;等待入库的文件也能随时取回。
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ Session = 消息列表,ORM 直写 PG `messages`(append-only,jsonb 存 LiteLLM
|
|||
|
||||
- **做成机制而非 skill**(判据:有独立于会话的持久状态需用户管理 → 机制):落盘 `user_root/.kb/<库名>/`(INDEX.md + docs/ 转换后 md + sources/ 原件)。**"已入库"判据 = INDEX.md 有条目**,sources 有而 INDEX 无 = 待入库 → 入库幂等、崩溃可恢复、零 migration。dotfile 命名同 `.memory` 双向防呆,GET /v1/files 天然隐藏。
|
||||
- **入库管线**(`core/kb_ingest.py`,上传即触发 + 手动兜底):markitdown Python API 转 md → 扫描件 PDF(文本近零)走方舟文档理解 OCR 兜底(§8.13 同通道)→ deepseek flash 单次 chat 写 标题/摘要/关键词(失败降级文件名+正文开头,不阻塞)→ 追加 INDEX 行。编排照定时执行器:create_task + to_thread + per-(user,库) 内存锁去重;进度存内存供前端轮询,崩了靠 FS 判据续跑。
|
||||
- **agent 侧零新工具**:`kb_block`(照 memory_block)把 INDEX 全文 + 契约(主动查阅无需点名 / INDEX 行格式 / 答题标来源)注 prompt,**用户有库才注入**;fs 工具在 user_root 内可读写、docker 沙箱整 user_root bind → `.kb` 天然可达。INDEX 行格式是对话内手动入库与后台产出的同一契约。
|
||||
- **agent 侧零新工具**:`kb_block`(照 memory_block)把 INDEX 全文 + 契约(主动查阅无需点名 / INDEX 行格式 / 答题标来源)注 prompt;**零库时注极简冷启动契约**(建库步骤 + INDEX 行格式 + "成篇资料进 KB / 短事实进记忆"分工,~百 token)——原"有库才注入"省 token,但零注入让模型不知道 KB 机制存在,用户说"放进知识库"被就近写进 `.memory/`(2026-07-24 真实事故),与 memory 空契约常驻是同一课:教会第一次,建库落盘后下轮 build_agent 自然切全量。fs 工具在 user_root 内可读写、docker 沙箱整 user_root bind → `.kb` 天然可达。INDEX 行格式是对话内手动入库与后台产出的同一契约。
|
||||
- **API 薄壳**(`/v1/kb*` 8 端点):列/建/删库、详情(带入库进度)、上传即入库、手动 ingest、看/删单篇。**不设 HTTP 检索端点**——检索是 agent 的事。前端两栏 modal(kb.js)管上传/删除,查询全走对话。
|
||||
- **记账**:`usage_events` kind="kb_ingest"(OCR 那笔走 kind="vision"),无 task 上下文 → 0022 放宽 task_id 可 NULL,溯源靠 units JSONB `{"kb", "source"}`。
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-07
|
||||
|
||||
- **07-24 / 0.59.4 / kb 冷启动契约(修「放进知识库」误存记忆)+ 查看原件 + 条目精简**:①真实用户事故——说「放进我的知识库」被 agent 写进 `.memory/`:根因 `kb_block` 零库返回空串,模型不知道 KB 机制存在(memory 空契约常驻恰是为解冷启动,kb 当时没照抄这课)。修 = 零库注 ~百 token 冷启动契约(建库步骤 / INDEX 行格式 / 「成篇资料进 KB、短事实进记忆」分工;仅用户明确要求才建库),建库落盘后下轮 build_agent 自然切全量注入;skill 方案再否(召回同为概率召回还多一层间接,且违背「机制而非 skill」定稿)。DESIGN §3.8 取舍随 commit 更新,`tests/test_kb_block.py` 4 用例锁两分支。②前端「查看原件」三入口(已入库条目行 👁 / 单篇页头 / 待入库列表),复用 `openFilePreview` 走 `.kb/<库>/sources/` 相对路径零后端;关键层级修正:file-preview-modal z 90→114 盖过 kb/skills/memory 等 112 层 modal(仍低于 app-dialog 130,删除确认在预览上),fp-open chat-form 95→115、mini 96→116,main.js Esc 关栈顺序同步先关预览再关 modal。「像文件目录浏览 .kb」提议对齐后定维持现状:.kb 继续对文件面板隐藏,守住删单篇「doc+原件+INDEX 行」三件联动判据。③已入库条目压两行:标题 + 单行省略摘要(原 `.sk-desc` 省略规则只覆盖左栏 `.sk-item`,右栏整段换行铺开),关键词挪进悬停 title 不丢信息。
|
||||
- **07-23 / 0.59.3 / 知识库下载入口(原件 + 解析 Markdown,纯前端)**:kb 面板此前只能在线看不能取回。关键发现:`.kb/` 就在 user_root 下且 `/v1/files/download` 的 safe_join 只挡越界不挡 dotfile → 零后端改动,`kb.js` 复用 `media.js::downloadFile`(Bearer + blob)拼 `.kb/<库>/{sources,docs}/…` 路径即可。入口三处:已入库条目行「⬇ 下载原件」(agent 手写无原件的条目不显示)、单篇查看页头「下载原件 / 下载 Markdown」(doc→source 映射渲染详情时记 `_docSources`)、待入库列表逐项「下载」。弃选方案:新增 `/v1/kb/*/sources` 专属下载端点(与 files 下载全量重复,公测期白养一个对外契约)。改动仅 kb.js + dev.html 三行 CSS。
|
||||
- **07-23 / 0.59.2 / 移动端对话附件入口(📎 拍照 / 照片库 / 选文件)**:chat 输入区此前仅粘贴 / 拖拽两条附件路(移动端双双不可用,手机没有任何传文件入口)。加 📎 按钮:触屏(`pointer: coarse`)弹 showMenu 三项菜单——拍照(`capture="environment"` 直起后置相机)/ 照片库(`accept="image/*,video/*"`)/ 选取文件(无 accept);桌面端点击直开系统文件选择器(拖拽 / 粘贴照旧)。三个隐藏 input 的 change 统一走既有 `uploadAttachFiles()`(chip 托盘 / 发送注入行零改动);change 后清 value 允许连选同一文件;微信 / 企业微信只读镜像随 `applyChannelComposerLock` 禁用按钮 + 点击侧 `_composerLocked()` 双保险。纯前端(dev.html / chat.js),无后端改动。裸 file input 方案(靠 iOS 原生三选单)因 Android 行为不确定被弃,三项菜单跨平台确定。
|
||||
- **07-23 / 0.59.1 / 架构审查落地:15 commit 内部重构大扫除(对外行为零变化)**:全库审查(web/核心循环/工具渲染/数据层四路并行探查)后按优先级逐项执行。①**web 拆分**——app.py 3972→197 行:73 路由迁 11 个 `web/routers/*`(register 范式同 admin.py),lifespan 后台协程按域析出(`background`/`scheduler_runner`/`wechat_runner`),schemas/model_gate/runs/userfiles/common 各归位;②**计费收口**——units JSONB 读侧唯一出口 `core/storage/usage_report.py`(web 层清零 cast),写侧拆 `telemetry.py`(四类 cost=0 留痕与真计费分家,kind 常量成 loop↔toolfail 契约单一事实源);③**loop 拆分**——传输健壮性层(畸形/吐空检测、留痕、非流式降级重试)析出 `core/llm_transport.py`(loop 1158→812),`_execute_tool_call` 148 行拆 4 个正交方法,测试打桩缝(`_collect_stream_once`/`_nonstream_once`)原样保留;④**工具注册声明式化**——`core/tool_registry.py` (组名,gate,factory) 表收敛四种 env-gate 写法(§7.5 #7 获代码强制),32 工具挂载逐名比对不变;⑤**媒体工具去重**——五工具同构(配额闸/落盘命名/记账兜底/超时重试/递归找 URL)收 `tools/media_common.py`,净减 122 行,banner/轮询等刻意差异不动;⑥**渲染统一**——块收集器(fence 闭合/表格/blockquote/段落并合)与 brief inline 切分下沉 `rendering/common`,**golden 基线**(四 profile 渲富 fixture、document.xml 逐字节对账,`tests/golden/`)零 diff 验收,分派状态机(in_refs 等)留 profile;⑦**测试 286→351**——scheduler 服务层、usage 口径、media 原语、wecom 加解密(自造密文覆盖失败路径)、路由两套(无 DB 面:11 router 鉴权门 19 端点 + kb/skills/memory/files FS;DB 面:tasks CRUD 全链 + files 顶层目录 DB-aware 四分支 + upload);测试库基建 = docker PG@5433 + `ZCBOT_TEST_DB_URL` 显式门控(RUN.md 一键命令)。**修 2 个真 bug**:restore 恢复真软删过的任务必 500(server-side onupdate 列 flush 后 expired → DetachedInstanceError,幂等路径掩盖多时,路由测试抓出);DB 测试曾沿 .env 连到生产隧道(127.0.0.1:6012)——插入的到点 job 被生产 green 调度守护真跑,门控收紧为只认 ZCBOT_TEST_DB_URL + connect_timeout=5 兜挂库,产物已全量清理。附带:scripts 50→29(已结案诊断/backfill 归档 `scripts/archive/`)、`tools/output.py`(输出处理自 base.py 析出)、export_docx 复用 rendering.common 字体助手(rendering 保持无 core 依赖的沙箱可用性,修正审查"迁 rendering"建议)。
|
||||
|
|
@ -252,7 +253,7 @@ web/auth.py ~190 ← 邮箱密码 + platform_key → JWT
|
|||
web/broker.py / sinks.py / pptx_render.py / admin.py
|
||||
web/static/dev.html + js/*.js ← dev SPA 零构建 ES module(main.js 入口;skills.js=技能 modal;kb.js=知识库 modal)
|
||||
web/static/vendor/ ~1 MB ← jszip / docx-preview / xlsx
|
||||
tests/ 351 项 ← golden/rendering 基线 + 路由两套(nodb/db)+ DB 套件(ZCBOT_TEST_DB_URL 门控)
|
||||
tests/ 355 项 ← golden/rendering 基线 + 路由两套(nodb/db)+ DB 套件(ZCBOT_TEST_DB_URL 门控)
|
||||
─────────────────────────────────
|
||||
Python 合计 ~5.5 万行(core+web+tools+rendering+tests+scripts;+ dev SPA + vendor 1MB)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.59.3"
|
||||
__version__ = "0.59.4"
|
||||
|
|
|
|||
|
|
@ -334,7 +334,8 @@ def _build_system_prompt(
|
|||
user_root(workspace_dir, user_id) / ".memory"
|
||||
)
|
||||
prompt += memory_block(workspace_dir, user_id, mem_dir_display)
|
||||
# 个人知识库 .kb/:路径换算同 .memory(docker 给容器路径);用户没建库时注空串零成本。
|
||||
# 个人知识库 .kb/:路径换算同 .memory(docker 给容器路径);零库时注冷启动契约
|
||||
# (否则用户说「放进知识库」会被就近写进记忆 —— 模型不知道 KB 机制存在)。
|
||||
kb_dir_display = "/workspace/.kb" if is_docker else str(
|
||||
user_root(workspace_dir, user_id) / ".kb"
|
||||
)
|
||||
|
|
|
|||
25
core/kb.py
25
core/kb.py
|
|
@ -239,6 +239,25 @@ def save_source(workspace_dir: Path, user_id: UUID, name: str, filename: str, da
|
|||
|
||||
# ── prompt 注入(照 memory_block 范式) ────────────────────────────────
|
||||
|
||||
# 零库时的冷启动契约:曾因零库零注入,用户说「放进知识库」被 agent 就近写进
|
||||
# .memory/(2026-07-24)—— 模型根本不知道 KB 机制存在。与 memory 空契约常驻
|
||||
# 同一课:教会第一次即可,建库落盘后下轮 build_agent 自然切到全量注入。
|
||||
_KB_COLD_START = """\
|
||||
|
||||
|
||||
## 个人知识库 (user 级,跨 task 共享)
|
||||
|
||||
用户还没有任何知识库(`{base}/` 下无库)。当用户明确要求把资料**放进知识库 /
|
||||
长期保存备查**时,不要写进记忆 —— 直接用 fs 工具建库入库(无需专用工具):
|
||||
- **建库**:创建 `{base}/<库名>/docs/` 与 `{base}/<库名>/sources/` 两个目录,写
|
||||
`{base}/<库名>/INDEX.md`(首行 `# <库名>`)。库名用中文/字母/数字。
|
||||
- **入库**:原件放 `sources/`(对话里没有原件就跳过);正文转成 markdown 写
|
||||
`docs/<文件名>.md`;再往 INDEX.md 追加一行,**格式必须是**:
|
||||
`{fmt}`
|
||||
(摘要写准 —— 它是下次召回的依据)。
|
||||
- **分工**:成篇资料(文档/规范/报告)进知识库;关于用户本人的短事实才写记忆。
|
||||
仅用户明确表达"存知识库/长期保存"时才建库,临时讨论材料不要自动入库。"""
|
||||
|
||||
_KB_CONTRACT = """\
|
||||
用法规矩:
|
||||
- **主动查阅**:回答前先扫一眼下方各库条目,标题 / 摘要 / 关键词与用户问题相关就先
|
||||
|
|
@ -257,7 +276,7 @@ def kb_block(
|
|||
user_id: UUID,
|
||||
kb_dir_display: Optional[str] = None,
|
||||
) -> str:
|
||||
"""构造注入 system prompt 的知识库段;用户没有任何库时返回空串(零成本)。
|
||||
"""构造注入 system prompt 的知识库段;零库时注极简冷启动契约(~百 token)。
|
||||
|
||||
kb_dir_display: `.kb/` 在 agent 视角下的路径前缀(docker 传 `/workspace/.kb`,
|
||||
host 传 None ⇒ 宿主绝对路径)—— 与 memory_block 的 mem_dir_display 同款约定。
|
||||
|
|
@ -265,10 +284,10 @@ def kb_block(
|
|||
description 逻辑),几十篇的个人库体量注得起;正文仍按需 read。
|
||||
"""
|
||||
kbs = list_kbs(workspace_dir, user_id)
|
||||
if not kbs:
|
||||
return ""
|
||||
root = kb_root(workspace_dir, user_id)
|
||||
base = (kb_dir_display if kb_dir_display is not None else str(root)).rstrip("/")
|
||||
if not kbs:
|
||||
return _KB_COLD_START.replace("{base}", base).replace("{fmt}", INDEX_LINE_FORMAT)
|
||||
|
||||
parts = ["\n\n## 个人知识库 (user 级,跨 task 共享)\n"]
|
||||
parts.append(_KB_CONTRACT.replace("{fmt}", INDEX_LINE_FORMAT))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
"""kb_block 注入契约:零库冷启动 vs 有库全量。
|
||||
|
||||
背景(2026-07-24):零库时 kb_block 返回空串,agent 不知道 KB 机制存在,用户说
|
||||
「放进我的知识库」被就近写进 .memory/。修复 = 零库注极简冷启动契约(建库步骤 +
|
||||
INDEX 行格式 + 知识库/记忆分工),与 memory 空契约常驻同一课。本测试锁住两个分支。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from core.kb import ( # noqa: E402
|
||||
INDEX_LINE_FORMAT,
|
||||
create_kb,
|
||||
format_index_line,
|
||||
kb_block,
|
||||
)
|
||||
|
||||
_UID = UUID("6b14d2ab-7a6e-4d0b-8562-ea62a801e32c")
|
||||
|
||||
|
||||
class TestKbBlock(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ws = Path(tempfile.mkdtemp()) / "workspace"
|
||||
|
||||
def test_cold_start_injects_minimal_contract(self):
|
||||
block = kb_block(self.ws, _UID, "/workspace/.kb")
|
||||
# 机制存在 + 建库/入库指引 + 行格式契约都在
|
||||
self.assertIn("个人知识库", block)
|
||||
self.assertIn("还没有任何知识库", block)
|
||||
self.assertIn("/workspace/.kb/<库名>/docs/", block)
|
||||
self.assertIn(INDEX_LINE_FORMAT, block)
|
||||
# 与记忆的分工写明(误写 .memory 正是要防的失败模式)
|
||||
self.assertIn("短事实才写记忆", block)
|
||||
# 冷启动块不含任何库清单
|
||||
self.assertNotIn("### 库「", block)
|
||||
|
||||
def test_cold_start_host_path_display(self):
|
||||
block = kb_block(self.ws, _UID, None)
|
||||
root = self.ws / "users" / str(_UID) / ".kb"
|
||||
self.assertIn(str(root), block)
|
||||
|
||||
def test_with_kb_injects_full_index(self):
|
||||
d = create_kb(self.ws, _UID, "标准库")
|
||||
assert d is not None
|
||||
line = format_index_line(
|
||||
title="水泥胶砂强度检验方法",
|
||||
doc="docs/gbt17671.md",
|
||||
source="sources/gbt17671.pdf",
|
||||
summary="ISO 法测定水泥胶砂抗压抗折强度。",
|
||||
keywords="水泥,强度,GB/T 17671",
|
||||
)
|
||||
(d / "INDEX.md").write_text(f"# 标准库\n\n{line}\n", encoding="utf-8")
|
||||
block = kb_block(self.ws, _UID, "/workspace/.kb")
|
||||
# 全量分支:库清单 + 条目 + 完整契约(对话内入库段)
|
||||
self.assertIn("### 库「标准库」", block)
|
||||
self.assertIn("水泥胶砂强度检验方法", block)
|
||||
self.assertIn("对话内入库", block)
|
||||
# 冷启动文案不应出现
|
||||
self.assertNotIn("还没有任何知识库", block)
|
||||
|
||||
def test_empty_kb_counts_as_having_kb(self):
|
||||
# 建了库但零文档:走全量分支(注库名 + 空库提示),不再是冷启动
|
||||
create_kb(self.ws, _UID, "新库")
|
||||
block = kb_block(self.ws, _UID, "/workspace/.kb")
|
||||
self.assertIn("### 库「新库」", block)
|
||||
self.assertIn("空库", block)
|
||||
self.assertNotIn("还没有任何知识库", block)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -508,6 +508,10 @@
|
|||
.kb-pending { padding: 3px 0; font-size: 12px; color: var(--muted); }
|
||||
.kb-entry { display: flex; align-items: flex-start; gap: 6px; padding: 6px 0; border-bottom: 1px dashed var(--border); }
|
||||
.kb-entry-main { flex: 1; min-width: 0; cursor: pointer; }
|
||||
/* 标题/摘要各压一行省略(sk-item 的省略规则只覆盖左栏,这里单列);全文在悬停 title */
|
||||
.kb-entry-main .sk-name, .kb-entry-main .sk-desc {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.kb-entry-main:hover .sk-name { text-decoration: underline; }
|
||||
.kb-entry .kb-doc-del, .kb-entry .kb-src-dl { flex-shrink: 0; opacity: .55; }
|
||||
.kb-entry:hover .kb-doc-del, .kb-entry:hover .kb-src-dl { opacity: 1; }
|
||||
|
|
@ -1195,13 +1199,15 @@
|
|||
padding-bottom 让出 chat-form 高度(--preview-bottom-inset 由 JS 按需写),
|
||||
使居中卡片停在输入区之上、不遮挡;输入区本身由 body.fp-open 抬到遮罩之上继续可用。 */
|
||||
#file-preview-modal {
|
||||
background: rgba(0,0,0,0.45); z-index: 90;
|
||||
background: rgba(0,0,0,0.45); z-index: 114;
|
||||
padding-bottom: var(--preview-bottom-inset, 0);
|
||||
}
|
||||
/* z=114:预览要能从 kb / skills / memory 等 112 层 modal 内打开并盖在其上
|
||||
(kb「查看原件」),同时低于 app-dialog(130,删除确认仍在预览之上)。 */
|
||||
/* 预览开启时:仅把输入区上浮成遮罩之上的"悬浮活动条"(实底 + 上向阴影),
|
||||
其余底部区域(左栏入口 / 右栏 / 状态)随整屏遮罩统一压暗,不再是一条通亮的带。 */
|
||||
body.fp-open #chat-form {
|
||||
position: relative; z-index: 95;
|
||||
position: relative; z-index: 115;
|
||||
box-shadow: 0 -10px 28px rgba(0,0,0,.22);
|
||||
}
|
||||
#file-preview-modal .card {
|
||||
|
|
@ -1263,7 +1269,7 @@
|
|||
#file-preview-modal .body .xlsx-sheet td, #file-preview-modal .body .xlsx-sheet th {
|
||||
border: 1px solid var(--border); padding: 4px 8px; white-space: nowrap;
|
||||
}
|
||||
#mini-preview-modal { background: rgba(0,0,0,0.18); z-index: 96; align-items: flex-start; justify-content: flex-end; padding: 56px 18px 0 0; }
|
||||
#mini-preview-modal { background: rgba(0,0,0,0.18); z-index: 116; align-items: flex-start; justify-content: flex-end; padding: 56px 18px 0 0; }
|
||||
#mini-preview-modal .card {
|
||||
width: min(520px, 92vw); height: min(420px, 72vh);
|
||||
display: flex; flex-direction: column; position: relative;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { escapeHtml } from "./format.js";
|
|||
import { renderMd, highlightIn } from "./markdown.js";
|
||||
import { dialogConfirm, dialogPrompt, message } from "./dialog.js";
|
||||
import { downloadFile } from "./media.js";
|
||||
import { openFilePreview } from "./preview.js";
|
||||
|
||||
const PLACEHOLDER = '<div class="sk-empty">← 选一个库查看,或「+ 新建库」</div>';
|
||||
const POLL_MS = 3000;
|
||||
|
|
@ -121,6 +122,7 @@ function renderDetail(d) {
|
|||
if (pending.length) {
|
||||
html += `<div class="sk-group-title" style="margin-top:10px;">待入库 (${pending.length})</div>`;
|
||||
html += pending.map((n) => `<div class="kb-pending">${escapeHtml(n)}
|
||||
<button class="small kb-src-view" data-src="${escapeHtml(n)}" title="查看原件">查看</button>
|
||||
<button class="small kb-src-dl" data-src="${escapeHtml(n)}" title="下载原件">下载</button></div>`).join("");
|
||||
if (!ing.running) {
|
||||
html += `<div style="padding:6px 0;"><button id="kb-reingest" class="small">重试入库</button></div>`;
|
||||
|
|
@ -133,12 +135,15 @@ function renderDetail(d) {
|
|||
const doc = e.doc.replace(/^docs\//, "");
|
||||
const src = (e.source || "").replace(/^sources\//, "");
|
||||
if (src) _docSources[doc] = src;
|
||||
// 行内只留标题 + 单行摘要(CSS 省略);完整摘要/关键词进 title 悬停提示,不占行高
|
||||
const tip = e.summary + (e.keywords ? "\n关键词: " + e.keywords : "");
|
||||
return `<div class="kb-entry">
|
||||
<div class="kb-entry-main" data-doc="${escapeHtml(doc)}">
|
||||
<div class="kb-entry-main" data-doc="${escapeHtml(doc)}" title="${escapeHtml(tip)}">
|
||||
<div class="sk-name">${escapeHtml(e.title)}</div>
|
||||
<div class="sk-desc">${escapeHtml(e.summary)}${e.keywords ? " | " + escapeHtml(e.keywords) : ""}</div>
|
||||
<div class="sk-desc">${escapeHtml(e.summary)}</div>
|
||||
</div>
|
||||
${src ? `<button class="small kb-src-dl" data-src="${escapeHtml(src)}" title="下载原件">⬇</button>` : ""}
|
||||
${src ? `<button class="small kb-src-view" data-src="${escapeHtml(src)}" title="查看原件">👁</button>
|
||||
<button class="small kb-src-dl" data-src="${escapeHtml(src)}" title="下载原件">⬇</button>` : ""}
|
||||
<button class="small danger kb-doc-del" data-doc="${escapeHtml(doc)}" title="删除该篇(含原件)">✕</button>
|
||||
</div>`;
|
||||
}).join("")
|
||||
|
|
@ -162,7 +167,8 @@ async function showDoc(filename) {
|
|||
`<div class="sk-d-head"><button id="kb-back" class="small">← 返回</button>
|
||||
<span class="sk-d-name">${escapeHtml(filename)}</span>
|
||||
<span class="spacer"></span>
|
||||
${src ? `<button id="kb-doc-src-dl" class="small" data-src="${escapeHtml(src)}">下载原件</button>` : ""}
|
||||
${src ? `<button id="kb-doc-src-view" class="small" data-src="${escapeHtml(src)}">查看原件</button>
|
||||
<button id="kb-doc-src-dl" class="small" data-src="${escapeHtml(src)}">下载原件</button>` : ""}
|
||||
<button id="kb-doc-md-dl" class="small" data-doc="${escapeHtml(filename)}">下载 Markdown</button></div>` +
|
||||
`<div class="sk-detail-md">${renderMd(data.content)}</div>`;
|
||||
highlightIn(detail);
|
||||
|
|
@ -214,6 +220,10 @@ $("kb-detail").addEventListener("click", async (e) => {
|
|||
if (e.target.id === "kb-back") { showKb(_current, null); return; }
|
||||
if (e.target.id === "kb-doc-md-dl") { downloadFile(kbRel("docs/" + e.target.getAttribute("data-doc"))); return; }
|
||||
if (e.target.id === "kb-doc-src-dl") { downloadFile(kbRel("sources/" + e.target.getAttribute("data-src"))); return; }
|
||||
// 查看原件:复用主预览弹框(preview.js,z 114 盖在 kb modal 之上),路径同下载走 .kb 相对路径
|
||||
if (e.target.id === "kb-doc-src-view") { openFilePreview(kbRel("sources/" + e.target.getAttribute("data-src"))); return; }
|
||||
const srcView = e.target.closest(".kb-src-view");
|
||||
if (srcView) { openFilePreview(kbRel("sources/" + srcView.getAttribute("data-src"))); return; }
|
||||
const srcDl = e.target.closest(".kb-src-dl");
|
||||
if (srcDl) { downloadFile(kbRel("sources/" + srcDl.getAttribute("data-src"))); return; }
|
||||
if (e.target.id === "kb-reingest") {
|
||||
|
|
|
|||
|
|
@ -107,7 +107,10 @@ async function loadVersion() {
|
|||
// ───── Esc 关弹窗栈(跨模块协调:chpw/选入/文件预览/小预览)─────
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Escape") return;
|
||||
// 多模态共存:优先关靠前栈顶 — 小预览(z 96)→ 选入(z 95)→ 文件预览(z 90)→ 新任务(z 80)
|
||||
// 多模态共存:优先关靠前栈顶 — 小预览(z 116)→ 文件预览(z 114,可从 kb 等
|
||||
// 112 层 modal 内打开,须先于它们关)→ 各 112 层 modal → 选入(z 95)→ 新任务(z 80)
|
||||
if ($("mini-preview-modal").classList.contains("show")) { closeMiniPreview(); return; }
|
||||
if ($("file-preview-modal").classList.contains("show")) { closeFilePreview(); return; }
|
||||
if ($("chpw-modal").classList.contains("show")) { closeChpwModal(); return; }
|
||||
if ($("skills-modal").classList.contains("show")) { closeSkillsModal(); return; }
|
||||
if ($("memory-modal").classList.contains("show")) { closeMemoryModal(); return; }
|
||||
|
|
@ -115,9 +118,7 @@ document.addEventListener("keydown", (e) => {
|
|||
if ($("crons-modal").classList.contains("show")) { closeCronsModal(); return; }
|
||||
if ($("wechat-modal").classList.contains("show")) { closeWechatModal(); return; }
|
||||
if ($("changelog-modal").classList.contains("show")) { closeChangelogModal(); return; }
|
||||
if ($("mini-preview-modal").classList.contains("show")) { closeMiniPreview(); return; }
|
||||
if ($("src-picker-modal").classList.contains("show")) { closeSrcPicker(); return; }
|
||||
if ($("file-preview-modal").classList.contains("show")) { closeFilePreview(); return; }
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue