魔改:从为知笔记(经典版)迁移所有笔记到思源

为知下小两千条笔记需要迁移,不想手动搞,魔改了下思源核心,实现自动迁移,分享给有同样需求的朋友们。

1、省流版 拢共分三步:

1.1、同步所有笔记到本地

使用为知笔记经典版,新版不支持,使用新版创建的笔记也不支持,同步所有笔记内容到本地:

image.png

1.2、替换思源笔记内核

内核位于安装路径的 SiYuan\resources\kernel\SiYuan-Kernel.exe,退出思源笔记后替换成我上传的内核:

SiYuanKernel.7z

此内核为==3.1.1==版本编译,其它版本请按照下方源码自行编译!

替换前记得备份原内核!!

1.3、开始迁移

==迁移前请备份思源笔记里原有笔记,这里不对任何迁移过程中丢失笔记数据的事件负责!!==

使用 导入 -Markdown 文件夹 功能导入;

image.png

选择为知账户数据文件夹

image.png

开始

2e22f327918209261e3d5ae815dc25e.png

2、Tips

已知的问题:

  • 仅支持经典版为知笔记客户端,和网页版创建的笔记,新版为知笔记创建的条目会自动忽略;
  • 支持隐藏笔记,不支持加密笔记,加密笔记会被忽略;
  • 不支持单元格内有多行或者其它复杂格式的表格,导入过程将格式复杂的表格转换为 [TABLE] 文本,须手动查找后修复;
  • 如果原笔记标题带路径分隔符'/',导入的笔记标题会变成'/'后的字符串,可以做转义,懒得搞了;
  • 为知笔记剪藏时会魔改一些格式,剪藏的内容,可能会格式错乱或者转换失败;
  • 为知笔记转换的 PDF、EXCEL、PPT 等不能转换成 Markdown,不过会保留附件,无伤大雅;
  • 导入过程不会在被导入的文件夹下创建一个新的文件夹来存储笔记,而是按照为知的笔记结构直接导入到被导入文件夹下。

3、魔改源码

仅修改了一个源码文件,即 model 中的 import.go 文件。

import{} 中添加

sqlite "database/sql" //魔改:导入原始sqlite3包
_ "github.com/mattn/go-sqlite3" // 魔改:导入原始sqlite3包

添加主函数,详情见注释:

