feat(knowledge): restore complete human rendering layer
This commit is contained in:
parent
893c1a0a12
commit
3466b507c6
15 changed files with 665 additions and 23 deletions
|
|
@ -14,3 +14,7 @@
|
|||
有意差异:概念图中的人格回复和日历执行是视觉示例;运行产品没有绑定人格模型和日历能力,因此不显示也不伪造这些结果。实现改为明确的 `WAITING_PERSONA` 回执。
|
||||
|
||||
首屏可见文字来自冰朔本轮要求或产品登记名,没有新增营销口号、指标、徽章或模型选择器。
|
||||
|
||||
## 知识库人类视觉渲染层纠正
|
||||
|
||||
冰朔指出首轮只有 Markdown 文本编辑器,不能称为完整知识库模块。现已从只读旧产品供体精确拆入 `knowledge-render` 切片,并在新壳中重新接线:目录、阅读渲染、frontmatter、标签、大纲、wikilink、callout、代码块、表格、来源证据、编辑/阅读切换均可见。验收截图为 `design/concepts/hololake-knowledge-human-render-1536x1024.png`;旧产品其他模块未合并。
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@
|
|||
"rust_clippy_denied_warnings": "PASS_100",
|
||||
"production_dependency_audit": "0_VULNERABILITIES",
|
||||
"browser_channel_create_message_separation_knowledge_create_edit_save": "PASS_100",
|
||||
"knowledge_human_visual_rendering_frontmatter_outline_wikilink_callout_table_code_evidence": "PASS_100",
|
||||
"responsive_viewports": ["1536x1024_PASS", "820x900_PASS"],
|
||||
"developer_id_signature": "PASS_100",
|
||||
"installed_application": "/Applications/HoloLake.app",
|
||||
"installed_version": "1.0.0",
|
||||
"installed_binary_sha256": "f3cd899f99bf7667cd70a897c98ef82ea862567409f61ed6f0a485fe9bc3a96e",
|
||||
"updater_archive_sha256": "87ae061b942e631a55d7a83002686f1be939431a4cfae62986e2d637c91ffefe",
|
||||
"installed_binary_sha256": "50e6206be32ce7c70d42998b6217956aaa99687976e3d9ee383d3671e03c250d",
|
||||
"updater_archive_sha256": "b126efa723415beacfad916621dbecceff7ea3ff24c79e6adacabd5bb343736a",
|
||||
"installed_process_readback": "PASS_100",
|
||||
"updater_artifact_signature": "PASS_100",
|
||||
"update_endpoint_http": 204,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
结论:新的独立干净产品根已经建立,HoloLake 1.0.0 已签名安装并在本机运行。它不是公众发行完成回执。
|
||||
|
||||
已通过:TCS→GIR 编译与双语投影、模块编号唯一性、七种来源分流、前端生产构建、Rust 测试与严格 Clippy、生产依赖审计、频道建立、语言/回执分离、知识页新建编辑保存、1536×1024 与 820×900 视口、Developer ID 签名、更新包签名、`/Applications/HoloLake.app` 进程读回。更新入口现场返回 HTTP 204,即服务在线且暂无更新。
|
||||
已通过:TCS→GIR 编译与双语投影、模块编号唯一性、七种来源分流、前端生产构建、Rust 测试与严格 Clippy、生产依赖审计、频道建立、语言/回执分离、知识页新建编辑保存、完整人类视觉阅读渲染(frontmatter、大纲、wikilink、callout、表格、代码块、来源证据)、1536×1024 与 820×900 视口、Developer ID 签名、更新包签名、`/Applications/HoloLake.app` 进程读回。更新入口现场返回 HTTP 204,即服务在线且暂无更新。
|
||||
|
||||
企业服务器读回:蓝环境保存 5 个中央仓和 5 个负责人工作仓;团队域权限尚未分配,公共切换尚未发生。冰朔语言主控工作仓没有独立编号与登记,因此本轮没有猜名创建。
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
|
|
@ -18,7 +18,7 @@ HoloLake 是公众个人初始化频道语言驱动操作系统。第一版只
|
|||
- 对话和执行事实使用不同来源类型,不把系统文字塞进用户或人格消息。
|
||||
- 公共四域由企业门户和企业服务器承载,不嵌入个人频道。
|
||||
- 第五域、冰朔私人核 `TCS-iZero∞` 及私人语料不进入公众发行。
|
||||
- 当前知识库可处理 Markdown 和纯文本文件夹;PDF、Office、网页结构化转换尚未实现。
|
||||
- 当前知识库具备人类视觉阅读层:安全 Markdown 渲染、frontmatter、页面大纲、callout、表格、代码块与 wikilink 跳转;可处理 Markdown 和纯文本文件夹。PDF、Office、网页结构化转换尚未实现。
|
||||
- 当前外部 AI 入口是本机来源明确的 inbox/outbox;实时加密通信尚未实现。
|
||||
- 更新客户端只信任光湖控制的 HTTPS 清单与签名;服务器发行清单尚未发布。
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
"@tauri-apps/plugin-dialog": "2.7.2",
|
||||
"@tauri-apps/plugin-process": "2.3.1",
|
||||
"@tauri-apps/plugin-updater": "2.10.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"marked": "^16.4.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
},
|
||||
|
|
@ -1509,6 +1511,13 @@
|
|||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
|
||||
|
|
@ -1630,6 +1639,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.420",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
|
||||
|
|
@ -1775,6 +1793,18 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "16.4.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.1.tgz",
|
||||
"integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
"@tauri-apps/plugin-dialog": "2.7.2",
|
||||
"@tauri-apps/plugin-process": "2.3.1",
|
||||
"@tauri-apps/plugin-updater": "2.10.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"marked": "^16.4.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
{"donor_id":"HLP-DONOR-GROK-BUILD-0001","path":"/Volumes/JZAO/HoloLake/reference-materials/grok-build-v-780d138","allowed_use":"BUILD_EXECUTOR_RESEARCH_ONLY","imported":false},
|
||||
{"donor_id":"HLP-DONOR-GUADA-0001","path":"/Volumes/JZAO/HoloLake/reference-materials/guada-v0.5.6","allowed_use":"KNOWLEDGE_UX_BEHAVIOR_ONLY","imported":false},
|
||||
{"donor_id":"HLP-DONOR-OUTLINE-0001","path":"/Volumes/JZAO/HoloLake/reference-materials/outline","allowed_use":"KNOWLEDGE_MODEL_AND_IMPORT_RESEARCH_ONLY","imported":false},
|
||||
{"donor_id":"HLP-DONOR-HOLOLAKE-LEGACY-0001","path":"product-source/hololake-native-desktop","allowed_use":"TAURI_SIGNING_UPDATE_AND_ACCEPTANCE_ORACLE_ONLY","imported":false}
|
||||
{"donor_id":"HLP-DONOR-HOLOLAKE-LEGACY-0001","path":"product-source/hololake-native-desktop","allowed_use":"TAURI_SIGNING_UPDATE_ACCEPTANCE_ORACLE_AND_KNOWLEDGE_RENDER_SLICE_ONLY","imported":true,"extracted_slices":["src/modules/knowledge-render/index.tsx","src/modules/knowledge-render/manifest.ts"],"whole_product_merge":false}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"module_id": "HLP-MOD-CHANNEL-0001",
|
||||
"name_zh": "个人初始化频道",
|
||||
"kind": "SYSTEM_FOUNDATION",
|
||||
"state": "LOCAL_INSTALLED_VERIFIED",
|
||||
"state": "LOCAL_INSTALLED_VERIFIED_WITH_HUMAN_VISUAL_RENDERING",
|
||||
"source": "src-tauri/src/lib.rs#create_channel",
|
||||
"data_owner": "CURRENT_LOCAL_USER"
|
||||
},
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
"kind": "PREINSTALLED_USER_MODULE",
|
||||
"state": "LOCAL_INSTALLED_VERIFIED",
|
||||
"source": "src-tauri/src/lib.rs#knowledge",
|
||||
"capabilities": ["CREATE", "READ", "EDIT", "IMPORT_FOLDER", "EXPORT", "MOVE_TO_TRASH", "PRIVATE_GIT_COMMIT"]
|
||||
"capabilities": ["CREATE", "READ", "EDIT", "HUMAN_MARKDOWN_RENDERING", "FRONTMATTER", "OUTLINE", "WIKILINK_NAVIGATION", "IMPORT_FOLDER", "EXPORT", "MOVE_TO_TRASH", "PRIVATE_GIT_COMMIT"]
|
||||
},
|
||||
{
|
||||
"module_id": "HLP-MOD-GLP-BRIDGE-0001",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ package = json.loads((ROOT / "package.json").read_text())
|
|||
for forbidden in ("openai", "anthropic", "@google/generative-ai", "langchain"):
|
||||
if forbidden in package.get("dependencies", {}):
|
||||
errors.append(f"internal model dependency forbidden: {forbidden}")
|
||||
for required in ("marked", "dompurify"):
|
||||
if required not in package.get("dependencies", {}):
|
||||
errors.append(f"knowledge human renderer dependency missing: {required}")
|
||||
renderer = ROOT / "src/modules/knowledge-render/index.tsx"
|
||||
if not renderer.exists() or "MarkdownDocument" not in renderer.read_text():
|
||||
errors.append("knowledge human visual renderer missing")
|
||||
|
||||
tauri = json.loads((ROOT / "src-tauri/tauri.conf.json").read_text())
|
||||
endpoints = tauri["plugins"]["updater"]["endpoints"]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import { relaunch } from "@tauri-apps/plugin-process";
|
|||
import { Icon } from "./icons";
|
||||
import * as api from "./runtime";
|
||||
import type { KnowledgeDocument, SourceKind, SystemSnapshot } from "./types";
|
||||
import {
|
||||
MarkdownDocument,
|
||||
documentOutline,
|
||||
jumpToHeading,
|
||||
splitFrontmatter,
|
||||
} from "./modules/knowledge-render";
|
||||
|
||||
type View =
|
||||
| "channel"
|
||||
|
|
@ -248,9 +254,10 @@ function Channel({
|
|||
<article>
|
||||
<h2>公共更新</h2>
|
||||
<strong>
|
||||
{data.publicDistributionState === "SIGNED_UPDATE_FEED_READY_NO_RELEASE"
|
||||
? "更新入口在线 · 暂无发行"
|
||||
: data.publicDistributionState}
|
||||
{data.publicDistributionState ===
|
||||
"SIGNED_UPDATE_FEED_READY_NO_RELEASE"
|
||||
? "更新入口在线 · 暂无发行"
|
||||
: data.publicDistributionState}
|
||||
</strong>
|
||||
<p>只接收签名发行,不自动执行普通 Git 拉取。</p>
|
||||
</article>
|
||||
|
|
@ -310,12 +317,15 @@ function Knowledge({
|
|||
[doc, setDoc] = useState<KnowledgeDocument | null>(null),
|
||||
[body, setBody] = useState(""),
|
||||
[query, setQuery] = useState(""),
|
||||
[newTitle, setNewTitle] = useState<string | null>(null);
|
||||
[newTitle, setNewTitle] = useState<string | null>(null),
|
||||
[editing, setEditing] = useState(false),
|
||||
[inspectorOpen, setInspectorOpen] = useState(true);
|
||||
useEffect(() => {
|
||||
if (selected)
|
||||
api.readDocument(selected).then((d) => {
|
||||
setDoc(d);
|
||||
setBody(d.body);
|
||||
setEditing(false);
|
||||
});
|
||||
}, [selected]);
|
||||
const list = useMemo(
|
||||
|
|
@ -337,6 +347,8 @@ function Knowledge({
|
|||
const save = async () => {
|
||||
if (doc) {
|
||||
await api.saveDocument(doc.path, body);
|
||||
setDoc({ ...doc, body });
|
||||
setEditing(false);
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
|
@ -348,6 +360,20 @@ function Knowledge({
|
|||
refresh();
|
||||
}
|
||||
};
|
||||
const parsed = useMemo(() => splitFrontmatter(doc?.body ?? ""), [doc?.body]);
|
||||
const outline = useMemo(() => documentOutline(doc?.body ?? ""), [doc?.body]);
|
||||
const stats = useMemo(() => {
|
||||
const chars = parsed.content.replace(/\s/g, "").length;
|
||||
return { chars, minutes: Math.max(1, Math.ceil(chars / 450)) };
|
||||
}, [parsed.content]);
|
||||
const openWiki = (target: string) => {
|
||||
const normalized = target.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
|
||||
const hit = data.documents.find(
|
||||
(item) =>
|
||||
item.title.replace(/[\s·•・\-_|*`]/g, "").toLowerCase() === normalized,
|
||||
);
|
||||
if (hit) setSelected(hit.path);
|
||||
};
|
||||
return (
|
||||
<div className="page knowledge-page">
|
||||
<header>
|
||||
|
|
@ -368,8 +394,14 @@ function Knowledge({
|
|||
<button onClick={() => setNewTitle("")}>新建知识页</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="knowledge-workspace">
|
||||
<aside>
|
||||
<div
|
||||
className={`knowledge-workspace complete ${inspectorOpen ? "" : "inspector-closed"}`}
|
||||
>
|
||||
<aside className="knowledge-browser">
|
||||
<div className="browser-title">
|
||||
<strong>知识目录</strong>
|
||||
<span>{data.documents.length} 篇</span>
|
||||
</div>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
|
@ -392,25 +424,69 @@ function Knowledge({
|
|||
))
|
||||
)}
|
||||
</aside>
|
||||
<section>
|
||||
<section className="document-workspace">
|
||||
{doc ? (
|
||||
<>
|
||||
<div className="editor-tools">
|
||||
<strong>{doc.title}</strong>
|
||||
<span />
|
||||
<button onClick={() => setInspectorOpen((value) => !value)}>
|
||||
{inspectorOpen ? "收起大纲" : "展开大纲"}
|
||||
</button>
|
||||
<button onClick={() => api.exportDocument(doc.path)}>
|
||||
下载
|
||||
</button>
|
||||
<button className="danger" onClick={del}>
|
||||
删除
|
||||
</button>
|
||||
<button onClick={save}>保存</button>
|
||||
{editing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBody(doc.body);
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button onClick={save}>保存</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="danger" onClick={del}>
|
||||
删除
|
||||
</button>
|
||||
<button onClick={() => setEditing(true)}>编辑</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
className="editor"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
{editing ? (
|
||||
<textarea
|
||||
className="editor"
|
||||
aria-label="Markdown 编辑器"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="document-scroll">
|
||||
<header className="reader-heading">
|
||||
<h1>{doc.title}</h1>
|
||||
<p>
|
||||
{stats.chars.toLocaleString()} 字 · 约 {stats.minutes}{" "}
|
||||
分钟读完
|
||||
</p>
|
||||
{parsed.tags.length > 0 && (
|
||||
<div className="meta-tags">
|
||||
{parsed.tags.map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<MarkdownDocument
|
||||
body={doc.body}
|
||||
knownTitles={data.documents.map((item) => item.title)}
|
||||
onWiki={openWiki}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="welcome">
|
||||
|
|
@ -421,6 +497,53 @@ function Knowledge({
|
|||
</div>
|
||||
)}
|
||||
</section>
|
||||
{inspectorOpen && (
|
||||
<aside className="document-inspector">
|
||||
<header>
|
||||
<strong>页面大纲</strong>
|
||||
<span>来源与证据</span>
|
||||
</header>
|
||||
{doc ? (
|
||||
<>
|
||||
<nav className="outline-list">
|
||||
{outline.length ? (
|
||||
outline.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
style={{ paddingLeft: 12 + (item.level - 1) * 10 }}
|
||||
onClick={() => jumpToHeading(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p>当前页面没有标题层级。</p>
|
||||
)}
|
||||
</nav>
|
||||
<dl className="document-evidence">
|
||||
<div>
|
||||
<dt>知识根</dt>
|
||||
<dd>本机私人 Git</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>路径</dt>
|
||||
<dd>{doc.path}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>内容指纹</dt>
|
||||
<dd>{doc.sha256.slice(0, 16)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>状态</dt>
|
||||
<dd>可编辑 · 可回收</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
) : (
|
||||
<p className="empty">选择页面后显示大纲与证据。</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
{newTitle !== null && (
|
||||
<div className="modal-backdrop" onMouseDown={() => setNewTitle(null)}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
//! 知识渲染件 · 渲染入口(常驻模块 · 插座口 knowledge-render)
|
||||
//!
|
||||
//! 职责:把仓库里的 Markdown 文件变成可读的页面——frontmatter 解析、
|
||||
//! Notion/Outline 导出兼容、标题锚点、大纲同源、跳转。
|
||||
import { useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked, Parser, type Tokens } from "marked";
|
||||
export { knowledgeRenderManifest } from "./manifest";
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
export function splitFrontmatter(body: string) {
|
||||
const normalized = body.replace(/\r\n/g, "\n");
|
||||
if (!normalized.startsWith("---\n"))
|
||||
return {
|
||||
content: normalized,
|
||||
metadata: {} as Record<string, string>,
|
||||
tags: [] as string[],
|
||||
};
|
||||
const closing = normalized.indexOf("\n---\n", 4);
|
||||
if (closing < 0)
|
||||
return {
|
||||
content: normalized,
|
||||
metadata: {} as Record<string, string>,
|
||||
tags: [] as string[],
|
||||
};
|
||||
const metadata: Record<string, string> = {};
|
||||
const tags: string[] = [];
|
||||
let pendingListKey = "";
|
||||
for (const line of normalized.slice(4, closing).split("\n")) {
|
||||
const listItem = /^\s+-\s+(.+)$/.exec(line);
|
||||
if (listItem && pendingListKey) {
|
||||
if (pendingListKey === "tags")
|
||||
tags.push(listItem[1].trim().replace(/^['"]|['"]$/g, ""));
|
||||
continue;
|
||||
}
|
||||
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
||||
if (!match) continue;
|
||||
pendingListKey = match[2] === "" ? match[1] : "";
|
||||
const value = match[2].replace(/^['"]|['"]$/g, "");
|
||||
metadata[match[1]] = value;
|
||||
if (match[1] === "tags") {
|
||||
const inline = /^\[(.*)\]$/.exec(match[2].trim());
|
||||
const source = inline ? inline[1] : value;
|
||||
source
|
||||
.split(",")
|
||||
.map((part) => part.trim().replace(/^['"]|['"]$/g, ""))
|
||||
.filter(Boolean)
|
||||
.forEach((tag) => tags.push(tag));
|
||||
}
|
||||
}
|
||||
return { content: normalized.slice(closing + 5), metadata, tags };
|
||||
}
|
||||
|
||||
function stripNotionLinks(line: string) {
|
||||
// Notion 页面链接 → 只留链接文字(跳转关系按谕旨失效)
|
||||
return line.replace(
|
||||
/\[([^\]]*)\]\(([^)]*)\)/g,
|
||||
(whole, text: string, href: string) =>
|
||||
/notion\.(?:so|site)/.test(href) ? text : whole,
|
||||
);
|
||||
}
|
||||
|
||||
export function notionCompat(content: string) {
|
||||
// 存量旧库文件尚未洗净:渲染时按"进门即洗"同规矩兜底过滤一遍。
|
||||
// 内容一字不改,只把外来知识库的外壳换成光湖原生 Markdown(冰朔 2026-08-15 谕:不兼容,进门就转)。
|
||||
const result: string[] = [];
|
||||
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "<aside>" || trimmed === "</aside>") continue;
|
||||
if (trimmed.startsWith(":::toggle")) {
|
||||
const title = trimmed.slice(":::toggle".length).trim();
|
||||
result.push(title ? `**▸ ${title}**` : "**▸ 详情**");
|
||||
continue;
|
||||
}
|
||||
if (trimmed === ":::" || trimmed.startsWith(":::toc")) continue;
|
||||
result.push(stripNotionLinks(line.replace(/<br\s*\/?>/g, "\n")));
|
||||
}
|
||||
// Notion 粗体写法(**文字:**紧接正文)不合 CommonMark:给收尾星号补一口气
|
||||
return result
|
||||
.join("\n")
|
||||
.replace(/(\*\*[^*\n]+\*\*)(?=[^\s.,;:!?,。;:!?、)\]"'])/g, "$1 ");
|
||||
}
|
||||
|
||||
const CALLOUT_TINTS: [RegExp, string][] = [
|
||||
[/📌|🌌|🧭|💜|🗂|🏷|📚/, "callout-lavender"],
|
||||
[/💡|⚡|🌟|☀|✨|🔑/, "callout-amber"],
|
||||
[/⚠|🔥|❗|🚨|❌/, "callout-rose"],
|
||||
[/✅|🌿|🍀|💚|✔/, "callout-mint"],
|
||||
[/🌊|💧|🔵|❄|🧊/, "callout-sky"],
|
||||
];
|
||||
function normalizeTitle(value: string) {
|
||||
return value.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function calloutTint(text: string) {
|
||||
for (const [pattern, tint] of CALLOUT_TINTS)
|
||||
if (pattern.test(text)) return tint;
|
||||
return "callout-slate";
|
||||
}
|
||||
|
||||
export function markdownHtml(
|
||||
body: string,
|
||||
knownTitles: readonly string[] = [],
|
||||
) {
|
||||
const compat = notionCompat(body);
|
||||
const titleMap = new Map<string, string>();
|
||||
for (const title of knownTitles) titleMap.set(normalizeTitle(title), title);
|
||||
const withWikiLinks = compat.replace(
|
||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||
(_whole, target: string, label?: string) =>
|
||||
`<button class="wiki-link" type="button" data-wiki="${escapeHtml(target.trim())}">${escapeHtml((label || target).trim())}</button>`,
|
||||
);
|
||||
let headingIndex = 0;
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.heading = (token) => {
|
||||
const id = `heading-${headingIndex}`;
|
||||
headingIndex += 1;
|
||||
const inlineHtml = token.tokens.length
|
||||
? Parser.parseInline(token.tokens)
|
||||
: escapeHtml(token.text);
|
||||
return `<h${token.depth} id="${id}">${inlineHtml}</h${token.depth}>\n`;
|
||||
};
|
||||
renderer.blockquote = (token) => {
|
||||
const inner = Parser.parse(token.tokens) as string;
|
||||
return `<blockquote class="callout ${calloutTint(token.raw)}">${inner}</blockquote>\n`;
|
||||
};
|
||||
renderer.link = (token) => {
|
||||
const inner = token.tokens.length
|
||||
? Parser.parseInline(token.tokens)
|
||||
: escapeHtml(token.text);
|
||||
const href = token.href || "";
|
||||
if (/notion\.(?:so|site)/i.test(href)) {
|
||||
// Notion 残留链接:认得出是自家页面就转成内部跳转,认不出标"外来页面"。
|
||||
const hit = titleMap.get(normalizeTitle(token.text));
|
||||
if (hit)
|
||||
return `<button class="wiki-link" type="button" data-wiki="${escapeHtml(hit)}">${inner}</button>`;
|
||||
return `<a class="external-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}<span class="external-mark">外来页面</span></a>`;
|
||||
}
|
||||
if (/^https?:\/\//i.test(href))
|
||||
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
|
||||
return `<a href="${escapeHtml(href)}">${inner}</a>`;
|
||||
};
|
||||
const html = marked.parse(withWikiLinks, {
|
||||
async: false,
|
||||
gfm: true,
|
||||
breaks: false,
|
||||
renderer,
|
||||
}) as string;
|
||||
return DOMPurify.sanitize(html, {
|
||||
ADD_ATTR: ["data-wiki", "id", "target", "rel"],
|
||||
ADD_TAGS: ["button"],
|
||||
});
|
||||
}
|
||||
|
||||
export function documentOutline(body: string) {
|
||||
const headings: { id: string; level: number; title: string }[] = [];
|
||||
const plain = (tokens: unknown): string =>
|
||||
(tokens as { type: string; text?: string; tokens?: unknown[] }[])
|
||||
.map((token) =>
|
||||
token.tokens && token.tokens.length
|
||||
? plain(token.tokens)
|
||||
: token.text || "",
|
||||
)
|
||||
.join("");
|
||||
marked
|
||||
.lexer(splitFrontmatter(body).content, { gfm: true })
|
||||
.filter((token): token is Tokens.Heading => token.type === "heading")
|
||||
.forEach((token, index) => {
|
||||
if (index >= 24) return;
|
||||
headings.push({
|
||||
id: `heading-${index}`,
|
||||
level: token.depth,
|
||||
title: plain(token.tokens).trim(),
|
||||
});
|
||||
});
|
||||
return headings;
|
||||
}
|
||||
|
||||
export function jumpToHeading(id: string) {
|
||||
const element = document.querySelector<HTMLElement>(
|
||||
`.document-scroll [id="${id}"]`,
|
||||
);
|
||||
if (!element) return;
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
element.classList.remove("heading-flash");
|
||||
void element.offsetWidth;
|
||||
element.classList.add("heading-flash");
|
||||
}
|
||||
|
||||
export function MarkdownDocument({
|
||||
body,
|
||||
knownTitles,
|
||||
onWiki,
|
||||
}: {
|
||||
body: string;
|
||||
knownTitles?: readonly string[];
|
||||
onWiki?: (target: string) => void;
|
||||
}) {
|
||||
const parsed = useMemo(() => splitFrontmatter(body), [body]);
|
||||
const html = useMemo(
|
||||
() => markdownHtml(parsed.content, knownTitles),
|
||||
[parsed.content, knownTitles],
|
||||
);
|
||||
return (
|
||||
<article
|
||||
className="markdown-document"
|
||||
onClick={(event) => {
|
||||
const element = (event.target as HTMLElement).closest<HTMLElement>(
|
||||
"[data-wiki]",
|
||||
);
|
||||
if (element?.dataset.wiki && onWiki) onWiki(element.dataset.wiki);
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//! 模块形状声明 · 插座制(冰朔 2026-08-15 架构谕)
|
||||
//!
|
||||
//! 软件只认这一个插座形状:每个模块 = manifest(我是谁/插哪个口/从哪来)
|
||||
//! + index(渲染入口导出)。常驻模块住在频道里;将来 origin='repo' 的
|
||||
//! 模块躺代码仓库,人格体按需拉取部署。
|
||||
export const knowledgeRenderManifest = {
|
||||
moduleId: "hololake.knowledge-render",
|
||||
name: "知识渲染件",
|
||||
version: "0.1.0",
|
||||
slot: "knowledge-render",
|
||||
origin: "resident",
|
||||
exports: [
|
||||
"splitFrontmatter",
|
||||
"documentOutline",
|
||||
"jumpToHeading",
|
||||
"MarkdownDocument",
|
||||
],
|
||||
} as const;
|
||||
|
||||
export type ModuleManifest = {
|
||||
moduleId: string;
|
||||
name: string;
|
||||
version: string;
|
||||
slot: string;
|
||||
origin: "resident" | "repo";
|
||||
exports: readonly string[];
|
||||
};
|
||||
|
|
@ -459,6 +459,32 @@ main {
|
|||
overflow: hidden;
|
||||
background: rgba(5, 17, 31, 0.76);
|
||||
}
|
||||
.knowledge-workspace.complete {
|
||||
grid-template-columns: 260px minmax(0, 1fr) 232px;
|
||||
height: min(720px, calc(100vh - 175px));
|
||||
min-height: 560px;
|
||||
}
|
||||
.knowledge-workspace.complete.inspector-closed {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
}
|
||||
.knowledge-browser {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.browser-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 2px 13px;
|
||||
}
|
||||
.browser-title strong {
|
||||
font-size: 12px;
|
||||
color: #cad8e6;
|
||||
}
|
||||
.browser-title span {
|
||||
font-size: 10px;
|
||||
color: #6f879e;
|
||||
}
|
||||
.knowledge-workspace > aside {
|
||||
border-right: 1px solid var(--line);
|
||||
padding: 13px;
|
||||
|
|
@ -532,6 +558,197 @@ main {
|
|||
font-family: "SFMono-Regular", Menlo, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
.document-workspace {
|
||||
min-height: 0;
|
||||
}
|
||||
.document-scroll {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 42px clamp(28px, 5vw, 70px) 70px;
|
||||
}
|
||||
.reader-heading {
|
||||
padding-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.reader-heading h1 {
|
||||
font-size: 34px;
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.reader-heading p {
|
||||
color: #8097ad;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.meta-tags {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.meta-tags span {
|
||||
padding: 4px 8px;
|
||||
border-radius: 7px;
|
||||
color: #b8d8ef;
|
||||
background: rgba(71, 139, 188, 0.14);
|
||||
font-size: 10px;
|
||||
}
|
||||
.markdown-document {
|
||||
color: #cbd8e6;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.markdown-document h1,
|
||||
.markdown-document h2,
|
||||
.markdown-document h3,
|
||||
.markdown-document h4 {
|
||||
color: #f0f5fb;
|
||||
line-height: 1.35;
|
||||
scroll-margin-top: 24px;
|
||||
}
|
||||
.markdown-document h1 {
|
||||
font-size: 28px;
|
||||
margin: 1.4em 0 0.6em;
|
||||
}
|
||||
.markdown-document h2 {
|
||||
font-size: 22px;
|
||||
margin: 1.7em 0 0.55em;
|
||||
}
|
||||
.markdown-document h3 {
|
||||
font-size: 18px;
|
||||
margin: 1.5em 0 0.5em;
|
||||
}
|
||||
.markdown-document p {
|
||||
margin: 0 0 1.1em;
|
||||
}
|
||||
.markdown-document a,
|
||||
.markdown-document .wiki-link {
|
||||
color: #84c9f3;
|
||||
}
|
||||
.markdown-document .wiki-link {
|
||||
display: inline;
|
||||
border: 0;
|
||||
padding: 0 2px;
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.markdown-document blockquote.callout {
|
||||
margin: 1.3em 0;
|
||||
padding: 14px 17px;
|
||||
border-left: 3px solid #74bee9;
|
||||
border-radius: 0 10px 10px 0;
|
||||
background: rgba(72, 137, 181, 0.1);
|
||||
}
|
||||
.markdown-document blockquote.callout-amber {
|
||||
border-left-color: var(--gold);
|
||||
background: rgba(205, 153, 64, 0.1);
|
||||
}
|
||||
.markdown-document blockquote.callout-rose {
|
||||
border-left-color: #df8991;
|
||||
background: rgba(190, 83, 94, 0.1);
|
||||
}
|
||||
.markdown-document blockquote.callout-mint {
|
||||
border-left-color: var(--green);
|
||||
background: rgba(81, 170, 119, 0.1);
|
||||
}
|
||||
.markdown-document pre {
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(1, 7, 14, 0.65);
|
||||
}
|
||||
.markdown-document code {
|
||||
font-family: "SFMono-Regular", Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.markdown-document table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
.markdown-document th,
|
||||
.markdown-document td {
|
||||
padding: 9px 11px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-document img {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.heading-flash {
|
||||
animation: headingFlash 1.2s ease;
|
||||
}
|
||||
@keyframes headingFlash {
|
||||
0% {
|
||||
color: var(--gold);
|
||||
text-shadow: 0 0 18px rgba(240, 197, 107, 0.4);
|
||||
}
|
||||
}
|
||||
.document-inspector {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border-left: 1px solid var(--line);
|
||||
border-right: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.document-inspector > header {
|
||||
padding: 18px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.document-inspector > header strong,
|
||||
.document-inspector > header span {
|
||||
display: block;
|
||||
}
|
||||
.document-inspector > header strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
.document-inspector > header span {
|
||||
color: #72899f;
|
||||
font-size: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.outline-list {
|
||||
padding: 10px 8px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.outline-list button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
color: #9fb2c5;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
padding-block: 7px;
|
||||
}
|
||||
.outline-list p {
|
||||
color: #72899f;
|
||||
font-size: 11px;
|
||||
padding: 8px;
|
||||
}
|
||||
.document-evidence {
|
||||
padding: 14px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
.document-evidence div {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
.document-evidence dt {
|
||||
color: #6f879e;
|
||||
font-size: 10px;
|
||||
}
|
||||
.document-evidence dd {
|
||||
color: #bdcddd;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin: 4px 0 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
.split-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
@ -848,6 +1065,13 @@ main {
|
|||
.knowledge-workspace {
|
||||
grid-template-columns: 210px 1fr;
|
||||
}
|
||||
.knowledge-workspace.complete,
|
||||
.knowledge-workspace.complete.inspector-closed {
|
||||
grid-template-columns: 210px minmax(0, 1fr);
|
||||
}
|
||||
.document-inspector {
|
||||
display: none;
|
||||
}
|
||||
.split-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
"canonical_tcs": "product-source/hololake-clean-desktop/language/HOLOLAKE-CLEAN-V1-FOUNDATION.tcs",
|
||||
"old_product_role": "READ_ONLY_DONOR_NO_WHOLE_MERGE",
|
||||
"first_preinstalled_module": "HLP-MOD-KB-0001",
|
||||
"knowledge_module_state": "LOCAL_COMPLETE_HUMAN_VISUAL_RENDERING_CREATE_READ_EDIT_IMPORT_EXPORT_TRASH_PRIVATE_GIT",
|
||||
"knowledge_human_rendering_source": "product-source/hololake-clean-desktop/src/modules/knowledge-render/index.tsx",
|
||||
"typed_language_sources": ["USER_MESSAGE", "PERSONA_RESPONSE", "SYSTEM_CONTEXT", "PROTOCOL_EVENT", "AGENT_ACTION", "TOOL_RESULT", "SYSTEM_RECEIPT"],
|
||||
"public_update_endpoint": "https://guanghulab.com/hololake/releases/latest.json",
|
||||
"public_update_release_state": "SERVER_HTTP_204_NO_PUBLIC_RELEASE",
|
||||
|
|
|
|||
Loading…
Reference in a new issue