news 2026/9/13 23:21:21

Super Productivity 问题集成开发指南:以 Issue-Provider 插件方式接入新外部系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Super Productivity 问题集成开发指南:以 Issue-Provider 插件方式接入新外部系统

Super Productivity 问题集成开发指南:以 Issue-Provider 插件方式接入新外部系统

【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity

导读

Super Productivity 为 Jira、GitLab、GitHub、Open Project 等外部系统提供了任务集成能力。随着插件系统成熟,新增外部 Issue 与日历集成的官方推荐路径已经统一为issue-provider 插件:将 provider 专属的配置、API 调用与数据映射逻辑封装在插件包中,宿主以plugin:<plugin-id>的形式分配 provider key,核心应用保持零侵入。本文基于仓库内 docs/add-new-integration.md 这一官方集成指南,结合 issue-provider 类型定义 与仓库内已打包的 GitHub、Google Calendar、CalDAV provider 实现,完整讲解从创建插件包、声明 manifest、实现IssueProviderPluginDefinition契约、处理 OAuth 与密钥,到打包进仓库、运行验证的整个流程。读完本文,你可以独立开发一个新的 issue-provider 插件,或为已有集成维护、扩展能力。

1. 为什么新集成必须是插件,而不是内置 Provider

原文档首先明确了架构决策:新的外部 Issue 与日历集成都应做成 issue-provider 插件,除非维护者批准了插件 API 无法满足的核心专属需求,否则不得再向src/app/features/issue/providers/添加新的内置 provider

这一约束的动机在文档末尾的 "Legacy core providers" 一节说得非常清楚:向核心添加 provider 会带来永久性的联合类型(unions)、配置状态、表单、数据迁移与同步兼容性负担。而插件方案把 provider 专属的一切隔离在包内,宿主只通过稳定的plugin:<plugin-id>key 与它通信。社区上传的插件不需要核心注册,天然保持该 key;仓库托管的插件即便打包进核心发布,也遵循同样的契约。

需要为既有内置 provider 修 bug 时,仍走其原有目录与测试;新集成则一律走插件路径。

2. 权威契约与参考实现

开发前先锁定两处权威定义,它们是事实标准(可能比任何二手文档更新):

  • packages/plugin-api/src/issue-provider-types.ts:IssueProviderPluginDefinitionPluginSearchResultPluginIssuePluginHttpPluginFieldMappingPluginFormField等 issue-provider 专属类型。
  • packages/plugin-api/src/types.ts:通用PluginManifestOAuthFlowConfigPluginAPI接口以及任务/项目/标签等共享模型。

仓库内三个已打包 provider 分别示范了不同侧重点,是"照着抄"的最佳范本:

参考实现目录侧重示范
GitHubpackages/plugin-dev/github-issue-provider/搜索、评论、backlog 导入、字段同步(fieldMappings)
Google Calendarpackages/plugin-dev/google-calendar-provider/OAuth 流程、议程(agenda)字段、日历事件写回
CalDAVpackages/plugin-dev/caldav-calendar-provider/文本响应(iCal/XML)、非标准 HTTP 动词(PROPFIND/REPORT)、经批准的私有网络 provider

通用的插件打包、UI、权限与安全知识,参考 docs/plugin-development.md;面向用户的问题集成管理,见 docs/wiki/2.17-Add-a-New-Issue-Integration.md。

3. 创建插件包:目录结构与最小 Manifest

仓库托管的 provider 通常位于packages/plugin-dev/<provider-name>/,标准结构如下:

<provider-name>/ ├── package.json ├── scripts/build.js ├── src/ │ ├── manifest.json │ ├── plugin.ts │ └── icon.svg └── *.spec.ts

约定非常明确:provider 的 API 类型与映射逻辑全部留在包内,不得把 provider 加入核心的 issue-provider 联合类型、默认值、表单或 Angular 服务中。以 GitHub provider 的 package.json 为参照,它依赖@super-productivity/plugin-api(本地file:../../plugin-api),提供buildtypechecklint三个脚本,构建走scripts/build.js(esbuild 打包)。