// 魔改:新增函数从为知笔记数据文件夹导入
func ImportFromWiz(boxID, localPath, baseHPath, baseTargetPath string) (err error) {
	wizdbPath := filepath.Join(localPath, "index.db")
	wizdb, err := sqlite.Open("sqlite3", wizdbPath)
	if err != nil {
		return nil
	}
	defer wizdb.Close()
	// 注意NULL值与任何值做任何比较,结果都是fasle,过滤条件须添加 `DOCUMENT_TYPE IS NULL`
	wizrows_count, err := wizdb.Query("SELECT COUNT(*) FROM WIZ_DOCUMENT WHERE (DOCUMENT_TYPE != 'collaboration' OR DOCUMENT_TYPE IS NULL) AND DOCUMENT_PROTECT = 0")
	if err != nil {
		return nil
	}
	wizrows_count.Next()
	wizCount := 0
	wizColumn := 0
	err = wizrows_count.Scan(&wizCount)
	wizrows_count.Close()
	if err != nil {
		return nil
	}
	wizrows, err := wizdb.Query("SELECT DOCUMENT_GUID, DOCUMENT_TITLE, DOCUMENT_LOCATION, DOCUMENT_NAME, DOCUMENT_URL, DOCUMENT_TYPE, DOCUMENT_FILE_TYPE, DT_CREATED, DT_MODIFIED, DT_ACCESSED, DOCUMENT_ATTACHEMENT_COUNT FROM WIZ_DOCUMENT WHERE (DOCUMENT_TYPE != 'collaboration' OR DOCUMENT_TYPE IS NULL) AND DOCUMENT_PROTECT = 0 ORDER BY DT_CREATED ASC")
	if err != nil {
		logging.LogErrorf("查询为知数据库失败:%s", err)
		return nil
	}
	defer wizrows.Close()
	util.PushEndlessProgress("检测到为知笔记数据库,正在尝试导入笔记……")

	var markdownStr string
	var tree *parse.Tree

	var DocumentGuid string         // GUID
	var DocumentTitle string        // 标题
	var DocumentLocation string     // 路径
	var DocumentName string         // 文件名
	var DocumentUrl *string         // 如果不为空则是剪藏的笔记,剪藏的url
	var DocumentType *string        // 类型,除了经典版不存储的值为collaboration的md笔记,lite/markdown为html和md混合排版笔记,其它默认为html笔记
	var DocumentFileType *string    // 如果不为空则是导入的笔记,被导入文件类型
	var DtCreated string            // 创建时间
	var DtModified string           // 修改时间
	var DtAccessed string           // 访问时间
	var DocumentAttachmentCount int // 附件数

	targetPaths := map[string]string{}
	tmpPath := os.Getenv("LOCALAPPDATA")
	if tmpPath == "" {
		return errors.New("获取系统应用程序数据文件夹路径失败!")
	}
	tmpPath = filepath.Join(tmpPath, "Temp")
	unzipPath := filepath.Join(tmpPath, "wizunzip")
	luaPath := filepath.Join(util.TempDir, "pandoc_script.lua")
	// 生成pandoc过滤器lua脚本,如果存在,则不创建
	if !gulu.File.IsExist(luaPath) {
		luaString := `
-- 定义一个函数,用于检查样式并转换为对应的Markdown标记
local function convert_style (elem)
  if #elem.content == 0 then
    return {}
  end
  -- 检查 elem.attributes 是否存在
  if not elem.attributes then
    return elem.content
  end
  -- 获取样式属性
  local style = elem.attributes.style
  if not style then
    return elem.content
  end
  -- 下划线样式,将内容用<u>包围,排除超链接
  if (string.find(style, 'text%-decoration%s*:%s*underline') or string.find(style, 'text%-decoration%-style')) and elem.t ~= "Link" then
    -- 创建 <u> 标签包裹内容
    local u_open = pandoc.RawInline('gfm', '<u>')
    local u_close = pandoc.RawInline('gfm', '</u>')
    -- 插入 <u> 标签到内容的前后
    table.insert(elem.content, 1, u_open)
    table.insert(elem.content, u_close)
  end
  -- 粗体样式,将内容用**包围
  if string.find(style, 'font%-weight%s*:%s*bold') then
    -- 创建字符串节点
    local s_label = pandoc.RawInline('gfm','**')
    -- 插入字符串到内容的前后
    table.insert(elem.content, 1, s_label)
    table.insert(elem.content, s_label)
  end
  -- 斜体样式,将内容用*包围
  if string.find(style, 'font%-style%s*:%s*italic') then
    local e_label = pandoc.RawInline('gfm','*')
    table.insert(elem.content, 1, e_label)
    table.insert(elem.content, e_label)
  end
  -- 删除线样式,将内容用~~包围
  if string.find(style, 'text%-decoration%s*:%s*line%-through') then
    local so_label = pandoc.RawInline('gfm','~~')
    table.insert(elem.content, 1, so_label)
    table.insert(elem.content, so_label)
  end
  -- 高亮样式,将内容用==包围
  if string.find(style, 'background%-color:%s*rgb%(255,%s*255,%s*0%)') then
    local m_label = pandoc.RawInline('gfm','==')
    table.insert(elem.content, 1, m_label)
    table.insert(elem.content, m_label)
  end
  return elem.content
end

-- 不带"+raw_html"情况下提取为知笔记代码块
function Div(elem)
  -- 仅处理 class="wiz-code-container" <div>
  if elem.classes[1] == "wiz-code-container" then
    local code_lang = elem.attributes.mode
	local code_content = ""
	elem.content = elem.content[1].content[6].content[1].content[1].content[1].content[1].content[5].content
	for i, code in ipairs(elem.content) do
	  if not code.content[4] then
	    code_content = code_content..code.content[2].text.."\n"
	  else
	    code_content = code_content..code.content[4].text.."\n"
	  end
	end
	return pandoc.CodeBlock(code_content, { class = code_lang })
  else return pandoc.Div(convert_style(elem))
  end
end

-- <br>标签转换为自然换行
function LineBreak(elem)
  return pandoc.Str("\n")
end

-- 处理<u>标签,跳过pandoc带-raw_html参数时转换
function Underline(elem)
  if #elem.content == 0 then
    return {}
  end
  local u_open = pandoc.RawInline('gfm','<u>')
  local u_close = pandoc.RawInline('gfm','</u>')
  table.insert(elem.content, 1, u_open)
  table.insert(elem.content, u_close)
  return elem.content
end
-- <sup>上标
function Superscript(elem)
  if #elem.content == 0 then
    return {}
  end
  local sup_label = pandoc.RawInline('gfm','^')
  table.insert(elem.content, 1, sup_label)
  table.insert(elem.content, sup_label)
  return elem.content
end
-- <sub>下标
function Subscript(elem)
  if #elem.content == 0 then
    return {}
  end
  local sub_label = pandoc.RawInline('gfm','~')
  table.insert(elem.content, 1, sub_label)
  table.insert(elem.content, sub_label)
  return elem.content
end
-- <kbd>处理流程在<span>标签中
-- 处理<SPAN>标签
function Span(elem)
  -- 处理<kbd>标签
  if elem.classes[1] == "kbd" and #elem.content ~= 0 then
    local k_open = pandoc.RawInline('gfm','<kbd>')
    local k_close = pandoc.RawInline('gfm','</kbd>')
	table.insert(elem.content, 1, k_open)
    table.insert(elem.content, k_close)
	return elem.content
  else
    -- 转换<span>的style样式为markdown标签
    return pandoc.Span(convert_style(elem))
  end
end
-- 处理<a>标签
function Link(elem)
  -- 删除空链接或空字符的超链接
  if elem.target == "" or #elem.content == 0 then
    -- 返回空字符串,删除整个<a>标签
    return {}
  -- 页内导航直接返回文本
  elseif elem.target:match('^#') then
    return convert_style(elem)
  end
  return pandoc.Link(convert_style(elem),elem.target,elem.title)
end
-- 处理<h>标签样式
function Header(elem)
  if #elem.content == 0 then
    return {}
  end
  return pandoc.Header(elem.level,convert_style(elem))
end

-- 处理base64编码内容
-- 识别mimetype,并转换为扩展名
local mimetypes = {
  ["image/jpeg"] = "jpg",
  ["image/png"] = "png",
  ["image/gif"] = "gif",
  ["image/svg+xml"] = "svg",
  ["audio/mpeg"] = "mp3",
  ["audio/ogg"] = "ogg",
  ["video/mp4"] = "mp4",
  ["application/pdf"] = "pdf",
}
-- 计算字符串hash值
function djb2_hash(str)
  local hash = 7
  for i = 1, #str do
      hash = hash * 3 + string.byte(str, i)
  end
  return hash
end
-- 解码base64
local function decode_base64(input)
  local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  input = string.gsub(input, '[^'..b..'=]', '')
  return (input:gsub('.', function(x)
    if (x == '=') then return '' end
    local r, f = '', (b:find(x) - 1)
    for i = 6, 1, -1 do
      r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and '1' or '0')
    end
    return r
  end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
    if (#x ~= 8) then return '' end
    local c = 0
    for i = 1, 8 do c = c + (x:sub(i, i) == '1' and 2 ^ (8 - i) or 0) end
    return string.char(c)
  end))
end
-- 生成文件和链接
local function extract_base64(elem)
  local src = elem.src
  local mime_type, base64_data = src:match('^data:(.-);base64,(.+)')
  -- 清除为知笔记像素点
  if base64_data == 'R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' then
    return {}
  end
  local ext = mimetypes[mime_type] or 'bin'
  local file_name = string.format("%08x", djb2_hash(base64_data)) .. '.' .. ext
  local file_path = os.getenv('LOCALAPPDATA').."\\Temp\\wizunzip\\"..file_name
  -- 如果file_path不存在,则创建文件
  local file = io.open(file_path, 'r')
  if not file then
    local decoded_data = decode_base64(base64_data)
    if ext == "svg" and not string.find(decoded_data, 'xmlns="http://www.w3.org') then
      decoded_data = decoded_data:gsub('<svg', '<svg xmlns="http://www.w3.org/2000/svg"')
    end
    -- 创建decode后的二进制文件
    file = io.open(file_path, 'wb')
    file:write(decoded_data)
    file:close()
  else
    io.close(file)
  end
  -- 生成markdown链接
  return pandoc.Image(elem.caption,file_name,elem.title)
end
-- 处理<img><svg>等标签
function Image(elem)
  if elem.src == "" then
    return {}
  -- 提取base64图片,思源无法识别gif和svg,此处预处理所有base64图像
  elseif elem.src:match('^data:') then
    return extract_base64(elem)
  end
end
-- 处理<pre>标签
function CodeBlock(elem)
  if #elem.classes == 0 then
    elem.classes = {"plaintext"}
  -- 如果elem.classes[1]的值是hljs,则是hljs样式,返回elem.classes[2]的值
  elseif elem.classes[1] == "hljs" then
    elem.classes = {elem.classes[2]}
  -- 如果elem.classes的值以brush:开头,则返回elem.classes的值为brush:后的值
  elseif elem.classes[1]:match('^brush:') then
    elem.classes = {elem.classes[1]:match('^brush:(.+)')}
  -- 如果elem.classes的值以language-开头,则返回elem.classes的值为language-后的值
  elseif elem.classes[1]:match('^language-') then
    elem.classes = {elem.classes[1]:match('^language-(.+)')}
  end
  return elem
end
-- 处理纯附件导入笔记的表格
function Table(elem)
  if elem.bodies[1].body[1].cells[1].contents[1].t == "Header" then
   local cell = pandoc.Plain(elem.bodies[1].body[1].cells[1].contents[1].content[1])
   elem.bodies[1].body[1].cells[1].contents[1] = cell
   return elem
  end
end
`
		if os.WriteFile(luaPath, []byte(luaString), 0755) != nil {
			msg := fmt.Sprintf("创建pandoc过滤器lua文件失败:%s", err)
			logging.LogErrorf(msg)
			return errors.New(msg)
		}
		defer os.RemoveAll(luaPath)
	}

	targetPaths["/"] = baseTargetPath
	succeed := 0
	failed := 0
	incomplete := 0

	for wizrows.Next() {
		// 清理资源
		os.RemoveAll(unzipPath)
		importTrees = []*parse.Tree{}
		searchLinks = map[string]string{}

		err = wizrows.Scan(&DocumentGuid, &DocumentTitle, &DocumentLocation, &DocumentName, &DocumentUrl, &DocumentType, &DocumentFileType, &DtCreated, &DtModified, &DtAccessed, &DocumentAttachmentCount)
		if err != nil {
			msg := fmt.Sprintf("读取为知数据库失败:%s", err)
			logging.LogErrorf(msg)
			return errors.New(msg)
		}
		// 构造targetPaths哈希表,键为文件夹的相对路径,值为思源笔记树形结构中的绝对路径 & 创建文件夹
		DocumentLocation = path.Dir(DocumentLocation)
		targetPath := ""
		if targetPaths[DocumentLocation] == "" {
			importTrees = []*parse.Tree{}
			// 生成当前文档父级路径
			DocumentPath := DocumentLocation
			for targetPaths[DocumentPath] == "" {
				targetPath = path.Join(wizDate(DtCreated)+"-"+randStr(7), targetPath)
				DocumentPath = path.Dir(DocumentPath)
			}
			targetPath = path.Join(strings.TrimSuffix(targetPaths[DocumentPath], ".sy"), targetPath)
			// 递归生成每级父级路径
			DocumentPath = DocumentLocation
			for targetPaths[DocumentPath] == "" {
				targetPaths[DocumentPath] = targetPath + ".sy"
				// 构造文件夹树
				tree = treenode.NewTree(boxID, targetPaths[DocumentPath], path.Join(baseHPath, DocumentPath), path.Base(DocumentPath))
				importTrees = append(importTrees, tree)

				DocumentPath = path.Dir(DocumentPath)
				targetPath = path.Dir(targetPath)
			}
		}

		wizColumn += 1
		wizNote := path.Join(DocumentLocation, DocumentTitle)
		util.PushEndlessProgress(fmt.Sprintf("正在导入为知笔记[%d/%d],成功[%d]失败[%d]:%s", wizColumn, wizCount, succeed, failed, wizNote))

		// 解压单篇为知笔记文件
		wizDocument := filepath.Join(localPath, DocumentLocation, DocumentName)
		err = gulu.Zip.Unzip(wizDocument, unzipPath)
		if nil != err {
			logging.LogErrorf("解压笔记[%s]错误:%s", wizNote, err)
			failed += 1
			continue
		}
		data, err := os.ReadFile(filepath.Join(unzipPath, "index.html"))
		if nil != err {
			logging.LogErrorf("读取笔记[%s]html文件失败: %s", wizNote, err)
			failed += 1
			continue
		}
		// 将data转换为utf8编码
		data, err = io.ReadAll(transform.NewReader(bytes.NewReader(data), unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
		if err != nil {
			logging.LogErrorf("解码笔记[%s]失败:%s", wizNote, err)
			failed += 1
			continue
		}

		// 根据笔记类型DocumentType值按条件处理笔记内容
		if DocumentType != nil && *DocumentType == "lite/markdown" {
			markdown := regexp.MustCompile(`(?i)(?s)<pre>(.*?)</pre>`).FindStringSubmatch(string(data))
			if len(markdown) > 1 {
				markdownStr = markdown[1]
			}
		} else {
			args := []string{
				"-f", "html+tex_math_dollars",
				"-t", "gfm-raw_html",
				"-L", luaPath,
				/*"-o", markdownPath, // 注释此行输出到标准输出*/
				"--wrap=none"}
			pandoc := exec.Command(Conf.Export.PandocBin, args...)
			gulu.CmdAttr(pandoc)
			pandoc.Stdin = bytes.NewBuffer(data)
			output, err := pandoc.Output()
			if nil != err {
				logging.LogErrorf("转换笔记[%s]格式失败:%s", wizNote, err)
				failed += 1
				continue
			}
			markdownStr = string(output)
		}
		markdownStr = strings.TrimSpace(markdownStr)
		// 修改带附件笔记
		if DocumentAttachmentCount > 0 {
			attachmentDir := strings.Replace(DocumentName, ".ziw", "_Attachments", 1)
			err = gulu.File.CopyDir(filepath.Join(localPath, DocumentLocation, attachmentDir), filepath.Join(unzipPath, attachmentDir))
			if err != nil {
				logging.LogErrorf("复制笔记[%s]附件失败:%s", wizNote, err)
				incomplete = 1
			}
			// 处理插入文档的附件
			attachmentSrc := `wiz://open_attachment\?guid=[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`
			SrcIndex := []int{0, 0}
			attachmentName := ""
			for {
				SrcIndex := regexp.MustCompile(attachmentSrc).FindStringIndex(markdownStr[SrcIndex[1]:])
				if SrcIndex == nil {
					break
				}
				guid := markdownStr[SrcIndex[0]+len("wiz://open_attachment?guid=") : SrcIndex[1]]
				attachmentRow, err := wizdb.Query("SELECT ATTACHMENT_NAME FROM WIZ_DOCUMENT_ATTACHMENT WHERE ATTACHMENT_GUID = ?", guid)
				if err != nil {
					logging.LogErrorf("查询笔记[%s]附件名称失败:%s", wizNote, err)
					incomplete = 1
				} else {
					if attachmentRow.Next() {
						err = attachmentRow.Scan(&attachmentName)
						if err != nil {
							logging.LogErrorf("读取笔记[%s]附件名称失败:%s", wizNote, err)
							incomplete = 1
						} else {
							attachmentSrc := filepath.Join(attachmentDir, attachmentName)
							markdownStr = markdownStr[:SrcIndex[0]] + attachmentSrc + markdownStr[SrcIndex[1]:]
						}
					}
					attachmentRow.Close()
				}
			}
			// 将附件附在笔记最后
			attachments, err := os.ReadDir(filepath.Join(unzipPath, attachmentDir))
			if err != nil {
				logging.LogErrorf("读取笔记[%s]附件文件夹失败:%s", wizNote, err)
				incomplete = 1
			} else {
				if len(attachments) > 0 {
					markdownStr = markdownStr + "\n---\n" + "# 附件:\n"
					for _, attachment := range attachments {
						markdownStr = markdownStr + "[" + attachment.Name() + "]" + "(" + path.Join(attachmentDir, attachment.Name()) + ")\n"
					}
					markdownStr = markdownStr + "---"
				}
			}
		}
		// 处理带tag的笔记
		var tagName string
		var tag string
		tagRow, err := wizdb.Query("SELECT t2.TAG_NAME FROM WIZ_DOCUMENT_TAG AS t1 JOIN WIZ_TAG AS t2 ON t1.TAG_GUID = t2.TAG_GUID WHERE DOCUMENT_GUID = ?", DocumentGuid)
		if err != nil {
			logging.LogErrorf("查询笔记[%s]tag失败:%s", wizNote, err)
			incomplete = 1
		} else {
			for tagRow.Next() {
				err = tagRow.Scan(&tagName)
				if err != nil {
					logging.LogErrorf("读取笔记[%s]tag信息失败: %s", wizNote, err)
					incomplete = 1
				}
				if tagName != "" {
					tag = tag + "#" + tagName + "#" + " "
				}
			}
			tagRow.Close()
		}
		if tag != "" {
			markdownStr = "---\n标签:" + tag + "\n---\n" + markdownStr
		}
		// 修改剪藏笔记,如果DocumentUrl指向的值以http开头,则在笔记前添加剪藏信息
		if DocumentUrl != nil && strings.HasPrefix(*DocumentUrl, "http") {
			clipInfo := fmt.Sprintf("---\n\n* 使用为知笔记剪藏自:\n* [%s](%s)\n* %s\n\n---\n", *DocumentUrl, *DocumentUrl, DtCreated)
			markdownStr = clipInfo + markdownStr
		}
		// 创建语法树
		tree := parseStdMd([]byte(markdownStr))
		if nil == tree {
			logging.LogErrorf("解析笔记[%s]MD文件失败:%s", wizNote, err)
			failed += 1
			continue
		}
		id := wizDate(DtCreated) + "-" + randStr(7)
		targetPath = strings.ReplaceAll((path.Join(targetPaths[DocumentLocation], id+".sy")), ".sy/", "/")
		tree.ID = id
		tree.Root.ID = id
		tree.Root.SetIALAttr("id", tree.Root.ID)
		tree.Root.SetIALAttr("title", DocumentTitle)
		tree.Root.SetIALAttr("updated", wizDate(DtModified))
		tree.Box = boxID
		tree.Path = targetPath
		tree.HPath = path.Join(baseHPath, DocumentLocation, DocumentTitle)
		tree.Root.Spec = "1"

		boxLocalPath := filepath.Join(util.DataDir, boxID)
		docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
		assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
		assetName := map[string]string{}
		ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
			if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a")) {
				return ast.WalkContinue
			}

			var dest string
			if ast.NodeLinkDest == n.Type {
				dest = n.TokensStr()
			} else {
				dest = n.TextMarkAHref
			}

			if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
				processBase64Img(n, dest, assetDirPath, err)
				return ast.WalkContinue
			}

			dest = strings.ReplaceAll(dest, "%20", " ")
			dest = strings.ReplaceAll(dest, "%5C", "/")
			if ast.NodeLinkDest == n.Type {
				n.Tokens = []byte(dest)
			} else {
				n.TextMarkAHref = dest
			}
			if !util.IsRelativePath(dest) {
				return ast.WalkContinue
			}
			dest = filepath.ToSlash(dest)
			if dest == "" {
				return ast.WalkContinue
			}

			absolutePath := filepath.Join(unzipPath, dest)
			exist := gulu.File.IsExist(absolutePath)
			if !exist {
				absolutePath = filepath.Join(unzipPath, string(html.DecodeDestination([]byte(dest))))
				exist = gulu.File.IsExist(absolutePath)
			}
			if exist {
				// 去重复制
				var name string
				if assetName[absolutePath] == "" {
					name = filepath.Base(absolutePath)
					name = util.AssetName(name)
					assetName[absolutePath] = name
					assetTargetPath := filepath.Join(assetDirPath, name)
					if err = filelock.Copy(absolutePath, assetTargetPath); nil != err {
						logging.LogErrorf("复制笔记[%s]资源文件失败:%s", wizNote, err)
						return ast.WalkContinue
					}
				} else {
					name = assetName[absolutePath]
				}
				if ast.NodeLinkDest == n.Type {
					n.Tokens = []byte("assets/" + name)
				} else {
					n.TextMarkAHref = "assets/" + name
				}
			}
			return ast.WalkContinue
		})

		importTrees = append(importTrees, tree)
		// 写入笔记
		if 0 < len(importTrees) {
			initSearchLinks()
			convertWikiLinksAndTags()
			buildBlockRefInText()
			for _, tree := range importTrees {
				err = indexWriteTreeIndexQueue(tree)
				if nil != err {
					logging.LogErrorf("写入笔记[%s]文件失败:%s", wizNote, err)
					failed += 1
					continue
				}
			}
		}
		succeed += 1
		failed += incomplete
		// 清理资源
		os.RemoveAll(unzipPath)
		importTrees = []*parse.Tree{}
		searchLinks = map[string]string{}
	}
	if gulu.File.IsExist(unzipPath) {
		os.RemoveAll(unzipPath)
	}
	IncSync()
	debug.FreeOSMemory()
	if failed == 0 {
		util.PushMsg(fmt.Sprintf("成功导入所有有效为知笔记,共%d条。", succeed), 15000)
	} else {
		return fmt.Errorf("成功导入有效为知笔记%d条,失败%d条,详情请查看日志。", succeed, failed)
	}
	return nil
}

