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:[…] } |
| Jira | issue 搜索 JSON(GET /rest/api/2/search) | { board:{name}, issues:[{key, fields:{summary,status,labels}}] } |
| NextCloud Deck | 带stacks的看板(每个 stack 携带cards) | { title, stacks:[{title, cards:[…]}] } |
| OpenProject | work-packages 集合(GET /api/v3/work_packages) | { _embedded:{ elements:[{subject, _links:{status}}] } } |
| GitHub | issues 数组(GET /repos/OWNER/REPO/issues) | issues 数组[{title, body, state, labels}] |
| GitLab | issues 数组(GET /projects/ID/issues) | issues 数组[{title, description, state, labels}] |
| Gitea | issues 数组(GET /repos/OWNER/REPO/issues) | issues 数组(与 GitHub 相似) |
| Forgejo | issues 数组(与 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的语法说明还列出kanboard、csv、excel、wekan、markdown等源/格式,覆盖范围比上表更宽:导入源为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):deck、openproject、github、gitlab、gitea、forgejo、asana、zenkit、markdown、jira。注意 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 date:
card.duedate或card.dueDate→date_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 date:
dueDate或due_date; - assignee:
_links.assignee.title→owner_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(
login或username)成为 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 Progress;due_on/due_at→ 截止日;assignee.email或assignee.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 };gitlab:state用'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取值trello、wekan、csv、jira、kanboard、excel、deck、openproject、github、gitlab、gitea、forgejo、asana、zenkit;- 请求体是该工具的导出 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=:tokenimportboardfrom的实现(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),仅供参考