3.1 最小 Manifest 逐字段解析

原文档给出的最小 manifest 如下,它定义了插件的身份、宿主兼容性、权限与 issue-provider 专属配置:

{ "id": "example-issue-provider", "name": "Example Issues", "version": "1.0.0", "manifestVersion": 1, "minSupVersion": "18.0.0", "description": "Connects Example issues to Super Productivity", "type": "issueProvider", "icon": "icon.svg", "iFrame": false, "permissions": ["http"], "hooks": [], "issueProvider": { "pollIntervalMs": 600000, "icon": "extension", "humanReadableName": "Example", "issueStrings": { "singular": "Issue", "plural": "Issues" } } }

结合 types.ts 中的PluginManifestIssueProviderManifestConfig,各字段说明如下:

  • 顶层字段hookspermissions是必需数组(不用就写[]);description可选;icon是相对插件根目录的 SVG 路径;iFrame: false表示这是纯宿主侧逻辑插件(不需要 iframe UI)。
  • type:取'issueProvider',区别于普通'standard'插件。
  • permissions"http"是网络出站能力的显式开关。GitHub provider 只声明了["http"];Google Calendar 声明["oauth", "http"](见其 manifest.json);CalDAV 同样["http"],但额外开启allowPrivateNetwork(见其 manifest.json)。
  • issueProvider.pollIntervalMs:轮询间隔,GitHub 用600000(10 分钟),两个日历 provider 用60000(1 分钟)。
  • issueProvider.icon:UI 中使用的 Material 图标名(如githubcalendarextension),并非 SVG 文件。
  • issueProvider.humanReadableName:UI 芯片与标签中的短名称,缺省时回退到插件名。
  • issueProvider.issueStrings:单复数显示文案。
  • issueProvider.useAgendaView:日历类 provider 设为true,改用日程议程视图而非搜索列表;Google Calendar 与 CalDAV 均如此。
  • issueProvider.defaultAutoAddToBacklog:创建该 provider 时是否默认开启新 issue 自动导入 backlog。
  • issueProvider.allowPrivateNetwork:默认false,SSRF 防护会拦截私有 IP 与 localhost;仅对受信任的内置插件生效,只有自托管且确实需要的 provider 才应开启。

3.2 关于issueProviderKey的重要约定

原文档特别强调:新 provider 不要写issueProvider.issueProviderKey,宿主会自动分配plugin:<plugin-id>作为 provider key。该字段是预留的,只有仓库托管的、需要从既有内置 key(如GITHUB)迁移并继承已持久化配置的插件才使用。GitHub provider 的 manifest.json 中就有"issueProviderKey": "GITHUB"——这正是它从前内置 provider 迁移而来的标志;对照 Google Calendar 与 CalDAV 的 manifest 均无此字段,因为它们是新插件,使用plugin:google-calendar-provider/plugin:caldav-calendar-provider这样的 key。

4. 注册 Provider:IssueProviderPluginDefinition契约实现

IssueProviderPluginDefinition是基于 Promise 的接口,实现时应精确对照当前类型定义,而不是把方法清单抄进插件。先看其完整签名(issue-provider-types.ts):

export interface IssueProviderPluginDefinition { configFields: PluginFormField[]; getHeaders(config: Record<string, unknown>): Record<string, string> | Promise<Record<string, string>>; searchIssues(searchTerm: string, config: Record<string, unknown>, http: PluginHttp): Promise<PluginSearchResult[]>; getById(issueId: string, config: Record<string, unknown>, http: PluginHttp): Promise<PluginIssue>; getIssueLink(issueId: string, config: Record<string, unknown>): string; testConnection?(config: Record<string, unknown>, http: PluginHttp): Promise<boolean>; getNewIssuesForBacklog?(config: Record<string, unknown>, http: PluginHttp): Promise<PluginSearchResult[]>; issueDisplay: PluginIssueField[]; commentsConfig?: PluginCommentsConfig; fieldMappings?: PluginFieldMapping[]; updateIssue?(...): Promise<void>; createIssue?(...): Promise<{ issueId: string; issueNumber?: number; issueData: PluginIssue }>; extractSyncValues?(issue: PluginIssue): Record<string, unknown>; deleteIssue?(...): Promise<void>; deletedStates?: string[]; timeBlock?: { upsertEvent(...): Promise<void>; deleteEvent(...): Promise<void> }; }

