news 2026/9/14 17:42:56

WeKan 外部工具迁移实战:NextCloud Deck、OpenProject、GitHub、GitLab、Gitea、Forgejo 的导入与导出机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WeKan 外部工具迁移实战:NextCloud Deck、OpenProject、GitHub、GitLab、Gitea、Forgejo 的导入与导出机制

WeKan 外部工具迁移实战:NextCloud Deck、OpenProject、GitHub、GitLab、Gitea、Forgejo 的导入与导出机制

【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan

WeKan 通过一套通用的"解析器 + 格式化器"架构,支持从 NextCloud Deck、OpenProject、GitHub、GitLab、Gitea、Forgejo、Asana、ZenKit 等工具导入看板,也支持反向导出为这些工具的原生 JSON 结构。本文基于 外部工具导入导出文档 与仓库源码,完整梳理每种工具的字段映射规则、损失报告机制、REST API 端点和 api.py 命令行脚本的用法,读完你可以用界面或脚本完成单看板的批量迁移。

支持的工具与 JSON 形态对照

导入入口在界面菜单All Boards → New → Import,导出入口在Board Settings → Export。每种源工具有一个小型解析器(parser),负责把该工具的 JSON 归一化为通用形状;每个目标格式有一个格式化器(formatter),负责输出该工具的 JSON。两者共享同一个导入引擎和同一个导出收集器。原文档给出的各工具 JSON 形态如下:

