
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-idkey 与它通信。社区上传的插件不需要核心注册天然保持该 key仓库托管的插件即便打包进核心发布也遵循同样的契约。需要为既有内置 provider 修 bug 时仍走其原有目录与测试新集成则一律走插件路径。2. 权威契约与参考实现开发前先锁定两处权威定义它们是事实标准可能比任何二手文档更新packages/plugin-api/src/issue-provider-types.tsIssueProviderPluginDefinition、PluginSearchResult、PluginIssue、PluginHttp、PluginFieldMapping、PluginFormField等 issue-provider 专属类型。packages/plugin-api/src/types.ts通用PluginManifest、OAuthFlowConfig、PluginAPI接口以及任务/项目/标签等共享模型。仓库内三个已打包 provider 分别示范了不同侧重点是照着抄的最佳范本参考实现目录侧重示范GitHubpackages/plugin-dev/github-issue-provider/搜索、评论、backlog 导入、字段同步fieldMappingsGoogle 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提供build、typecheck、lint三个脚本构建走scripts/build.jsesbuild 打包。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 中的PluginManifest与IssueProviderManifestConfig各字段说明如下顶层字段hooks与permissions是必需数组不用就写[]description可选icon是相对插件根目录的 SVG 路径iFrame: false表示这是纯宿主侧逻辑插件不需要 iframe UI。type取issueProvider区别于普通standard插件。permissionshttp是网络出站能力的显式开关。GitHub provider 只声明了[http]Google Calendar 声明[oauth, http]见其 manifest.jsonCalDAV 同样[http]但额外开启allowPrivateNetwork见其 manifest.json。issueProvider.pollIntervalMs轮询间隔GitHub 用60000010 分钟两个日历 provider 用600001 分钟。issueProvider.iconUI 中使用的 Material 图标名如github、calendar、extension并非 SVG 文件。issueProvider.humanReadableNameUI 芯片与标签中的短名称缺省时回退到插件名。issueProvider.issueStrings单复数显示文案。issueProvider.useAgendaView日历类 provider 设为true改用日程议程视图而非搜索列表Google Calendar 与 CalDAV 均如此。issueProvider.defaultAutoAddToBacklog创建该 provider 时是否默认开启新 issue 自动导入 backlog。issueProvider.allowPrivateNetwork默认falseSSRF 防护会拦截私有 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. 注册 ProviderIssueProviderPluginDefinition契约实现IssueProviderPluginDefinition是基于 Promise 的接口实现时应精确对照当前类型定义而不是把方法清单抄进插件。先看其完整签名issue-provider-types.tsexport interface IssueProviderPluginDefinition { configFields: PluginFormField[]; getHeaders(config: Recordstring, unknown): Recordstring, string | PromiseRecordstring, string; searchIssues(searchTerm: string, config: Recordstring, unknown, http: PluginHttp): PromisePluginSearchResult[]; getById(issueId: string, config: Recordstring, unknown, http: PluginHttp): PromisePluginIssue; getIssueLink(issueId: string, config: Recordstring, unknown): string; testConnection?(config: Recordstring, unknown, http: PluginHttp): Promiseboolean; getNewIssuesForBacklog?(config: Recordstring, unknown, http: PluginHttp): PromisePluginSearchResult[]; issueDisplay: PluginIssueField[]; commentsConfig?: PluginCommentsConfig; fieldMappings?: PluginFieldMapping[]; updateIssue?(...): Promisevoid; createIssue?(...): Promise{ issueId: string; issueNumber?: number; issueData: PluginIssue }; extractSyncValues?(issue: PluginIssue): Recordstring, unknown; deleteIssue?(...): Promisevoid; deletedStates?: string[]; timeBlock?: { upsertEvent(...): Promisevoid; deleteEvent(...): Promisevoid }; }必选契约只有六项configFields、getHeaders、searchIssues、getById、getIssueLink、issueDisplay。其余全部是可选的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(): Recordstring, string { return { Accept: application/json }; }, async searchIssues( searchTerm: string, config: Recordstring, unknown, http: PluginHttp, ): PromisePluginSearchResult[] { const workspace String(config.workspace); return http.getPluginSearchResult[](${API}/workspaces/${workspace}/issues, { params: { query: searchTerm }, }); }, async getById( issueId: string, _config: Recordstring, unknown, http: PluginHttp, ): PromisePluginIssue { return http.getPluginIssue(${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就是用户在配置表单里填写的值对象searchIssues与getById通过传入的http参数PluginHttp发起请求。4.2 配置字段类型PluginFormFieldconfigFields的元素类型PluginFormField支持多种形态issue-provider-types.tstype可取input | password | textarea | checkbox | select | multiSelect | link | oauthButton公共属性key存进 config 的键、label、required、description字段下方帮助文本、pattern输入校验正则、advanced折叠进高级配置区、showIf仅当指定 config key 为真时显示link类型配url用于展示如何获取 Token之类的帮助链接——GitHub provider 的tokenHelp字段正是这种用法oauthButton类型配oauthConfigOAuthFlowConfig触发宿主启动 OAuth 流程select类型可选loadOptions回调在运行时例如 OAuth 完成后动态加载选项。对照 GitHub plugin.ts真实 provider 的配置字段包括repo必填仓库名、tokenpassword 类型可选、tokenHelplink 帮助、apiBaseUrl、filterUsername、backlogQuery、includePullRequestscheckbox其中后四项均为advanced: true。4.3 核心数据模型PluginSearchResult与PluginIssue搜索返回PluginSearchResult[]详情返回PluginIssueissue-provider-types.tsPluginSearchResultid、title、url、status、assignee、labels供 tagIds 映射在初始导入时使用、start议程视图所需的事件开始时间戳 ms、dueWithTime带时间的精确截止时间戳设置后任务以dueWithTime而非dueDay创建、duration事件时长 ms、isAllDay是否全天事件、description外加[key: string]: unknown的 provider 自定义扩展。PluginIssueid、title、body、url、state、lastUpdated、assignee、labels、commentsPluginIssueComment[]author/body/created加扩展字段同样允许扩展键。GitHub provider 在getById里把PluginIssue扩展出number、summary、creator、creatorAvatarUrl、assigneeUrl、milestone、locked、isPullRequest、pullRequestUrl、createdAt、closedAt等字段再通过issueDisplay与commentsConfig消费它们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支持params、headers、timeout、responseType: 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权限并在配置字段中加一个带OAuthFlowConfig的oauthButton字段。宿主会启动平台适配的 OAuth 流程并保存 tokenprovider 方法通过PluginAPI.getOAuthToken()异步获取declare const PluginAPI: { getOAuthToken(): Promisestring | null; }; async function getHeaders(): PromiseRecordstring, string { const token await PluginAPI.getOAuthToken(); if (!token) throw new Error(Connect the account first.); return { Authorization: Bearer ${token} }; }OAuthFlowConfigtypes.ts支持多平台 Client ID 覆盖与桌面重定向覆盖authUrl、tokenUrl、clientId、scopes为基础字段extraAuthParams可追加授权 URL 查询参数如access_type、promptmobileClientIdAndroid按包名SHA-1 签名密钥认证、iosClientIdiOS按 bundle ID 认证、webClientId浏览器端 Authorization Code PKCE 公开客户端分别覆盖对应平台的clientId且省略clientSecretredirectUri仅桌面 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最终落入同步状态、导出与备份。正确做法是使用按插件隔离的本地密钥 APIawait 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()是典型写法只返回PluginSearchResult与PluginIssue需要的字段只为安全、可逆的语义定义fieldMappings远程写行为出人意料时把映射默认成off或pullOnly在 provider 允许的情况下让 create/update/delete幂等处理限流、分页、已删除状态与部分 API 响应provider 专属数据留在插件内不扩展核心模型。PluginFieldMappingissue-provider-types.ts的结构taskField可取isDone | title | notes | dueDay | dueWithTime | timeEstimate | tagIdsissueField是远程字段名defaultDirection为off | pullOnly | pushOnly | bothtoIssueValue/toTaskValue做双向转换mutuallyExclusive声明互斥的任务字段如dueWithTime与dueDay。特别地tagIds映射按标签标题/名称匹配而非内部 tag id。GitHub provider 的映射是很好的范本plugin.tsisDone ↔ statepullOnly方向toIssueValue用closed/opentitle ↔ titlepullOnly用#number前缀做无损往返避免拖尾空格丢失notes ↔ body默认off双向写正文出人意料故关闭。此外extractSyncValues返回{ state, title, body }供同步层消费updateIssue/createIssue在缺少 token 或遇 401/403/404 时抛翻译过的错误文案。8. 打包与文档化仓库托管 Provider若要把 provider 打包进仓库随应用分发按原文档的清单操作将构建加入 packages/plugin-dev/scripts/build-all.js把构建产物复制到src/assets/bundled-plugins/plugin-id/在 src/app/plugins/plugin.service.ts 的 bundled 列表中添加该资源路径当前列表包含github-issue-provider、google-calendar-provider、caldav-calendar-provider、clickup-issue-provider、gitea-issue-provider、linear-issue-provider、trello-issue-provider、azure-devops-issue-provider等只添加英文源字符串遵循既有插件的 i18n 打包方式GitHub provider 的 i18n/en.json 是其 28 种语言目录之一在同一变更中更新 docs/wiki/ 下的 issue-provider 对比文档。社区上传的插件无需核心注册保持plugin:plugin-idprovider 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 CalendarOAuth 与事件写回、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),仅供参考