必选契约只有六项:configFieldsgetHeaderssearchIssuesgetByIdgetIssueLinkissueDisplay。其余全部是可选的(connection testing、comments、backlog import、field mappings、create/update/delete、calendar time-block),只声明 provider 实际支持的能力

4.1 最小可运行注册示例

原文档的完整示例(配置字段 + 请求头 + 搜索 + 详情 + 链接 + 展示)如下:

import type { IssueProviderPluginDefinition, PluginHttp, PluginIssue, PluginSearchResult, } from '@super-productivity/plugin-api'; declare const PluginAPI: { registerIssueProvider(definition: IssueProviderPluginDefinition): void; }; const API = 'https://api.example.com'; PluginAPI.registerIssueProvider({ configFields: [ { key: 'workspace', type: 'input', label: 'Workspace', required: true, }, ], getHeaders(): Record<string, string> { return { Accept: 'application/json' }; }, async searchIssues( searchTerm: string, config: Record<string, unknown>, http: PluginHttp, ): Promise<PluginSearchResult[]> { const workspace = String(config.workspace); return http.get<PluginSearchResult[]>(`${API}/workspaces/${workspace}/issues`, { params: { query: searchTerm }, }); }, async getById( issueId: string, _config: Record<string, unknown>, http: PluginHttp, ): Promise<PluginIssue> { return http.get<PluginIssue>(`${API}/issues/${encodeURIComponent(issueId)}`); }, getIssueLink(issueId: string): string { return `https://example.com/issues/${encodeURIComponent(issueId)}`; }, issueDisplay: [ { field: 'title', label: 'Title', type: 'link', linkField: 'url' }, { field: 'state', label: 'State', type: 'text' }, { field: 'body', label: 'Description', type: 'markdown', hideEmpty: true }, ], });

其中config就是用户在配置表单里填写的值对象,searchIssuesgetById通过传入的http参数(PluginHttp)发起请求。

4.2 配置字段类型(PluginFormField

configFields的元素类型PluginFormField支持多种形态(issue-provider-types.ts):

  • type可取'input' | 'password' | 'textarea' | 'checkbox' | 'select' | 'multiSelect' | 'link' | 'oauthButton'
  • 公共属性:key(存进 config 的键)、labelrequireddescription(字段下方帮助文本)、pattern(输入校验正则)、advanced(折叠进"高级配置"区)、showIf(仅当指定 config key 为真时显示);
  • link类型配url,用于展示"如何获取 Token"之类的帮助链接——GitHub provider 的tokenHelp字段正是这种用法;
  • oauthButton类型配oauthConfigOAuthFlowConfig),触发宿主启动 OAuth 流程;
  • select类型可选loadOptions回调,在运行时(例如 OAuth 完成后)动态加载选项。

对照 GitHub plugin.ts,真实 provider 的配置字段包括:repo(必填仓库名)、token(password 类型,可选)、tokenHelp(link 帮助)、apiBaseUrlfilterUsernamebacklogQueryincludePullRequests(checkbox),其中后四项均为advanced: true

4.3 核心数据模型:PluginSearchResultPluginIssue

搜索返回PluginSearchResult[],详情返回PluginIssue(issue-provider-types.ts):

  • PluginSearchResultidtitleurlstatusassigneelabels(供 tagIds 映射在初始导入时使用)、start(议程视图所需的事件开始时间戳 ms)、dueWithTime(带时间的精确截止时间戳,设置后任务以dueWithTime而非dueDay创建)、duration(事件时长 ms)、isAllDay(是否全天事件)、description,外加[key: string]: unknown的 provider 自定义扩展。
  • PluginIssueidtitlebodyurlstatelastUpdatedassigneelabelscommentsPluginIssueComment[]author/body/created加扩展字段),同样允许扩展键。