工具导入时粘贴的 JSON 形态导出格式
Trello看板导出 JSON(来自 Trello 的.json{ name, lists:[…], cards:[…], labels:[…] }
Jiraissue 搜索 JSON(GET /rest/api/2/search{ board:{name}, issues:[{key, fields:{summary,status,labels}}] }
NextCloud Deckstacks的看板(每个 stack 携带cards{ title, stacks:[{title, cards:[…]}] }
OpenProjectwork-packages 集合(GET /api/v3/work_packages{ _embedded:{ elements:[{subject, _links:{status}}] } }
GitHubissues 数组(GET /repos/OWNER/REPO/issuesissues 数组[{title, body, state, labels}]
GitLabissues 数组(GET /projects/ID/issuesissues 数组[{title, description, state, labels}]
Giteaissues 数组(GET /repos/OWNER/REPO/issuesissues 数组(与 GitHub 相似)
Forgejoissues 数组(与 Gitea 相同 API)issues 数组(与 GitHub 相似)
Asana任务导出{ data:[{name, notes, memberships:[{section}], tags, due_on}] }{ data:[{name, notes, completed, due_on, memberships, tags}] }
ZenKit列表导出{ title, stages:[{name}], items:[{title, description, stage_name, due, tags}] }{ title, stages:[…], items:[…] }

从源码结构看,api.py的语法说明还列出kanboardcsvexcelwekanmarkdown等源/格式,覆盖范围比上表更宽:导入源为trello/wekan/csv/jira/kanboard/excel/deck/openproject/github/gitlab/gitea/forgejo/asana/zenkit,导出格式为kanboard/trello/jira/deck/openproject/github/gitlab/gitea/forgejo/asana/zenkit(见 api.py 的语法段)。

通用架构:解析器归一化 + 格式化器发射

导入侧的所有解析器集中在 models/lib/externalParsers.js。每个解析器把源 JSON 归一化为统一的 "Kanboard 形状":

// 归一化后的任务形状(来自 externalParsers.js 文件头注释) { title, description, column_name, swimlane_name, date_due, owner_username, tags: [string] } // 以及看板级: { board: { name }, columns: [{title}], swimlanes: [{name}], tasks: [...] }

解析器按源名注册在EXTERNAL_PARSERS映射表中(externalParsers.js#L356-L367):deckopenprojectgithubgitlabgiteaforgejoasanazenkitmarkdownjira。注意 Gitea 与 Forgejo 共用同一个解析器parseGitea,因为它们共享相同的 issue API 形状。

导出侧集中在 models/lib/externalExporters.js:一个共享的collect()函数先把 WeKan 看板(列表、泳道、卡片、标签)收集成中性中间结构,再由formatters映射表中对应格式的格式化器发射目标 JSON(externalExporters.js#L63-L166)。这正好对应文档说的"一个导入引擎 + 一个导出收集器"。

各工具的导入字段映射

NextCloud Deck:stacks 到列表

parseNextcloudDeck(externalParsers.js#L15-L44)接受 Deck 看板对象(Deck REST APIGET /boards/{id}+/stacks的返回形态),映射规则:

  • stacks → 列表stacks数组逐个映射为columns
  • card → 卡片card.title→ 标题,card.description→ 描述;
  • labels → 标签card.labels(字符串或{title}对象均可)归一为tags
  • assigned user → 卡片成员:取card.assignedUsers[0],优先participant.uid,回退uid,再回退card.owner
  • due datecard.duedatecard.dueDatedate_due

OpenProject:statuses 到列表

parseOpenProject(externalParsers.js#L50-L73)接受 work-packages 集合(GET /api/v3/work_packages{ _embedded: { elements: [...] } }形态,也兼容裸数组):

  • work package → 卡片subject(或name)→ 标题;description.raw(回退description.html)→ 描述;
  • status → 列表_links.status.title作为column_name,所有出现过的 status 去重后生成columns
  • due datedueDatedue_date
  • assignee_links.assignee.titleowner_username
  • type → 标签_links.type.title作为唯一的 tag。

GitHub / Gitea / Forgejo:共享的 issue 解析器

三个工具共用parseIssuesArray(externalParsers.js#L92-L163),行为如下:

  • 接受 issues 数组(GET /repos/{o}/{r}/issues),也接受把多页分页结果拼接成的数组——源码注释明确指出"补全分页是 API 客户端的事,不是解析器的事",它只处理拿到的 JSON;
  • pull request 被跳过issues.filter(issue => !issue.pull_request)
  • issue state → Open / Closed 列表state === 'closed'Closed,否则进Open,两个列表固定生成;
  • labels → 标签;milestone 追加为milestone:<title>标签;
  • assignee → 卡片成员:只取第一个 assignee(loginusername)成为 task Owner;多余 assignee 以assignee:<login>标签保留,并写入unsupported损失记录;
  • state_reason:非completed的 state reason 以state_reason:<reason>标签保留;
  • 来源追溯:卡片描述末尾自动追加Source: #<number>和 issue 的html_url,便于回查原始 issue;
  • 评论:若导出内嵌了comments_data数组,则以Comments:段落追加进描述;若只有comments计数而无内嵌数据,则记录 unsupported 说明"上游存在 N 条评论但未嵌入本次导出";
  • externalId:issue number 作为同步匹配键(详见下文"周期性同步");
  • requested_by:issue 创建者(user.login/author)映射为"WeKan 的 Requested By",即使该用户在本看板没有账号也能以纯文本保留。

GitLab 由独立的parseGitlab(externalParsers.js#L177-L196)处理:GitLab 的 state 是opened/closed(而非 GitHub 的open/closed),labels 是字符串数组,assignee 用username字段,同步键取iid

文档中"Member mapping is skipped for these (map members afterwards)"的说法对应的正是这一机制:issue 的 assignee 被解析为owner_username自由文本,而不是直接授权板成员——导入不会仅仅因为源文件里出现了一个用户名就授予板访问权限(这也是 Format-Coverage 契约 中明确的安全约束),因此导入后需要在界面上手动把成员映射到实际的板成员。

Asana 与 ZenKit

  • parseAsana(externalParsers.js#L201-L224):memberships[0].section.name决定列表;无 section 时按completed归入Done/In Progressdue_on/due_at→ 截止日;assignee.emailassignee.name→ Owner;tags归一为标签。
  • parseZenkit(externalParsers.js#L229-L248):items中每项的stage_name(或stageName/list)→ 列表,缺省Inbox;顶层stages若存在则优先生成列,否则从任务推导。

导出:WeKan 看板到工具 JSON

导出由buildExternalExport完成(externalExporters.js#L170-L177):collect()读取看板的未归档列表、泳道、卡片(按sort排序)及标签名,构建中间结构后交给格式化器。关键规则:

"已完成"列表的判定isClosed用正则/done|closed|complete|archiv|finished/i匹配列表名(externalExporters.js#L50-L52)。列表名命中 Done/Closed/Complete/Archived 等"终结性"词汇时,其中的卡片导出为 closed 状态的 issue——这就是原文档"where a list namedDone/Closed/Complete/Archivedmaps to a closed issue"的实现。

各格式化器的输出形状(externalExporters.js#L63-L166):

  • github/gitea/forgejo共用githubLike{ title, body, state: 'open'|'closed', labels: [{name}], due_date }
  • gitlabstate'opened'/'closed',labels 为字符串数组;
  • deck{ title, stacks: [{title, cards: [{title, description, duedate, labels:[{title}]}]}] }
  • openproject{ _embedded: { elements: [{subject, description:{raw}, dueDate, _links:{status:{title}}}] } }
  • asana{ data: [{name, notes, completed, due_on, memberships:[{section:{name}}], tags:[{name}]}] }
  • zenkit{ title, stages:[{name}], items:[{title, description, stage_name, due, tags}] }
  • trello:完整看板 JSON(name/prefs/lists/cards/labels/checklists/actions),可与 WeKan 的 Trello 导入往返;
  • jira{ board:{name}, issues:[{key: 'WEKAN-<n>', fields:{summary, description, status:{name}, labels, duedate}}] },与 WeKan 的 Jira 导入往返。

导出结果最后统一经过/server/lib/secureTransfer的出站校验(非有限数字、非法日期、不安全 URL、意外密钥、循环引用等),即使数据库行早于当前导入校验逻辑存在也能兜底。

损失报告:{ normalized, warnings, unsupported }契约

解析器从不静默丢弃源字段。以 issue 解析器为例,归一化形状之外的信息(第二个 assignee、state reason、未内嵌的评论数)会作为顶层的warnings/unsupported键附加在结果上——这是 Format-Coverage 设计 规定的{ normalized, warnings, unsupported }契约,unsupported携带有界 JSON-pointer 风格路径和原因(不含机密值),最终在 WeKan 的 Problems → Recovery 界面展示。因此一个带 unsupported 字段的导入结果是completed-with-warnings,而非静默的completed

导出侧同理:当目标工具没有 WeKan 某字段的等价物时,格式化器在 JSON 允许的位置输出x-wekan扩展块,并把字段记入_wekan.losses;消费者可忽略扩展,而 WeKan 后续导入会利用它们恢复无损往返。字段级完整覆盖矩阵(每种格式的权威形状、必须覆盖的字段、验证矩阵)以 Format-Coverage.md 为合同,它声明"文档声称的映射若不存在于代码中,字段清单测试必须失败"。

REST API 端点

导入:POST /api/boards/import/:source

服务端路由在 server/models/boards.js#L1063-L1081。要点:

  • :source取值trellowekancsvjirakanboardexceldeckopenprojectgithubgitlabgiteaforgejoasanazenkit
  • 请求体是该工具的导出 JSON,直接发送或包成{ "board": <export> }均可;
  • 可选membersMapping{ 源userId: 本地userId })用于成员映射;
  • 路由内部复用与 UI 相同的importBoardMeteor method,保证"一种鉴权、校验、净化与超时边界覆盖所有传输通道";成功返回{ _id: 新看板ID }

导出:GET /api/boards/:boardId/export/:format

统一处理函数serveExternalExport在 models/export.js#L428-L479:每个格式一条路由、共享一个鉴权处理器;公共看板可匿名导出,私有看板通过登录态或?authToken=<token>(登录 token,超长会 400)鉴权,最终权限由exporter.canExport()按看板可见性判定。支持fields查询参数做导出选择(只导出 description/labels/dates 中指定部分)。除 Markdown 格式以text/markdown直接返回纯文本外,其余格式均返回 JSON。

用 api.py 脚本批量迁移

两个方向都可通过 REST API 脚本化,因此可以批量迁移所有看板。仓库根目录的 api.py 是配套的 Python CLI:

# 从某工具的导出文件导入 (SOURCE = deck/openproject/github/gitlab/gitea/forgejo/asana/zenkit/trello/jira/…) python3 api.py importboardfrom github issues.json # → POST /api/boards/import/github (body: 该工具的导出 JSON) # 把看板导出为某工具的 JSON 形状 (FORMAT = trello/jira/deck/openproject/github/gitlab/gitea/forgejo/asana/zenkit/kanboard) python3 api.py exportboardformat BOARDID deck deck-board.json # → GET /api/boards/:boardId/export/deck?authToken=:token

importboardfrom的实现(api.py#L2070-L2078)读取本地 JSON 文件后以{"board": <内容>}包装 POST 到import/<source>exportboardformat(api.py#L2080-L2089)GET 对应格式路由并把响应写入输出文件。

使用前需要修改脚本顶部的 SETTINGS 段(api.py#L172-L183):

# Username is your Wekan username or email address. # OIDC/OAuth2 etc uses email address as username. username = 'testtest' password = 'testtest' wekanurl = 'http://localhost:4000/'

脚本启动时先POST users/login换取 token,之后所有请求以Authorization: Bearer <token>头携带。注意它依赖requests库,且凭据以明文写在脚本中,生产环境使用时请自行管理权限。

周期性同步:导入之外的增量能力

EXTERNAL_PARSERS中有一个刻意更小的子集SYNC_CAPABLE_SOURCES = ['jira', 'github', 'gitlab', 'gitea', 'forgejo'](externalParsers.js#L369-L374)。这些解析器会在归一化任务上输出externalId(issue key / number / iid),使models/lib/listSyncReconcile.js能把再次抓取到的 issue 匹配回已创建的卡片,从而支持周期性同步(配合server/listSync.js与 models/listSyncCredentials.js);deck/openproject/asana/zenkit/markdown 解析器尚未输出externalId,源码注释明确说明若加入同步列表会导致每次同步都重复建卡,故有意排除。

相关文档

  • Kanboard 导入导出
  • Jira 迁移
  • Trello 迁移
  • CSV/TSV 导入导出
  • Excel 导入导出
  • 从另一 WeKan 迁移所有看板

【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 17:40:49

random seq

我建议 8bit,先用前6位,后两位以后留给 error/BUSY: localparam bit [7:0] RAND_ADDR = 8b0000_0001; localparam bit [7:0] RAND_DATA = 8b0000_0010; localparam bit [7:0] RAND_SIZE = 8b0000_0100; localparam bit [7:0] RAND_BURST = 8b0000_1000; localparam bit …

作者头像 李华
网站建设 2026/9/14 17:39:47

PLC配方功能块设计与工业自动化优化实践

1. 为什么需要告别触摸屏宏&#xff1f;在工业自动化领域&#xff0c;触摸屏&#xff08;HMI&#xff09;与PLC的交互方式一直是个值得深入探讨的话题。传统方案中&#xff0c;很多工程师习惯在触摸屏上编写宏指令来处理配方管理功能&#xff0c;比如通过威纶通触摸屏的"元…

作者头像 李华
网站建设 2026/9/14 17:39:11

Scrapy-Redis分布式爬虫实战与优化技巧

1. Scrapy框架与分布式爬虫基础解析 Scrapy作为Python生态中最成熟的爬虫框架之一&#xff0c;其架构设计充分考虑了大规模数据采集的需求。框架内置的Twisted异步网络库引擎&#xff0c;使得单个爬虫实例就能高效处理数百个并发请求。但当我们面对千万级页面抓取任务时&#x…

作者头像 李华
网站建设 2026/9/14 17:36:25

别空谈发酵与分子实验!生物工程开题报告写作干货,思梦航 AI 帮你锁定研究创新点## 开篇引流钩子

生物工程专业的开题&#xff0c;一边泡实验室做培养、跑电泳&#xff0c;一边还要查阅海量中外文献&#xff0c;时间十分紧张。 不少同学熬很久写完开题&#xff0c;交到导师手里直接驳回&#xff1a;文献综述简单堆砌分子生物学、发酵工程相关研究&#xff0c;实验方案存在漏洞…

作者头像 李华