添加转换为知笔记数据库中时间元数据为思源笔记时间格式的函数:

// 魔改,转换为知笔记中的时间变量
func wizDate(wiz_time string) string {
	t, err := time.Parse("2006-01-02 15:04:05", wiz_time)
	if err == nil {
		return t.Format("20060102150405")
	} else {
		return time.Now().Format("20060102150405")
	}
}

在功能函数 func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) 中的

	var baseHPath, baseTargetPath, boxLocalPath string
	if "/" == toPath {
		baseHPath = "/"
		baseTargetPath = "/"
	} else {
		block := treenode.GetBlockTreeRootByPath(boxID, toPath)
		if nil == block {
			logging.LogErrorf("not found block by path [%s]", toPath)
			return nil
		}
		baseHPath = block.HPath
		baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
	}
	boxLocalPath = filepath.Join(util.DataDir, boxID)

下方插入

	// 魔改:导入为知笔记
	err = ImportFromWiz(boxID, localPath, baseHPath, baseTargetPath)
	if nil != err {
		return err
	}

4、后话

迁移方案中使用 Pandoc 转换笔记格式,如果对转换效果不满意,可以手动创建 lua 过滤器 pandoc_script.lua,位于思源笔记数据文件夹的 temp 文件夹下,魔改核心不会修改和删除它。

参考

Github 思源笔记

siyuan-note/siyuan: A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (github.com)

Pandoc 参考手册

Pandoc - Pandoc User’s Guide

Pandoc lua 过滤器参考手册

Pandoc - Pandoc Lua Filters

ChatGPT

ChatGPT

通义千问

通义 (aliyun.com)

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    23007 引用 • 92546 回帖
2 操作
gama 在 2024-07-17 16:41:25 更新了该帖
gama 在 2024-07-17 15:51:32 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • China-yuqin

    请问 思源可以和为知经典版一样,在文件夹内建立二级文件夹么? 我看好像都是只有一个文件夹再建的都是文档了呀?都是文档嵌套

  • 其他回帖
  • 成本很高. 会丢东西.

    我 5000+ 的东西转到思源,鬼知道丢了什么.

    反正附件基本全丢了.

    主要是 wis 的 导出不行. 他的 wisx 跟老版本都不兼容, 更不好导出.

    我一看情况不对, 赶快跑了

  • qyf127

    为知会员还有两年,一直觉得挺好用的,可惜感觉要跑路了

  • AchatesRay

    我也是从为知迁移过来,还有 2 年的会员放弃了。