GitHub provider 在getById里把PluginIssue扩展出numbersummarycreatorcreatorAvatarUrlassigneeUrlmilestonelockedisPullRequestpullRequestUrlcreatedAtclosedAt等字段,再通过issueDisplaycommentsConfig消费它们(plugin.ts)。值得注意的细节:它把评论按updated_at计算lastUpdated,并用filterUsername过滤掉自己的评论,避免自身操作触发"有更新"的误判。

5. HTTP 调用与安全边界:PluginHttp、SSRF 与allowedHosts

原文档用较大篇幅告诫开发者谨慎使用 HTTP 能力,这是 issue-provider 插件安全性的核心:

统一使用PluginHttp参数发起 provider 请求(issue-provider-types.ts)。它:

  • 返回 Promise;
  • 自动应用getHeaders返回的请求头;
  • 限制可用方法与超时(get/post/put/patch/delete,以及任意方法的request(method, url, body, options)——CalDAV 的 PROPFIND/REPORT 就靠它);
  • PluginHttpOptions支持paramsheaderstimeoutresponseType: 'json' | 'text'(文本响应用于 XML/iCal)。

SSRF 防护的现状与局限(原文档明确警示):

  • PluginHttp的初始 URL 检查默认拒绝已知元数据主机、常见本地主机名与字面私有 IP 地址;
  • 但它不会在请求前解析主机名,也不会重新校验重定向目标;issue-provider 请求当前会跟随重定向;
  • 因此PluginHttp不是完整的 SSRF 边界,应使用你信任的固定 HTTPS API 源
  • allowPrivateNetwork只对受信任的内置插件生效,仅为自托管且确实需要的 provider 开启。

allowedHosts的作用域(types.ts 有详细注释):

  • 它只约束独立的PluginAPI.request方法,不约束传给 issue-provider 方法的PluginHttp对象;
  • PluginAPI.request需要同时声明"http"权限并列出精确主机名(仅主机、精确匹配、无通配符、忽略端口),缺任一即 fail-closed 拒绝;
  • 在 web 与桌面端,PluginAPI.request拒绝跟随重定向(redirect: 'error');原生(Capacitor)平台仍会跟随重定向,且任何平台都不会重新校验 DNS 解析结果。

6. 凭据处理:OAuth 与本地密钥

6.1 OAuth 流程

声明 OAuth 的 provider 需要在 manifest 中同时声明"oauth""http"权限,并在配置字段中加一个带OAuthFlowConfigoauthButton字段。宿主会启动平台适配的 OAuth 流程并保存 token;provider 方法通过PluginAPI.getOAuthToken()异步获取:

declare const PluginAPI: { getOAuthToken(): Promise<string | null>; }; async function getHeaders(): Promise<Record<string, string>> { const token = await PluginAPI.getOAuthToken(); if (!token) throw new Error('Connect the account first.'); return { Authorization: `Bearer ${token}` }; }

OAuthFlowConfig(types.ts)支持多平台 Client ID 覆盖与桌面重定向覆盖:

  • authUrltokenUrlclientIdscopes为基础字段,extraAuthParams可追加授权 URL 查询参数(如access_typeprompt);
  • mobileClientId(Android,按包名+SHA-1 签名密钥认证)、iosClientId(iOS,按 bundle ID 认证)、webClientId(浏览器端 Authorization Code + PKCE 公开客户端)分别覆盖对应平台的clientId且省略clientSecret
  • redirectUri仅桌面 Electron loopback 流程生效,web 与原生平台各自只有固定回调,该字段会被忽略。

关键安全提示:嵌入插件源码或配置中的clientSecret并不机密。只有 provider 明确将该客户端视为公开客户端(如 Google 按 RFC 8252 的 installed-app 凭据)时才可包含,绝不提交机密 OAuth secret。Google Calendar provider 的 OAuth 配置(桌面、Android、iOS、scope、PKCE)就是完整的参考实现。

