dsh-mishu 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -4
- package/lib/client.js +225 -1
- package/lib/core/ask.js +16 -0
- package/lib/core/board.js +15 -3
- package/lib/core/brief.js +16 -0
- package/lib/core/index.js +3 -0
- package/lib/core/watch/fetch.js +169 -0
- package/lib/core/watch/source.js +116 -0
- package/lib/core/watch/sync.js +171 -0
- package/lib/dsh/api.js +92 -2
- package/lib/dsh/runtime.js +20 -2
- package/lib/types/core/ask.d.ts +5 -0
- package/lib/types/core/board.d.ts +12 -1
- package/lib/types/core/brief.d.ts +5 -0
- package/lib/types/core/index.d.ts +3 -0
- package/lib/types/core/watch/fetch.d.ts +50 -0
- package/lib/types/core/watch/source.d.ts +68 -0
- package/lib/types/core/watch/sync.d.ts +51 -0
- package/lib/types/dsh/runtime.d.ts +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,15 +55,28 @@
|
|
|
55
55
|
|
|
56
56
|
同一封邮件的往来回复算一件事。你在邮件客户端里看过之后,对应的事项会自动关闭。
|
|
57
57
|
|
|
58
|
+
**自己关注的**:GitHub 和邮箱之外,你也可以在设置页填任意一个网址让小秘书盯着:产品公告页、价格页、招聘页、RSS 订阅、JSON 接口都行。可以填几个"关注哪些字"(比如"上新、补丁、面试"),填了就只在出现这些字时提醒,不填就是有变化就提醒。检查间隔 15 分钟到 24 小时,默认一小时。
|
|
59
|
+
|
|
60
|
+
| 理由码 | 意思 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `watch-keyword` | 页面内容提到了你关注的字 |
|
|
63
|
+
| `watch-new` | 订阅源里出现了新的条目 |
|
|
64
|
+
| `watch-changed` | 普通网页的正文变了(比如价格) |
|
|
65
|
+
| `watch-removed` | 你删掉了这个关注,还没处理的事项一并关掉 |
|
|
66
|
+
|
|
67
|
+
一个关注项算一张卡片,不会因为一天更新十条就刷一屏。刚添加时第一次检查只记下"现在有什么",不报警;但填了关键词的,第一次命中就告诉你。读不到(断网、404、要登录)会写清原因,卡片不会因此凭空出现。
|
|
68
|
+
|
|
58
69
|
**密码放在系统钥匙串**:macOS 钥匙串、Windows 凭据管理器、Linux 密钥环(Secret Service)。密码只在设置页输入一次,直接交给钥匙串,不写进任何文件,接口也从不返回它。没有钥匙串的电脑(比如没有桌面的 Linux 服务器),密码和 token 存进记事本目录里的 `secrets.json`:权限 600,只有你的系统账号能读,但没有加密,设置页会写明这一点和原因。
|
|
59
70
|
|
|
71
|
+
**关注一个网址**:点"+ 关注一个网址",填网址(和想关注的字),点"先试读一下"当场看读到什么,再"保存并看一次"。
|
|
72
|
+
|
|
60
73
|
**添加邮箱**:点"+ 添加邮箱"弹出向导。填了地址就自动选好服务商,显示去哪里开 IMAP、拿授权码,带"打开设置页";授权码位数不对会提醒;"测试并保存"连得上才保存。
|
|
61
74
|
|
|
62
75
|
### 用法
|
|
63
76
|
|
|
64
77
|
**看板**:点 DSH 左侧栏的"小秘书",打开整页看板。顶部是一句话总结和四个数字(要你处理、7 天内到期、今天替你关掉、待你核对),下面分三栏:
|
|
65
78
|
|
|
66
|
-
看板上方可以按来源筛选:全部、GitHub
|
|
79
|
+
看板上方可以按来源筛选:全部、GitHub、邮件、关注、交代。
|
|
67
80
|
|
|
68
81
|
- **要你处理**:现在需要你的事。卡片平时是收起的,只显示标题;点标题展开,一次只展开一张,点另一张时前一张自动收起。展开后写明**是什么、问题在哪、怎么办**(比如 CI 失败在哪个 job 的哪一步、上次成功是什么时候),主按钮直接打开该看的地方("看失败日志""去关掉 Actions""去审查"),还有办完了、明天再说、忽略。
|
|
69
82
|
- **我记着**:说过"明天再说"的事,以及还没到提醒时间的交代。底部可以直接交代新的事,填写时会实时显示小秘书理解成了什么。
|
|
@@ -73,7 +86,7 @@
|
|
|
73
86
|
|
|
74
87
|
卡片可以拖动:拖到"替你办了"就是办完,拖到"我记着"就是明天再提醒,从"我记着"拖回"要你处理"就是现在处理。三栏之间的分隔条也能拖动调宽度,双击恢复等宽。
|
|
75
88
|
|
|
76
|
-
**设置**:点页面顶部的"设置",按秘书的活分成四块:① 它看哪里(GitHub
|
|
89
|
+
**设置**:点页面顶部的"设置",按秘书的活分成四块:① 它看哪里(GitHub、邮箱、自己关注的网址);② 什么事要告诉你(哪些算急事、各类别提前几天);③ 什么时候、怎么告诉你(主动提醒,见下);④ 能替你办什么(在 GitHub 上标已读的方式;邮件一律只读)。最后是语言和密码存在哪。每块都是折叠的卡片,平时只显示标题,点开才展开,一次只开一张;里面的邮箱账户、通知渠道也是同样可折叠的卡片,开关就在标题右边,不展开也能点。
|
|
77
90
|
|
|
78
91
|
**主动提醒**:不用打开看板也能知道。
|
|
79
92
|
- 每日简报:每天到点(默认 09:00)发一份"要你处理"和"快到期"的清单;没事就不发。
|
|
@@ -197,15 +210,28 @@ Default warning times: documents 90 days, bills 3, birthdays 7, renewals 7, ever
|
|
|
197
210
|
|
|
198
211
|
A thread of replies is one item. Once you read it in your mail client, its item closes by itself.
|
|
199
212
|
|
|
213
|
+
**Things you watch yourself**: beyond GitHub and mail, you can give the secretary any address on the settings page — a product announcements page, a price, a job posting, an RSS feed, a JSON endpoint. You can list a few words to watch for (say "release, patch, interview"): with words it only tells you when they show up, without words any change is worth a card. The interval runs from 15 minutes to 24 hours, one hour by default.
|
|
214
|
+
|
|
215
|
+
| Reason | Meaning |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `watch-keyword` | the content mentions a word you watch for |
|
|
218
|
+
| `watch-new` | the feed has entries you have not seen |
|
|
219
|
+
| `watch-changed` | the text of a plain page changed (a price, say) |
|
|
220
|
+
| `watch-removed` | you deleted the watch; anything it left open is closed |
|
|
221
|
+
|
|
222
|
+
One watch is one card, so a feed that updates ten times a day does not flood the board. The first look only writes down what is there and says nothing — unless you gave it words, in which case a hit is reported at once. A read that fails (offline, 404, behind a login) says why, and never invents a card.
|
|
223
|
+
|
|
200
224
|
**Passwords live in the system keychain**: the macOS Keychain, Windows Credential Manager or the Linux keyring (Secret Service). You type a password once on the settings page; it goes straight to the keychain, is never written to a file and is never returned by any endpoint. On a computer without one (a Linux server with no desktop, say), passwords and tokens go to `secrets.json` in the notebook directory: mode 600, readable by your system account only, but not encrypted, and the settings page says so and why.
|
|
201
225
|
|
|
226
|
+
**Watching an address**: "+ Watch an address" asks for the address and, if you want, the words to watch for; "Read it once" shows what it finds before you save it, then "Save and look once".
|
|
227
|
+
|
|
202
228
|
**Adding a mail account**: "+ Add a mail account" opens a short guide. The address picks the provider; it says where to turn on IMAP and get the app password, with a button that opens that page; it warns when the password has the wrong length; "Test and save" keeps only an account that connects.
|
|
203
229
|
|
|
204
230
|
### Usage
|
|
205
231
|
|
|
206
232
|
**The board**: click "Secretary" in DSH's left sidebar to open a full-page board. On top: one summary sentence and four numbers (to handle, due in 7 days, closed today, to review). Below, three columns:
|
|
207
233
|
|
|
208
|
-
Filter the board by source: All, GitHub, Mail, Noted.
|
|
234
|
+
Filter the board by source: All, GitHub, Mail, Watching, Noted.
|
|
209
235
|
|
|
210
236
|
- **To handle**: what needs you now. Cards stay folded and show only the title; click a title to unfold it — one at a time, so opening another folds the first. Unfolded, a card says **what it is, where the problem is and what to do** (for a failing CI: which job and step, when it last passed), and its main button opens the right place ("Open the failing log", "Turn off Actions", "Review it"), next to done, tomorrow and dismiss.
|
|
211
237
|
- **Keeping for you**: things you put off until tomorrow, and things you told it that are not due for a reminder yet. Add a new one at the bottom, with a live preview of what was understood.
|
|
@@ -217,7 +243,7 @@ Cards can be dragged: onto "Done for you" to mark done, onto "Keeping for you" t
|
|
|
217
243
|
|
|
218
244
|
**Settings**: the "Settings" tab at the top of the page: language, warning days per category, GitHub (on/off, where the token comes from, connection test, how to mark read), mail accounts (add, test, check now, edit, delete). Every block is a folded card that shows only its title until you click it, and only one is open at a time; mail accounts and notification channels fold the same way, with their switch on the title row so you can flip it without opening the card.
|
|
219
245
|
|
|
220
|
-
The settings follow the secretary's job in four parts: 1. where it looks (GitHub, mail); 2. what to tell you about (what counts as urgent, warning days per category); 3. when and how to tell you (reminders, below); 4. what it may do for you (marking read on GitHub; mail is always read-only). Then language and where secrets are kept.
|
|
246
|
+
The settings follow the secretary's job in four parts: 1. where it looks (GitHub, mail, addresses you watch); 2. what to tell you about (what counts as urgent, warning days per category); 3. when and how to tell you (reminders, below); 4. what it may do for you (marking read on GitHub; mail is always read-only). Then language and where secrets are kept.
|
|
221
247
|
|
|
222
248
|
**Reminders**, without opening the board:
|
|
223
249
|
- Daily brief: at its time (09:00 by default) a list of what needs you and what is coming up; nothing is sent when there is nothing.
|
package/lib/client.js
CHANGED
|
@@ -80,7 +80,11 @@ var settingsApi = {
|
|
|
80
80
|
saveMail: (lang, account, password) => post("/mail/save", { lang, account, password }),
|
|
81
81
|
testMail: (lang, account, password) => post("/mail/test", { lang, account, password }),
|
|
82
82
|
deleteMail: (lang, id) => post("/mail/delete", { lang, id }),
|
|
83
|
-
syncMail: (lang, id) => post("/mail/sync", { lang, id })
|
|
83
|
+
syncMail: (lang, id) => post("/mail/sync", { lang, id }),
|
|
84
|
+
saveWatch: (lang, watch) => post("/watch/save", { lang, watch }),
|
|
85
|
+
testWatch: (lang, watch) => post("/watch/test", { lang, watch }),
|
|
86
|
+
deleteWatch: (lang, id) => post("/watch/delete", { lang, id }),
|
|
87
|
+
syncWatch: (lang, id) => post("/watch/sync", { lang, id })
|
|
84
88
|
};
|
|
85
89
|
|
|
86
90
|
// src/client/AskAI.tsx
|
|
@@ -156,6 +160,7 @@ var zh = {
|
|
|
156
160
|
sourceGithub: "GitHub",
|
|
157
161
|
sourceMail: "\u90AE\u4EF6",
|
|
158
162
|
sourceManual: "\u4EA4\u4EE3",
|
|
163
|
+
sourceWatch: "\u5173\u6CE8",
|
|
159
164
|
sGeneral: "\u901A\u7528",
|
|
160
165
|
sLanguage: "\u8BED\u8A00",
|
|
161
166
|
sLanguageAuto: "\u8DDF\u968F\u7CFB\u7EDF",
|
|
@@ -208,6 +213,34 @@ var zh = {
|
|
|
208
213
|
sPasswordTestOnly: "\u6CA1\u6709\u94A5\u5319\u4E32\uFF1A\u8FD9\u91CC\u586B\u7684\u53EA\u7528\u6765\u6D4B\u8BD5\u8FDE\u63A5\uFF0C\u4E0D\u4F1A\u4FDD\u5B58",
|
|
209
214
|
secretKinds: { macos: "macOS \u94A5\u5319\u4E32", windows: "Windows \u51ED\u636E\u7BA1\u7406\u5668", linux: "Linux \u5BC6\u94A5\u73AF\uFF08Secret Service\uFF09", memory: "\u5185\u5B58\uFF08\u6D4B\u8BD5\u7528\uFF09" },
|
|
210
215
|
sNoAccounts: "\u8FD8\u6CA1\u6709\u6DFB\u52A0\u90AE\u7BB1\u3002",
|
|
216
|
+
sWatch: "\u81EA\u5DF1\u5173\u6CE8\u7684",
|
|
217
|
+
sWatchHint: "\u9664\u4E86 GitHub \u548C\u90AE\u7BB1\uFF0C\u4F60\u4E5F\u53EF\u4EE5\u8BA9\u5C0F\u79D8\u4E66\u76EF\u7740\u4EFB\u610F\u4E00\u4E2A\u7F51\u5740\uFF1A\u516C\u544A\u9875\u3001\u4EF7\u683C\u3001\u62DB\u8058\u9875\u3001\u8BA2\u9605\u6E90\u90FD\u884C\u3002\u5B83\u4F1A\u9694\u4E00\u4F1A\u513F\u770B\u4E00\u6B21\uFF0C\u6709\u53D8\u5316\u5C31\u653E\u5230\u770B\u677F\u4E0A\u3002",
|
|
218
|
+
sNoWatches: "\u8FD8\u6CA1\u6709\u5173\u6CE8\u4EFB\u4F55\u7F51\u5740\u3002",
|
|
219
|
+
sAddWatch: "+ \u5173\u6CE8\u4E00\u4E2A\u7F51\u5740",
|
|
220
|
+
wTitleAdd: "\u5173\u6CE8\u4E00\u4E2A\u7F51\u5740",
|
|
221
|
+
wTitleEdit: "\u4FEE\u6539\u5173\u6CE8",
|
|
222
|
+
wUrl: "\u7F51\u5740",
|
|
223
|
+
wUrlHint: "RSS \u8BA2\u9605\u3001JSON \u63A5\u53E3\u3001\u666E\u901A\u7F51\u9875\u90FD\u53EF\u4EE5\u3002\u8981\u767B\u5F55\u624D\u80FD\u770B\u7684\u9875\u9762\u8BFB\u4E0D\u5230\u3002",
|
|
224
|
+
wLabel: "\u540D\u79F0",
|
|
225
|
+
wKeywords: "\u5173\u6CE8\u54EA\u4E9B\u5B57\uFF08\u53EF\u9009\uFF0C\u7528\u9017\u53F7\u5206\u5F00\uFF09",
|
|
226
|
+
wKeywordsHint: "\u586B\u4E86\u5C31\u53EA\u5728\u51FA\u73B0\u8FD9\u4E9B\u5B57\u65F6\u63D0\u9192\u4F60\uFF1B\u4E0D\u586B\u5C31\u662F\u6709\u53D8\u5316\u5C31\u63D0\u9192\u3002",
|
|
227
|
+
wEvery: "\u591A\u4E45\u770B\u4E00\u6B21",
|
|
228
|
+
wEveryUnit: (n) => `${n} \u5206\u949F`,
|
|
229
|
+
wTest: "\u5148\u8BD5\u8BFB\u4E00\u4E0B",
|
|
230
|
+
wTesting: "\u6B63\u5728\u8BFB\u2026",
|
|
231
|
+
wTestOk: (n, title) => `\u8BFB\u5230\u4E86 ${n} \u6761\u5185\u5BB9\uFF0C\u7B2C\u4E00\u6761\u662F\u300C${title}\u300D\u3002`,
|
|
232
|
+
wTestNoHit: (word) => `\u8BFB\u5230\u4E86\uFF0C\u4F46\u7B2C\u4E00\u6761\u91CC\u6CA1\u6709\u300C${word}\u300D\u3002\u4FDD\u5B58\u540E\u8FD8\u4F1A\u7EE7\u7EED\u76EF\u7740\u3002`,
|
|
233
|
+
wTestHit: (word) => `\u8BFB\u5230\u4E86\uFF0C\u7B2C\u4E00\u6761\u91CC\u5C31\u6709\u300C${word}\u300D\u3002`,
|
|
234
|
+
wSave: "\u4FDD\u5B58",
|
|
235
|
+
wSaveAndCheck: "\u4FDD\u5B58\u5E76\u770B\u4E00\u6B21",
|
|
236
|
+
wCheckNow: "\u73B0\u5728\u770B\u4E00\u6B21",
|
|
237
|
+
wDeleteConfirm: "\u5220\u6389\u8FD9\u4E2A\u5173\u6CE8\uFF1F\u5B83\u8FD8\u6CA1\u5904\u7406\u7684\u4E8B\u9879\u4F1A\u88AB\u5173\u6389\u3002",
|
|
238
|
+
wNeverChecked: "\u8FD8\u6CA1\u770B\u8FC7",
|
|
239
|
+
wKeywordsNone: "\u6709\u4EFB\u4F55\u53D8\u5316\u5C31\u544A\u8BC9\u6211",
|
|
240
|
+
wKeywordsList: (words) => `\u63D0\u5230 ${words} \u65F6\u544A\u8BC9\u6211`,
|
|
241
|
+
wChecked: (at, n) => `${at} \u770B\u8FC7\uFF0C${n} \u6761\u5185\u5BB9`,
|
|
242
|
+
wDisabled: "\u5DF2\u505C\u7528",
|
|
243
|
+
wAskSetup: "\u4E0D\u77E5\u9053\u8BE5\u586B\u4EC0\u4E48\u7F51\u5740\uFF1F",
|
|
211
244
|
sAddAccount: "+ \u6DFB\u52A0\u90AE\u7BB1",
|
|
212
245
|
sEdit: "\u7F16\u8F91",
|
|
213
246
|
sDelete: "\u5220\u9664",
|
|
@@ -343,6 +376,7 @@ var en = {
|
|
|
343
376
|
sourceGithub: "GitHub",
|
|
344
377
|
sourceMail: "Mail",
|
|
345
378
|
sourceManual: "Noted",
|
|
379
|
+
sourceWatch: "Watching",
|
|
346
380
|
sGeneral: "General",
|
|
347
381
|
sLanguage: "Language",
|
|
348
382
|
sLanguageAuto: "Follow the system",
|
|
@@ -395,6 +429,34 @@ var en = {
|
|
|
395
429
|
sPasswordTestOnly: "No keychain: used only to test the connection, not saved",
|
|
396
430
|
secretKinds: { macos: "the macOS Keychain", windows: "Windows Credential Manager", linux: "the Linux keyring (Secret Service)", memory: "memory (tests)" },
|
|
397
431
|
sNoAccounts: "No mail account yet.",
|
|
432
|
+
sWatch: "Things you watch",
|
|
433
|
+
sWatchHint: "Beyond GitHub and mail, the secretary can keep an eye on any address you give it: an announcements page, a price, a job posting, a feed. It looks now and then and puts a card on the board when something changes.",
|
|
434
|
+
sNoWatches: "Not watching anything yet.",
|
|
435
|
+
sAddWatch: "+ Watch an address",
|
|
436
|
+
wTitleAdd: "Watch an address",
|
|
437
|
+
wTitleEdit: "Edit watch",
|
|
438
|
+
wUrl: "Address",
|
|
439
|
+
wUrlHint: "An RSS feed, a JSON endpoint or a plain page all work. Pages behind a login cannot be read.",
|
|
440
|
+
wLabel: "Name",
|
|
441
|
+
wKeywords: "Words to watch for (optional, comma separated)",
|
|
442
|
+
wKeywordsHint: "With words, it only tells you when they show up; without, any change is worth a card.",
|
|
443
|
+
wEvery: "Check every",
|
|
444
|
+
wEveryUnit: (n) => `${n} minutes`,
|
|
445
|
+
wTest: "Read it once",
|
|
446
|
+
wTesting: "Reading\u2026",
|
|
447
|
+
wTestOk: (n, title) => `Read ${n} entries; the first is "${title}".`,
|
|
448
|
+
wTestNoHit: (word) => `It loaded, but the first entry does not mention "${word}". It keeps watching after you save.`,
|
|
449
|
+
wTestHit: (word) => `It loaded, and the first entry mentions "${word}".`,
|
|
450
|
+
wSave: "Save",
|
|
451
|
+
wSaveAndCheck: "Save and look once",
|
|
452
|
+
wCheckNow: "Look now",
|
|
453
|
+
wDeleteConfirm: "Delete this watch? Anything it still has open is closed.",
|
|
454
|
+
wNeverChecked: "Not checked yet",
|
|
455
|
+
wKeywordsNone: "tell me about any change",
|
|
456
|
+
wKeywordsList: (words) => `tell me when it mentions ${words}`,
|
|
457
|
+
wChecked: (at, n) => `checked ${at}, ${n} entries`,
|
|
458
|
+
wDisabled: "disabled",
|
|
459
|
+
wAskSetup: "Not sure what address to use?",
|
|
398
460
|
sAddAccount: "+ Add a mail account",
|
|
399
461
|
sEdit: "Edit",
|
|
400
462
|
sDelete: "Delete",
|
|
@@ -743,6 +805,7 @@ function MishuSettings({ lang, locale, openChat: openChat2 }) {
|
|
|
743
805
|
const [confirmOn, setConfirmOn] = (0, import_react5.useState)(false);
|
|
744
806
|
const [pasting, setPasting] = (0, import_react5.useState)(false);
|
|
745
807
|
const [editing, setEditing] = (0, import_react5.useState)();
|
|
808
|
+
const [watching, setWatching] = (0, import_react5.useState)();
|
|
746
809
|
const take = (result, saved) => {
|
|
747
810
|
if (result.ok) {
|
|
748
811
|
setView(result.view);
|
|
@@ -856,6 +919,34 @@ function MishuSettings({ lang, locale, openChat: openChat2 }) {
|
|
|
856
919
|
if (next !== void 0) take({ ok: true, view: next }, note2 ?? t.sSaved);
|
|
857
920
|
}
|
|
858
921
|
}
|
|
922
|
+
),
|
|
923
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { children: t.sWatch }),
|
|
924
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mishu-hint", children: t.sWatchHint }),
|
|
925
|
+
view.watches.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mishu-empty", children: t.sNoWatches }) : view.watches.map((watch) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
926
|
+
WatchRow,
|
|
927
|
+
{
|
|
928
|
+
watch,
|
|
929
|
+
lang,
|
|
930
|
+
locale,
|
|
931
|
+
openChat: openChat2,
|
|
932
|
+
onEdit: () => setWatching({ ...watch }),
|
|
933
|
+
onView: (next) => take(next)
|
|
934
|
+
},
|
|
935
|
+
watch.id
|
|
936
|
+
)),
|
|
937
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn mishu-add-toggle", onClick: () => setWatching({ label: "", url: "", keywords: "", enabled: true, intervalMinutes: 60 }), children: t.sAddWatch }),
|
|
938
|
+
watching === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
939
|
+
WatchDialog,
|
|
940
|
+
{
|
|
941
|
+
draft: watching,
|
|
942
|
+
lang,
|
|
943
|
+
locale,
|
|
944
|
+
openChat: openChat2,
|
|
945
|
+
onDone: (next, note2) => {
|
|
946
|
+
setWatching(void 0);
|
|
947
|
+
if (next !== void 0) take({ ok: true, view: next }, note2 ?? t.sSaved);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
859
950
|
)
|
|
860
951
|
] }),
|
|
861
952
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Fold, { id: "what", className: "mishu-panel", title: t.bWhat, children: [
|
|
@@ -1185,6 +1276,133 @@ function MailDialog({ draft, view, lang, locale, openChat: openChat2, onDone })
|
|
|
1185
1276
|
] })
|
|
1186
1277
|
] });
|
|
1187
1278
|
}
|
|
1279
|
+
function WatchRow({ watch, lang, locale, openChat: openChat2, onEdit, onView }) {
|
|
1280
|
+
const t = TEXT[lang];
|
|
1281
|
+
const [busy, setBusy] = (0, import_react5.useState)(false);
|
|
1282
|
+
const failed = watch.report?.error !== void 0;
|
|
1283
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Fold, { id: `watch-${watch.id}`, title: `${watch.label}${watch.enabled ? "" : ` (${t.wDisabled})`}`, children: [
|
|
1284
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mishu-card-meta", children: [
|
|
1285
|
+
watch.url,
|
|
1286
|
+
" \xB7 ",
|
|
1287
|
+
t.wEveryUnit(watch.intervalMinutes)
|
|
1288
|
+
] }),
|
|
1289
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mishu-card-meta", children: watch.summary }),
|
|
1290
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `mishu-card-note ${failed ? "mishu-danger" : ""}`, children: [
|
|
1291
|
+
watch.status,
|
|
1292
|
+
failed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AskAI, { topic: { kind: "watch-error", watchId: watch.id }, lang, locale, openChat: openChat2, compact: true }) : null
|
|
1293
|
+
] }),
|
|
1294
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mishu-actions", children: [
|
|
1295
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn", disabled: busy, onClick: () => {
|
|
1296
|
+
setBusy(true);
|
|
1297
|
+
void settingsApi.syncWatch(locale(), watch.id).then((r) => {
|
|
1298
|
+
setBusy(false);
|
|
1299
|
+
onView(r);
|
|
1300
|
+
});
|
|
1301
|
+
}, children: busy ? t.wTesting : t.wCheckNow }),
|
|
1302
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn", onClick: onEdit, children: t.sEdit }),
|
|
1303
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn mishu-btn-quiet", onClick: () => {
|
|
1304
|
+
if (window.confirm(t.wDeleteConfirm)) void settingsApi.deleteWatch(locale(), watch.id).then(onView);
|
|
1305
|
+
}, children: t.sDelete })
|
|
1306
|
+
] })
|
|
1307
|
+
] });
|
|
1308
|
+
}
|
|
1309
|
+
function WatchDialog({ draft, lang, locale, openChat: openChat2, onDone }) {
|
|
1310
|
+
const t = TEXT[lang];
|
|
1311
|
+
const existing = draft.id !== void 0;
|
|
1312
|
+
const [watch, setWatch] = (0, import_react5.useState)(draft);
|
|
1313
|
+
const [message, setMessage] = (0, import_react5.useState)();
|
|
1314
|
+
const [tested, setTested] = (0, import_react5.useState)();
|
|
1315
|
+
const [busy, setBusy] = (0, import_react5.useState)(false);
|
|
1316
|
+
const set = (patch) => {
|
|
1317
|
+
setWatch({ ...watch, ...patch });
|
|
1318
|
+
setMessage(void 0);
|
|
1319
|
+
setTested(void 0);
|
|
1320
|
+
};
|
|
1321
|
+
const save = async (thenCheck = false) => {
|
|
1322
|
+
setBusy(true);
|
|
1323
|
+
const r = await settingsApi.saveWatch(locale(), watch);
|
|
1324
|
+
if (!r.ok) {
|
|
1325
|
+
setBusy(false);
|
|
1326
|
+
setMessage({ ok: false, text: r.message });
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
let view = r.view;
|
|
1330
|
+
if (thenCheck) {
|
|
1331
|
+
const checked = await settingsApi.syncWatch(locale(), r.id);
|
|
1332
|
+
if (checked.ok) view = checked.view;
|
|
1333
|
+
}
|
|
1334
|
+
setBusy(false);
|
|
1335
|
+
onDone(view, t.sSaved);
|
|
1336
|
+
};
|
|
1337
|
+
const test = async () => {
|
|
1338
|
+
setBusy(true);
|
|
1339
|
+
setMessage({ ok: true, text: t.wTesting });
|
|
1340
|
+
const r = await settingsApi.testWatch(locale(), watch);
|
|
1341
|
+
setBusy(false);
|
|
1342
|
+
if (!r.ok) {
|
|
1343
|
+
setMessage({ ok: false, text: r.message });
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
setTested(r);
|
|
1347
|
+
const hit = r.keywordHit === void 0 ? void 0 : r.keyword;
|
|
1348
|
+
setMessage({
|
|
1349
|
+
ok: true,
|
|
1350
|
+
text: r.keywordHit === false && hit !== void 0 ? `${t.wTestOk(r.items, r.title)} ${t.wTestNoHit(hit)}` : r.keywordHit === true && hit !== void 0 ? `${t.wTestOk(r.items, r.title)} ${t.wTestHit(hit)}` : t.wTestOk(r.items, r.title)
|
|
1351
|
+
});
|
|
1352
|
+
};
|
|
1353
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Modal, { title: existing ? t.wTitleEdit : t.wTitleAdd, onClose: () => onDone(), children: [
|
|
1354
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "mishu-field", children: [
|
|
1355
|
+
t.wUrl,
|
|
1356
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { value: watch.url, autoComplete: "off", autoFocus: !existing, placeholder: "https://example.com/news.xml", onChange: (e) => set({ url: e.target.value }) })
|
|
1357
|
+
] }),
|
|
1358
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mishu-hint", children: t.wUrlHint }),
|
|
1359
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "mishu-field", children: [
|
|
1360
|
+
t.wLabel,
|
|
1361
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { value: watch.label, placeholder: hostOf(watch.url), onChange: (e) => set({ label: e.target.value }) })
|
|
1362
|
+
] }),
|
|
1363
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "mishu-field", children: [
|
|
1364
|
+
t.wKeywords,
|
|
1365
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { value: watch.keywords, placeholder: lang === "zh" ? "\u4E0A\u65B0, \u8865\u4E01, \u9762\u8BD5" : "release, patch, interview", onChange: (e) => set({ keywords: e.target.value }) })
|
|
1366
|
+
] }),
|
|
1367
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mishu-hint", children: t.wKeywordsHint }),
|
|
1368
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mishu-row", children: [
|
|
1369
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "mishu-field", children: [
|
|
1370
|
+
t.wEvery,
|
|
1371
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "number", min: 15, max: 1440, step: 15, value: watch.intervalMinutes, onChange: (e) => set({ intervalMinutes: Number(e.target.value) }) })
|
|
1372
|
+
] }),
|
|
1373
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "mishu-check", children: [
|
|
1374
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "checkbox", checked: watch.enabled, onChange: (e) => set({ enabled: e.target.checked }) }),
|
|
1375
|
+
t.sEnabled
|
|
1376
|
+
] })
|
|
1377
|
+
] }),
|
|
1378
|
+
tested === void 0 ? null : tested.excerpt === "" ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mishu-preview", children: tested.excerpt }),
|
|
1379
|
+
message === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `mishu-preview ${message.ok ? "" : "mishu-danger"}`, children: [
|
|
1380
|
+
message.text,
|
|
1381
|
+
message.ok ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AskAI, { topic: { kind: "watch-setup" }, lang, locale, openChat: openChat2, compact: true })
|
|
1382
|
+
] }),
|
|
1383
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mishu-actions", children: [
|
|
1384
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn", disabled: busy || watch.url.trim() === "", onClick: () => {
|
|
1385
|
+
void test();
|
|
1386
|
+
}, children: t.wTest }),
|
|
1387
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn primary", disabled: busy || watch.url.trim() === "", onClick: () => {
|
|
1388
|
+
void save(true);
|
|
1389
|
+
}, children: busy ? t.wTesting : t.wSaveAndCheck }),
|
|
1390
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mishu-btn", onClick: () => onDone(), children: t.sCancel })
|
|
1391
|
+
] }),
|
|
1392
|
+
existing ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "mishu-hint", children: [
|
|
1393
|
+
t.wAskSetup,
|
|
1394
|
+
" ",
|
|
1395
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AskAI, { topic: { kind: "watch-setup" }, lang, locale, openChat: openChat2, compact: true })
|
|
1396
|
+
] })
|
|
1397
|
+
] });
|
|
1398
|
+
}
|
|
1399
|
+
function hostOf(url) {
|
|
1400
|
+
try {
|
|
1401
|
+
return new URL(url).hostname;
|
|
1402
|
+
} catch {
|
|
1403
|
+
return "";
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1188
1406
|
|
|
1189
1407
|
// src/client/Board.tsx
|
|
1190
1408
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
@@ -1400,6 +1618,7 @@ function MishuBoard({ locale, openChat: openChat2 }) {
|
|
|
1400
1618
|
{ id: "all", label: t.all },
|
|
1401
1619
|
{ id: "github", label: t.sourceGithub },
|
|
1402
1620
|
{ id: "mail", label: t.sourceMail },
|
|
1621
|
+
{ id: "watch", label: t.sourceWatch },
|
|
1403
1622
|
{ id: "manual", label: t.sourceManual }
|
|
1404
1623
|
];
|
|
1405
1624
|
const template = `minmax(0, ${widths[0]}fr) 10px minmax(0, ${widths[1]}fr) 10px minmax(0, ${widths[2]}fr)`;
|
|
@@ -1435,6 +1654,11 @@ function MishuBoard({ locale, openChat: openChat2 }) {
|
|
|
1435
1654
|
m.text,
|
|
1436
1655
|
m.state === "error" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AskAI, { topic: { kind: "mail-error", accountId: m.id }, lang, locale, openChat: openChat2, compact: true }) : null
|
|
1437
1656
|
] }, m.id)),
|
|
1657
|
+
board.sources.watch.map((w) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
1658
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("i", { className: `mishu-dot ${w.state}` }),
|
|
1659
|
+
w.text,
|
|
1660
|
+
w.state === "error" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AskAI, { topic: { kind: "watch-error", watchId: w.id }, lang, locale, openChat: openChat2, compact: true }) : null
|
|
1661
|
+
] }, `watch-${w.id}`)),
|
|
1438
1662
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mishu-btn", disabled: syncing, onClick: () => {
|
|
1439
1663
|
void sync();
|
|
1440
1664
|
}, children: syncing ? t.syncing : t.sync })
|
package/lib/core/ask.js
CHANGED
|
@@ -16,6 +16,7 @@ import { mailReportKey } from "./mail/sync.js";
|
|
|
16
16
|
import { isActive } from "./matters.js";
|
|
17
17
|
import { CATEGORY_LABELS, describeRepeat, occurrence, remindDay } from "./recur.js";
|
|
18
18
|
import { dayOf, formatMoment } from "./time.js";
|
|
19
|
+
import { watchById, watchReportKey } from "./watch/source.js";
|
|
19
20
|
/** The tools the model can use in that chat, so it can look further and act only with your OK. */
|
|
20
21
|
function toolsHint(lang, id) {
|
|
21
22
|
return lang === 'zh'
|
|
@@ -128,5 +129,20 @@ export function buildAsk(store, request, options) {
|
|
|
128
129
|
? `我想让小秘书只读地查看我的「${preset.label.zh}」收件箱,需要开通 IMAP 并拿到客户端授权码(或应用专用密码)。请一步步教我怎么做,包括在网页上点哪里、要注意什么。\n\n小秘书给的说明:${preset.howTo.zh}\nIMAP 服务器:${preset.host || '(自己填)'}:${preset.port}${lines('\n测试连接时报错:', request.error)}`
|
|
129
130
|
: `I want my secretary to read my ${preset.label.en} inbox (read-only), which needs IMAP turned on and an app password. Walk me through it step by step, including where to click and what to watch out for.\n\nThe secretary's note: ${preset.howTo.en}\nIMAP server: ${preset.host || '(enter your own)'}:${preset.port}${lines('\nThe connection test said: ', request.error)}`;
|
|
130
131
|
}
|
|
132
|
+
case 'watch-error': {
|
|
133
|
+
const source = watchById(store, request.watchId);
|
|
134
|
+
if (source === undefined)
|
|
135
|
+
throw notFound();
|
|
136
|
+
const report = store.get(watchReportKey(source.id));
|
|
137
|
+
const error = report?.error?.[lang] ?? (zh ? '(没有记录到错误)' : '(no error recorded)');
|
|
138
|
+
return zh
|
|
139
|
+
? `我让小秘书盯着「${source.label}」这个网址,但它读不到内容,帮我弄明白原因、怎么解决:\n\n网址:${source.url}\n多久看一次:${source.intervalMinutes} 分钟\n我关注的字:${source.keywords.length === 0 ? '(没设,有任何变化就告诉我)' : source.keywords.join('、')}\n报错:${error}\n\n小秘书是怎么读的:普通 GET,20 秒超时,能读 RSS/Atom、JSON 列表或普通网页。如果这个网站要登录才能看,或者防爬,告诉我该怎么办。`
|
|
140
|
+
: `I asked my secretary to keep an eye on ${source.url}, but it cannot read it. Help me understand why and what to do:\n\nAddress: ${source.url}\nCheck every: ${source.intervalMinutes} minutes\nWords I watch for: ${source.keywords.length === 0 ? '(none — tell me about any change)' : source.keywords.join(', ')}\nError: ${error}\n\nHow it reads: a plain GET with a 20-second timeout, working on RSS/Atom, a JSON list or a plain page. If the site needs a login or blocks robots, tell me what to do instead.`;
|
|
141
|
+
}
|
|
142
|
+
case 'watch-setup': {
|
|
143
|
+
return zh
|
|
144
|
+
? '我想让小秘书帮我盯着一些我自己关心的东西:某个公告页、某个商品的价格、某个职位的招聘页、某个订阅源。请告诉我:哪些网址它读得到、哪些读不到(要登录的、全靠 JS 渲染的、防爬的),RSS 和普通网页分别会怎么处理,关键词该怎么写才能不漏也不吵,检查间隔设多长合适。'
|
|
145
|
+
: 'I want my secretary to watch a few things I care about myself: a page of announcements, the price of something, a job posting, a feed. Tell me which addresses it can read and which it cannot (login walls, JS-only pages, bot blocking), how it treats a feed versus a plain page, how to write the words I watch for so nothing is missed and nothing is noisy, and how often to check.';
|
|
146
|
+
}
|
|
131
147
|
}
|
|
132
148
|
}
|
package/lib/core/board.js
CHANGED
|
@@ -13,6 +13,7 @@ import { decode } from "./i18n.js";
|
|
|
13
13
|
import { manualExplain, storedExplain } from "./explain.js";
|
|
14
14
|
import { isActive } from "./matters.js";
|
|
15
15
|
import { mailReportKey } from "./mail/sync.js";
|
|
16
|
+
import { watchReportKey } from "./watch/source.js";
|
|
16
17
|
import { CATEGORY_LABELS, describeRepeat, occurrence, remindDay } from "./recur.js";
|
|
17
18
|
import { addDays, dayOf, dayStart, daysBetween, formatMoment, relativeDay, weekday } from "./time.js";
|
|
18
19
|
const zhOr = (lang, zh, en) => (lang === 'zh' ? zh : en);
|
|
@@ -43,7 +44,7 @@ function matterCard(store, m, today, now, lang, column) {
|
|
|
43
44
|
}
|
|
44
45
|
else {
|
|
45
46
|
const label = CATEGORY_LABELS[m.kind];
|
|
46
|
-
const where = m.source === 'mail' ? zhOr(lang, '邮件', 'Mail') : 'GitHub';
|
|
47
|
+
const where = m.source === 'mail' ? zhOr(lang, '邮件', 'Mail') : m.source === 'watch' ? zhOr(lang, '关注', 'Watching') : 'GitHub';
|
|
47
48
|
meta = manual && label !== undefined ? label[lang] : `${where} · ${reasonLabel(m.reason, lang)}`;
|
|
48
49
|
}
|
|
49
50
|
const actions = column === 'handle'
|
|
@@ -61,7 +62,8 @@ function matterCard(store, m, today, now, lang, column) {
|
|
|
61
62
|
explain.why += zhOr(lang, `你说了"明天再说",${m.snoozeUntil} 会回到"要你处理"。`, ` You put it off; it comes back on ${m.snoozeUntil}.`);
|
|
62
63
|
}
|
|
63
64
|
// The open button says what it opens ("Turn off Actions", "Open the failing log").
|
|
64
|
-
|
|
65
|
+
const source = manual ? 'manual' : m.source === 'mail' ? 'mail' : m.source === 'watch' ? 'watch' : 'github';
|
|
66
|
+
return { id: m.id, target: 'matter', source, title, meta, note: note === '' ? null : note, url: explain?.link?.url ?? m.url, tone, actions, explain };
|
|
65
67
|
}
|
|
66
68
|
export function buildBoard(store, options) {
|
|
67
69
|
const { now, lang } = options;
|
|
@@ -121,6 +123,14 @@ export function buildBoard(store, options) {
|
|
|
121
123
|
return { id: account.id, label: account.label, state: 'error', text: zhOr(lang, `${account.label}:${report.error.zh}`, `${account.label}: ${report.error.en}`) };
|
|
122
124
|
return { id: account.id, label: account.label, state: 'ok', text: zhOr(lang, `${account.label} ${formatMoment(report.at, now)} 收信`, `${account.label} checked ${formatMoment(report.at, now)}`) };
|
|
123
125
|
});
|
|
126
|
+
const watch = (options.watches ?? []).map((source) => {
|
|
127
|
+
const report = store.get(watchReportKey(source.id));
|
|
128
|
+
if (report === undefined)
|
|
129
|
+
return { id: source.id, label: source.label, state: 'never', text: zhOr(lang, `${source.label} 还没看过`, `${source.label} not checked yet`) };
|
|
130
|
+
if (report.error !== undefined)
|
|
131
|
+
return { id: source.id, label: source.label, state: 'error', text: zhOr(lang, `${source.label}:${report.error.zh}`, `${source.label}: ${report.error.en}`) };
|
|
132
|
+
return { id: source.id, label: source.label, state: 'ok', text: zhOr(lang, `${source.label} ${formatMoment(report.at, now)} 看过`, `${source.label} checked ${formatMoment(report.at, now)}`) };
|
|
133
|
+
});
|
|
124
134
|
const github = !options.github.enabled
|
|
125
135
|
? { state: 'off', text: zhOr(lang, 'GitHub 未开启', 'GitHub off') }
|
|
126
136
|
: last === undefined
|
|
@@ -143,12 +153,14 @@ export function buildBoard(store, options) {
|
|
|
143
153
|
parts.push(zhOr(lang, 'GitHub 这次没同步成。', 'The last GitHub sync failed.'));
|
|
144
154
|
for (const box of mail.filter(b => b.state === 'error'))
|
|
145
155
|
parts.push(zhOr(lang, `邮箱「${box.label}」这次没收成。`, `Mail "${box.label}" could not be checked.`));
|
|
156
|
+
for (const box of watch.filter(b => b.state === 'error'))
|
|
157
|
+
parts.push(zhOr(lang, `关注「${box.label}」这次没读成。`, `Watch "${box.label}" could not be read.`));
|
|
146
158
|
return {
|
|
147
159
|
lang,
|
|
148
160
|
at: now,
|
|
149
161
|
summary: parts.join(lang === 'zh' ? '' : ' '),
|
|
150
162
|
metrics: { handle: handle.length, overdue, dueSoon, closedToday, shadow },
|
|
151
|
-
sources: { github, manual: { series: store.activeSeries().length }, mail },
|
|
163
|
+
sources: { github, manual: { series: store.activeSeries().length }, mail, watch },
|
|
152
164
|
markRead: options.github.markRead,
|
|
153
165
|
columns: {
|
|
154
166
|
handle: handle.map(m => matterCard(store, m, today, now, lang, 'handle')),
|
package/lib/core/brief.js
CHANGED
|
@@ -10,6 +10,7 @@ import { isActive } from "./matters.js";
|
|
|
10
10
|
import { mailReportKey } from "./mail/sync.js";
|
|
11
11
|
import { CATEGORY_LABELS } from "./recur.js";
|
|
12
12
|
import { dayOf, daysBetween, formatMoment, relativeDay, weekday } from "./time.js";
|
|
13
|
+
import { watchReportKey } from "./watch/source.js";
|
|
13
14
|
export const REASON_LABELS = {
|
|
14
15
|
merged: bi('已合并', 'merged'),
|
|
15
16
|
closed: bi('已关闭', 'closed'),
|
|
@@ -43,6 +44,10 @@ export const REASON_LABELS = {
|
|
|
43
44
|
'mail-cc': bi('只是抄送', 'Cc only'),
|
|
44
45
|
'read-in-mail': bi('你在邮箱里看过了', 'read in your mail'),
|
|
45
46
|
'account-removed': bi('邮箱账户已移除', 'mail account removed'),
|
|
47
|
+
'watch-new': bi('有新内容', 'new entries'),
|
|
48
|
+
'watch-changed': bi('内容有变化', 'changed'),
|
|
49
|
+
'watch-keyword': bi('提到你关注的字', 'mentioned your words'),
|
|
50
|
+
'watch-removed': bi('你删掉了这个关注', 'watch removed'),
|
|
46
51
|
};
|
|
47
52
|
export function reasonLabel(reason, lang) {
|
|
48
53
|
if (reason === null)
|
|
@@ -75,6 +80,8 @@ function sourceTag(m, lang) {
|
|
|
75
80
|
return 'GitHub';
|
|
76
81
|
if (m.source === 'mail')
|
|
77
82
|
return pick(bi('邮件', 'Mail'), lang);
|
|
83
|
+
if (m.source === 'watch')
|
|
84
|
+
return pick(bi('关注', 'Watching'), lang);
|
|
78
85
|
return m.source;
|
|
79
86
|
}
|
|
80
87
|
/** One matter as a few lines. */
|
|
@@ -181,6 +188,15 @@ export function renderBrief(store, options) {
|
|
|
181
188
|
else
|
|
182
189
|
out.push(zh ? `邮件「${account.label}」:${formatMoment(report.at, now)} 收信,${report.messages} 封未读。` : `Mail "${account.label}": checked ${formatMoment(report.at, now)}, ${report.messages} unread.`);
|
|
183
190
|
}
|
|
191
|
+
for (const source of options.watches ?? []) {
|
|
192
|
+
const report = store.get(watchReportKey(source.id));
|
|
193
|
+
if (report === undefined)
|
|
194
|
+
out.push(zh ? `关注「${source.label}」:还没看过。` : `Watching "${source.label}": not checked yet.`);
|
|
195
|
+
else if (report.error !== undefined)
|
|
196
|
+
out.push(zh ? `关注「${source.label}」:${formatMoment(report.at, now)} 没读成:${report.error.zh}` : `Watching "${source.label}": failed at ${formatMoment(report.at, now)}: ${report.error.en}`);
|
|
197
|
+
else
|
|
198
|
+
out.push(zh ? `关注「${source.label}」:${formatMoment(report.at, now)} 看过,${report.items} 条内容。` : `Watching "${source.label}": checked ${formatMoment(report.at, now)}, ${report.items} entries.`);
|
|
199
|
+
}
|
|
184
200
|
return out.join('\n');
|
|
185
201
|
}
|
|
186
202
|
/** Recently closed matters with reasons, so a wrong closure can be reopened. */
|
package/lib/core/index.js
CHANGED
|
@@ -29,6 +29,9 @@ export { DEFAULT_NOTIFY, deliver, inQuiet, localClock, notifySettings, notifyTic
|
|
|
29
29
|
export { restartPlan, scheduleRestart } from "./restart.js";
|
|
30
30
|
export { FileSecrets, KeychainOrFile, MemorySecrets, secretEnvName, secretFromEnv, systemSecrets } from "./secrets.js";
|
|
31
31
|
export { deleteMailAccount, effectiveSettings, mailAccounts, saveMailAccount, saveSettings } from "./settings.js";
|
|
32
|
+
export { deleteWatch, dueWatches, entryKey, parseKeywords, resolveWatch, saveWatch, watchById, watchReportKey, watchSources, WATCH_SOURCE, } from "./watch/source.js";
|
|
33
|
+
export { describeWatchError, httpFetch, parseFeed, parseJson, parsePage, plainText, readWatchBody } from "./watch/fetch.js";
|
|
34
|
+
export { describeKeywords, describeWatch, diffItems, judgeWatch, syncWatch } from "./watch/sync.js";
|
|
32
35
|
export const DEFAULT_HOME = '~/.mishu';
|
|
33
36
|
/** `~` and `~/…` expand to the home directory on every platform. */
|
|
34
37
|
export function expandHome(path) {
|