来自 PaperOut To-Authors(longform-paperbell,本仓库子插件)。完整提案写在我们仓库里:docs/PROPOSAL_PROJECTS_SCOPE.md,这个 issue 是它的摘要 + 我们最需要答复的几个点。
刚刚适配完 0.4.7(MIGRATION-0.4.7.md,我们那边已发 2.4.0-beta.6),确认这个 scope 到 0.4.7 仍未实现。我们的类型已经 vendored 并标注为提案,每次调用都同时检查 capabilities.includes("projects") 和 typeof client.requestProjects === "function",拿不到就回退成纯文本输入框——所以这个提案被改、被拆、被拒都不会弄坏任何东西,不用为了兼容我们而将就。
背景
PaperOut 把论文写进 50 - Outputs,但笔记里没有任何字段说明它属于哪个项目,Project Manager 因此认不出它是项目交付物。现在「新建 PaperBell 论文项目…」弹窗会问用户属于哪个项目,并写进每个 draft 的 frontmatter。这个下拉框里的内容从哪来,就是本提案要解决的。
1. 最想要答复的:frontmatter 约定(不依赖你们写任何代码)
我们目前在每个 draft 索引笔记的顶层写:
纯字符串的项目缩写,沿用 PAPERBELL_SUITE 里已经写下的约定。如果 Project Manager 实际查询的是别的形状,请在这个版本铺开到用户之前告诉我们。
主要的替代方案是 wikilink(project: "[[40 - Projects/ColMemo]]"),能拿到原生反链、且项目笔记重命名后不会断——客观上更好的性质。改我们的写入端只要一行;迁移用户磁盘上已经存在的笔记不是。所以这件事拖得越久,改错的代价越大。
一个不必中心化决策的办法:在每个 project 上返回一个 frontmatterValue 字段,我们原样写入。这样互操作格式的权威就留在 Project Manager 那边,双方以后都不用再重新约定一次。
2. 一篇论文最多四个笔记,请按 longform.title 去重
一个 PaperBell 论文项目可以包含正文、补充材料、回复信、投稿信。每个都是独立笔记、独立 frontmatter,且都带同一个 project: 值——这是我们有意为之:补充材料和回复信确实属于该项目的产出。
对你们的影响是:数笔记会把一篇论文数成最多四篇。两个现成的去重键:
longform.title——同一篇论文的所有 draft 完全一致;
- 论文文件夹
metadata.json 里的 _longform.acronym——这篇论文自己的缩写。
注意 _longform.acronym(如 SLM,论文代号,用于 PDF 文件名)和 project(如 ColMemo,项目代号)是两个不同的东西。我们在 UI 里把它们明确区分开,建议你们也是。
3. scope 契约本身
export const PPB_PROJECTS_CHANGED_EVENT = "paperbell:projects-changed";
export interface PPBProject {
id: string; // 稳定 id,重命名 / 移动后不变
name: string; // 展示名
acronym?: string; // 写入 `project:` 的值;同 vault 内须唯一
notePath?: string; // 项目笔记路径(可选)
status?: "active" | "planned" | "paused" | "done" | "archived";
folder?: string; // 项目根文件夹(可选)
concepts?: string[]; // 关联的 featured concepts(可选)
}
export interface PPBProjectsQuery {
status?: NonNullable<PPBProject["status"]>[]; // 缺省 ["active", "planned"]
query?: string;
}
export interface PPBProjectsResult {
ok: boolean;
projects: PPBProject[];
error?: string;
}
// 挂在 PPBClient 上:
requestProjects?(params?: PPBProjectsQuery): Promise<PPBProjectsResult | null>;
onProjectsChange?(cb: () => void): () => void;
三个关于数据的请求:
id 要在重命名和移动后保持不变。 vault 路径不满足这一点。我们展示 name、写入 acronym、id 留给将来的反向上报。如果你们唯一稳定的句柄就是路径,直说,我们就不在 id 上建任何东西。
acronym 在同一个 vault 内必须唯一。 两个项目共用一个缩写,就会写出同样的 project: 值,交付物归属会静默出错。如果保证不了唯一性,就把 acronym 从契约里去掉,改给我们 §1 说的 frontmatterValue——每个项目一个权威字符串。
- 希望这个 scope 的同意成本尽量低。 这里没有任何敏感数据:项目名和缩写本来就摆在用户自己的 vault 里。作为一个需要同意的 scope,用户第一次建论文时会吃一个权限弹框——纯摩擦,没有安全收益。要么标成低摩擦,要么并进现有的
config scope。
同意门槛带来的一个具体后果
我们在新建论文弹窗打开时调 requestProjects(),而契约没有给我们取消一个在途请求的办法。如果用户在你们的权限弹框还开着的时候关掉了弹窗,那个弹框会活得比弹窗久——它孤零零地停在那里,问的是一个已经不在屏幕上的字段。两个出路,任选其一:
- 一个免同意的探测(比如
hasProjects(): boolean,或干脆在 getPluginInfo() 里报个数量),我们只在确实有东西可展示时才触发真正的弹框;或
- 给
requestProjects 加一个 AbortSignal 参数,让关闭中的弹窗能撤回这次询问。
把 scope 做成低摩擦(上一条)同样能化解它——那样就没有弹框可孤立了。
4. paperbell:projects-changed
语义对齐现有的 paperbell:config-changed。没有它我们就每次开弹窗重拉一次;有了它可以缓存并按需刷新。优先级低于 §3——功能没有它也能用。
5. 请让 capabilities 诚实
加这套 API 时,请同时 bump PaperBellPluginInfo.version 并且把 "projects" 加进 capabilities。
我们的探测刻意忽略版本号字符串,只看两件事:
capabilities.includes("projects") && typeof client.requestProjects === "function"
所以宣称了却没实现是唯一会波及用户的失败模式——我们不会抛异常,但下拉框会静默停留在文本框状态。这个改动是向后兼容的,schemaVersion 不 bump 也行;如果你们 bump 了,我们会重新 vendoring 并对齐 PPB_SCHEMA_VERSION。
6. 反向通道(下一轮,不阻塞这个)
契约目前是单向的:子插件消费,没有发布路径。PPBRequestSource 上没有任何字段能让子插件暴露自己的 API,所以今天 Project Manager 想问我们「这个项目有哪些交付物、各自进展如何」,除了直接去伸手拿 app.plugins.plugins["longform-paperbell"].api 之外没有别的办法——而 0.4.7 收紧的正是这类写法,方向上我们完全赞成。
两种封口方式,我们都能接受,请挑一个:
- 注册表:让
registerPPBplugin 接受一个 api 字段,宿主按请求把某个兄弟插件的 API 交给另一个子插件。通用,一次解决所有配对。
- 总线:给宿主一个
emit(event, payload),让我们在脚手架建好、编译完成时广播 paperout:deliverable-changed。更简单、推送式、不用设计查询面。
在这之前 Project Manager 只能扫 frontmatter——这也正是 §1 和 §2 是本文里最需要先有答复的两节的原因。
来自 PaperOut To-Authors(
longform-paperbell,本仓库子插件)。完整提案写在我们仓库里:docs/PROPOSAL_PROJECTS_SCOPE.md,这个 issue 是它的摘要 + 我们最需要答复的几个点。刚刚适配完 0.4.7(
MIGRATION-0.4.7.md,我们那边已发 2.4.0-beta.6),确认这个 scope 到 0.4.7 仍未实现。我们的类型已经 vendored 并标注为提案,每次调用都同时检查capabilities.includes("projects")和typeof client.requestProjects === "function",拿不到就回退成纯文本输入框——所以这个提案被改、被拆、被拒都不会弄坏任何东西,不用为了兼容我们而将就。背景
PaperOut 把论文写进
50 - Outputs,但笔记里没有任何字段说明它属于哪个项目,Project Manager 因此认不出它是项目交付物。现在「新建 PaperBell 论文项目…」弹窗会问用户属于哪个项目,并写进每个 draft 的 frontmatter。这个下拉框里的内容从哪来,就是本提案要解决的。1. 最想要答复的:frontmatter 约定(不依赖你们写任何代码)
我们目前在每个 draft 索引笔记的顶层写:
纯字符串的项目缩写,沿用 PAPERBELL_SUITE 里已经写下的约定。如果 Project Manager 实际查询的是别的形状,请在这个版本铺开到用户之前告诉我们。
主要的替代方案是 wikilink(
project: "[[40 - Projects/ColMemo]]"),能拿到原生反链、且项目笔记重命名后不会断——客观上更好的性质。改我们的写入端只要一行;迁移用户磁盘上已经存在的笔记不是。所以这件事拖得越久,改错的代价越大。一个不必中心化决策的办法:在每个 project 上返回一个
frontmatterValue字段,我们原样写入。这样互操作格式的权威就留在 Project Manager 那边,双方以后都不用再重新约定一次。2. 一篇论文最多四个笔记,请按
longform.title去重一个 PaperBell 论文项目可以包含正文、补充材料、回复信、投稿信。每个都是独立笔记、独立 frontmatter,且都带同一个
project:值——这是我们有意为之:补充材料和回复信确实属于该项目的产出。对你们的影响是:数笔记会把一篇论文数成最多四篇。两个现成的去重键:
longform.title——同一篇论文的所有 draft 完全一致;metadata.json里的_longform.acronym——这篇论文自己的缩写。注意
_longform.acronym(如SLM,论文代号,用于 PDF 文件名)和project(如ColMemo,项目代号)是两个不同的东西。我们在 UI 里把它们明确区分开,建议你们也是。3. scope 契约本身
三个关于数据的请求:
id要在重命名和移动后保持不变。 vault 路径不满足这一点。我们展示name、写入acronym、id留给将来的反向上报。如果你们唯一稳定的句柄就是路径,直说,我们就不在id上建任何东西。acronym在同一个 vault 内必须唯一。 两个项目共用一个缩写,就会写出同样的project:值,交付物归属会静默出错。如果保证不了唯一性,就把acronym从契约里去掉,改给我们 §1 说的frontmatterValue——每个项目一个权威字符串。configscope。同意门槛带来的一个具体后果
我们在新建论文弹窗打开时调
requestProjects(),而契约没有给我们取消一个在途请求的办法。如果用户在你们的权限弹框还开着的时候关掉了弹窗,那个弹框会活得比弹窗久——它孤零零地停在那里,问的是一个已经不在屏幕上的字段。两个出路,任选其一:hasProjects(): boolean,或干脆在getPluginInfo()里报个数量),我们只在确实有东西可展示时才触发真正的弹框;或requestProjects加一个AbortSignal参数,让关闭中的弹窗能撤回这次询问。把 scope 做成低摩擦(上一条)同样能化解它——那样就没有弹框可孤立了。
4.
paperbell:projects-changed语义对齐现有的
paperbell:config-changed。没有它我们就每次开弹窗重拉一次;有了它可以缓存并按需刷新。优先级低于 §3——功能没有它也能用。5. 请让
capabilities诚实加这套 API 时,请同时 bump
PaperBellPluginInfo.version并且把"projects"加进capabilities。我们的探测刻意忽略版本号字符串,只看两件事:
所以宣称了却没实现是唯一会波及用户的失败模式——我们不会抛异常,但下拉框会静默停留在文本框状态。这个改动是向后兼容的,
schemaVersion不 bump 也行;如果你们 bump 了,我们会重新 vendoring 并对齐PPB_SCHEMA_VERSION。6. 反向通道(下一轮,不阻塞这个)
契约目前是单向的:子插件消费,没有发布路径。
PPBRequestSource上没有任何字段能让子插件暴露自己的 API,所以今天 Project Manager 想问我们「这个项目有哪些交付物、各自进展如何」,除了直接去伸手拿app.plugins.plugins["longform-paperbell"].api之外没有别的办法——而 0.4.7 收紧的正是这类写法,方向上我们完全赞成。两种封口方式,我们都能接受,请挑一个:
registerPPBplugin接受一个api字段,宿主按请求把某个兄弟插件的 API 交给另一个子插件。通用,一次解决所有配对。emit(event, payload),让我们在脚手架建好、编译完成时广播paperout:deliverable-changed。更简单、推送式、不用设计查询面。在这之前 Project Manager 只能扫 frontmatter——这也正是 §1 和 §2 是本文里最需要先有答复的两节的原因。