6.2 API Token 与密码:使用本地密钥 API

原文档强调:不要把密钥存进同步的插件数据或 provider 配置中,仅仅因为字段是type: "password"——那只是 UI 遮罩。配置字段的值会进入同步的 issue-provider config,最终落入同步状态、导出与备份。正确做法是使用按插件隔离的本地密钥 API:

await PluginAPI.setSecret('api-token', token); const token = await PluginAPI.getSecret('api-token'); await PluginAPI.deleteSecret('api-token');

其语义(types.ts 与 docs/plugin-development.md 的 "Secret Storage" 一节):

  • 按设备本地存储,不参与 Super Productivity 同步、导出与备份;
  • 目前静态未加密,属于隔离边界而非硬件级安全存储;
  • 用户必须在每台设备上重新输入密钥;
  • 插件卸载时其全部密钥自动清除;
  • 每个插件只能读取自己的 key。

由于宿主只把同步的config传入 provider 回调,configFields表单永远写入同步配置,因此密钥应通过getHeaders内的PluginAPI.getSecret(...)读取(getHeaders支持返回 Promise),并通过你自己的 UI(如registerConfigHandler配置对话框)调用setSecret落盘。

日志纪律:绝不记录 token、Authorization 头、含用户内容的响应体或 issue 标题。

7. 数据映射与字段同步

原文档给出六条映射纪律:

  • 将远程 ID 规范化为字符串(GitHub 用String(issue.number)正是如此);
  • 显式转换时间戳,并测试时区与全天事件行为(new Date(issue.updated_at).getTime()是典型写法);
  • 只返回PluginSearchResultPluginIssue需要的字段;
  • 只为安全、可逆的语义定义fieldMappings;远程写行为出人意料时,把映射默认成offpullOnly
  • 在 provider 允许的情况下,让 create/update/delete幂等
  • 处理限流、分页、已删除状态与部分 API 响应
  • provider 专属数据留在插件内,不扩展核心模型。

PluginFieldMapping(issue-provider-types.ts)的结构:taskField可取'isDone' | 'title' | 'notes' | 'dueDay' | 'dueWithTime' | 'timeEstimate' | 'tagIds'issueField是远程字段名,defaultDirection'off' | 'pullOnly' | 'pushOnly' | 'both'toIssueValue/toTaskValue做双向转换,mutuallyExclusive声明互斥的任务字段(如dueWithTimedueDay)。特别地,tagIds映射按标签标题/名称匹配,而非内部 tag id。

GitHub provider 的映射是很好的范本(plugin.ts):

  • isDone ↔ statepullOnly方向,toIssueValueclosed/open
  • title ↔ titlepullOnly,用#<number>前缀做无损往返(避免拖尾空格丢失);
  • notes ↔ body,默认off(双向写正文出人意料,故关闭)。

此外extractSyncValues返回{ state, title, body }供同步层消费,updateIssue/createIssue在缺少 token 或遇 401/403/404 时抛翻译过的错误文案。

8. 打包与文档化仓库托管 Provider

若要把 provider 打包进仓库随应用分发,按原文档的清单操作:

  1. 将构建加入 packages/plugin-dev/scripts/build-all.js;
  2. 把构建产物复制到src/assets/bundled-plugins/<plugin-id>/
  3. 在 src/app/plugins/plugin.service.ts 的 bundled 列表中添加该资源路径(当前列表包含github-issue-providergoogle-calendar-providercaldav-calendar-providerclickup-issue-providergitea-issue-providerlinear-issue-providertrello-issue-providerazure-devops-issue-provider等);
  4. 只添加英文源字符串,遵循既有插件的 i18n 打包方式(GitHub provider 的 i18n/en.json 是其 28 种语言目录之一);
  5. 在同一变更中更新 docs/wiki/ 下的 issue-provider 对比文档。

社区上传的插件无需核心注册,保持plugin:<plugin-id>provider key 即可(安装路径为 设置 → 插件 → 选择插件 ZIP 文件上传)。

9. 验证与测试

仓库内插件的package.json提供typecheck脚本。原文档要求的最低验证流程:

cd packages/plugin-dev/<provider-name> npm run typecheck npm test npm run build

若包还没有测试脚本,则应先为以下行为补充针对性测试:响应映射、认证失败、分页、日期、写回转换。Google Calendar provider 提供了 vitest 测试配置(vitest.config.ts)与 plugin.spec.ts 作为参考。之后运行仓库级插件构建:

npm run plugins:build

最后在 web、Electron 及每个声称支持的原生平台上手动验证:配置、连接测试、搜索/导入、轮询,以及所有已启用的写回能力。

10. 遗留核心 Provider 的维护边界

已内置的 provider 仍实现 src/app/features/issue/issue-service-interface.ts 中的IssueServiceInterface,其当前方法均返回 Promise。给既有内置 provider 修复 bug 时,应遵循其既有目录与测试组织方式。

新增另一个核心 provider 会带来永久的联合类型、配置状态、表单、迁移与同步兼容负担。原文档给出了明确结论:除非存在架构决策文档解释为何插件契约不充分,否则新集成不要走核心内置路径——这正是本指南以插件为中心的根本原因。

结语

从创建packages/plugin-dev/<provider-name>/包、声明最小 manifest,到实现IssueProviderPluginDefinition的六项必选契约与按需可选能力,再到 OAuth/密钥处理、数据映射纪律、打包进src/assets/bundled-plugins/与多平台验证,issue-provider 插件路径把外部系统接入的复杂度完整隔离在插件包内。参考仓库中 GitHub(搜索与字段同步)、Google Calendar(OAuth 与事件写回)、CalDAV(文本协议与私有网络)三个范本,配合 issue-provider-types.ts 这一权威契约,即可安全、可维护地为 Super Productivity 添加新的问题与日历集成。

【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity

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

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

NetApp MetroCluster高可用存储架构与运维实践

1. MetroCluster技术架构解析NetApp MetroCluster&#xff08;MCC&#xff09;是一种将基于阵列的集群与同步复制相结合的高可用存储解决方案。我在金融行业数据中心运维中接触过多种MCC部署案例&#xff0c;其核心价值在于通过跨站点镜像技术实现RPO0和RTO≈0的业务连续性保障…

作者头像 李华
网站建设 2026/9/13 23:20:43

VS Code 十年的故事:一切才刚刚开始

最近微软悄咪咪放出了一部纪录片&#xff0c;叫 《The Story of VS Code》 &#xff0c;将近 100 分钟&#xff0c;把 VS Code 这十年的老底都抖出来了。我看完之后最大的感受就是&#xff1a;这玩意儿能活下来并统治世界&#xff0c;简直是个奇迹。它最初只是个浏览器里的“玩…

作者头像 李华
网站建设 2026/9/13 23:20:00

验证码戒断反应:系统接管后的第一周

验证码戒断反应&#xff1a;系统接管后的第一周 一段反常的心理记录&#xff1a; 「系统上线第一周&#xff0c;我居然不适应。干活的时候手总想点鼠标&#xff0c;五分钟不看屏幕心里发慌&#xff0c;晚上定了三个闹钟起来查挂机——明明什么都没坏。朋友说我这是验证码PTSD的…

作者头像 李华
网站建设 2026/9/13 23:19:27

【电路分析】采样和限流的理解

一、基础分析1.1、基础电路工作逻辑Q1 是 NPN 功率管 TIP41C&#xff1a;VCC 通过电阻给 Q1 基极 b 提供偏置&#xff0c;Q1 导通&#xff0c;主电流Ic路径&#xff1a;BATT → Q1 集电极 c → Q1 发射极 e → 采样电阻 R1 → GND。理想无限流时&#xff0c;负载短路 / 过载会让…

作者头像 李华