dsh-mishu 0.5.0 → 0.5.4
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 +12 -6
- package/lib/client.js +137 -12
- package/lib/core/ask.js +2 -2
- package/lib/core/exec.js +68 -0
- package/lib/core/github/client.js +10 -0
- package/lib/core/github/token.js +79 -0
- package/lib/core/index.js +3 -27
- package/lib/core/mail/account.js +5 -3
- package/lib/core/mail/sync.js +6 -3
- package/lib/core/restart.js +149 -0
- package/lib/core/secrets.js +71 -23
- package/lib/core/settings.js +7 -3
- package/lib/dsh/api.js +29 -11
- package/lib/dsh/runtime.js +8 -2
- package/lib/types/core/ask.d.ts +1 -0
- package/lib/types/core/exec.d.ts +28 -0
- package/lib/types/core/github/client.d.ts +2 -0
- package/lib/types/core/github/token.d.ts +39 -0
- package/lib/types/core/index.d.ts +4 -12
- package/lib/types/core/mail/sync.d.ts +2 -2
- package/lib/types/core/restart.d.ts +73 -0
- package/lib/types/core/secrets.d.ts +13 -3
- package/lib/types/core/settings.d.ts +4 -2
- package/lib/types/dsh/runtime.d.ts +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
同一封邮件的往来回复算一件事。你在邮件客户端里看过之后,对应的事项会自动关闭。
|
|
56
56
|
|
|
57
|
-
**密码放在系统钥匙串**:macOS 钥匙串、Windows 凭据管理器、Linux 密钥环(Secret Service
|
|
57
|
+
**密码放在系统钥匙串**:macOS 钥匙串、Windows 凭据管理器、Linux 密钥环(Secret Service)。密码只在设置页输入一次,直接交给钥匙串,不写进任何文件,接口也从不返回它。没有钥匙串的电脑(比如没有桌面的 Linux 服务器),设置页会说明原因;账户照样能保存,密码改用环境变量 `DSH_MISHU_SECRET_MAIL_<编号>` 提供。
|
|
58
58
|
|
|
59
59
|
### 用法
|
|
60
60
|
|
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
|
|
73
73
|
**设置**:点页面顶部的"设置":语言、各类别提前几天提醒、GitHub(开关、token 从哪来、测试连接、在 GitHub 上标已读的方式)、邮箱账户(添加、测试连接、现在收一次、编辑、删除)。飞书即将接入。
|
|
74
74
|
|
|
75
|
+
**重启 DSH**:页面顶部"设置"后面的按键。重装或升级插件后点它,桌面版会按正常方式退出(有任务在跑时会先问你),再自动打开。用 `dsh` 命令启动的 DSH 没法从页面重启(新的登录地址只会打印在那个终端里),按键会告诉你回终端按 Ctrl+C 再运行一次。Windows 和 Linux 桌面版的重启还没在真机上测过。
|
|
76
|
+
|
|
75
77
|
**文字命令**:
|
|
76
78
|
|
|
77
79
|
```
|
|
@@ -97,10 +99,11 @@ agent 可用的工具:`mishu_add`(交代,需要你审批)、`mishu_list`
|
|
|
97
99
|
|
|
98
100
|
### GitHub token
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
按顺序取,token 不写进任何文件:
|
|
101
103
|
|
|
102
104
|
1. 环境变量(默认 `GITHUB_TOKEN`)
|
|
103
|
-
2.
|
|
105
|
+
2. 你在设置页填的 token:先拿到 GitHub 上检查,再存进系统钥匙串。要用 classic token,勾选 `notifications`(看私有仓库再勾 `repo`);GitHub 的通知接口不认 fine-grained token
|
|
106
|
+
3. `gh auth token`:装了 GitHub CLI,并运行过 `gh auth login` 就行。从 Dock、开始菜单启动的 DSH 拿到的 PATH 往往很短,所以小秘书除了 PATH,还会去 Homebrew、`~/.local/bin`、winget、Scoop 等常见安装位置找 `gh`
|
|
104
107
|
|
|
105
108
|
### 安装和配置
|
|
106
109
|
|
|
@@ -177,7 +180,7 @@ Default warning times: documents 90 days, bills 3, birthdays 7, renewals 7, ever
|
|
|
177
180
|
|
|
178
181
|
A thread of replies is one item. Once you read it in your mail client, its item closes by itself.
|
|
179
182
|
|
|
180
|
-
**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.
|
|
183
|
+
**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), the settings page says why; accounts still save, and each password comes from the environment variable `DSH_MISHU_SECRET_MAIL_<id>`.
|
|
181
184
|
|
|
182
185
|
### Usage
|
|
183
186
|
|
|
@@ -195,6 +198,8 @@ Cards can be dragged: onto "Done for you" to mark done, onto "Keeping for you" t
|
|
|
195
198
|
|
|
196
199
|
**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). Feishu / Lark is coming.
|
|
197
200
|
|
|
201
|
+
**Restart DSH**: the button after "Settings" at the top of the page. After reinstalling or upgrading plugins, click it: the desktop app quits the normal way (asking first if a task is running) and opens again. DSH started with the `dsh` command cannot be restarted from the page (its new sign-in address is printed only in that terminal), so the button tells you to press Ctrl+C there and run it again. Restarting the Windows and Linux desktop apps is not yet tested on real machines.
|
|
202
|
+
|
|
198
203
|
**Text commands**:
|
|
199
204
|
|
|
200
205
|
```
|
|
@@ -220,10 +225,11 @@ Tools for the agent: `mishu_add` (needs your approval), `mishu_list`, `mishu_det
|
|
|
220
225
|
|
|
221
226
|
### GitHub token
|
|
222
227
|
|
|
223
|
-
|
|
228
|
+
It uses, in order, and never writes a token to a file:
|
|
224
229
|
|
|
225
230
|
1. an environment variable (default `GITHUB_TOKEN`)
|
|
226
|
-
2.
|
|
231
|
+
2. a token you paste on the settings page: checked with GitHub first, then kept in the system keychain. Use a classic token with the `notifications` scope (plus `repo` for private repositories); GitHub's notifications API does not take fine-grained tokens
|
|
232
|
+
3. `gh auth token`: if the GitHub CLI is installed and you have run `gh auth login`. DSH started from the Dock or Start menu often gets a short PATH, so besides PATH the secretary looks for `gh` where Homebrew, `~/.local/bin`, winget and Scoop put it
|
|
227
233
|
|
|
228
234
|
### Install and configure
|
|
229
235
|
|
package/lib/client.js
CHANGED
|
@@ -60,10 +60,16 @@ var mishuApi = {
|
|
|
60
60
|
add: (lang, item) => post("/add", { lang, item }),
|
|
61
61
|
ask: (lang, ask) => post("/ask", { lang, ask })
|
|
62
62
|
};
|
|
63
|
+
var restartApi = {
|
|
64
|
+
info: (lang) => post("/restart/info", { lang }),
|
|
65
|
+
restart: (lang) => post("/restart", { lang })
|
|
66
|
+
};
|
|
63
67
|
var settingsApi = {
|
|
64
68
|
load: (lang) => post("/settings", { lang }),
|
|
65
69
|
save: (lang, settings) => post("/settings/save", { lang, settings }),
|
|
66
70
|
testGithub: (lang) => post("/github/test", { lang }),
|
|
71
|
+
saveToken: (lang, token) => post("/github/token/save", { lang, token }),
|
|
72
|
+
deleteToken: (lang) => post("/github/token/delete", { lang }),
|
|
67
73
|
saveMail: (lang, account, password) => post("/mail/save", { lang, account, password }),
|
|
68
74
|
testMail: (lang, account, password) => post("/mail/test", { lang, account, password }),
|
|
69
75
|
deleteMail: (lang, id) => post("/mail/delete", { lang, id }),
|
|
@@ -132,6 +138,10 @@ var zh = {
|
|
|
132
138
|
resize: "\u62D6\u52A8\u8C03\u6574\u5BBD\u5EA6\uFF0C\u53CC\u51FB\u6062\u590D\u7B49\u5BBD",
|
|
133
139
|
tabBoard: "\u770B\u677F",
|
|
134
140
|
tabSettings: "\u8BBE\u7F6E",
|
|
141
|
+
restart: "\u91CD\u542F DSH",
|
|
142
|
+
restartTip: "\u91CD\u88C5\u6216\u5347\u7EA7\u63D2\u4EF6\u540E\uFF0C\u53EF\u4EE5\u7528\u8FD9\u4E2A\u6309\u952E\u91CD\u542F\u672C DSH",
|
|
143
|
+
restartConfirm: "\u91CD\u542F DSH\uFF1F\u5B89\u88C5\u63D2\u4EF6\u6216\u8005\u63D2\u4EF6\u66F4\u65B0\u540E\u9700\u8981\u91CD\u542F DSH\u3002",
|
|
144
|
+
restarting: "\u6B63\u5728\u91CD\u542F\u2026\u7A97\u53E3\u4F1A\u5173\u6389\u518D\u81EA\u52A8\u6253\u5F00",
|
|
135
145
|
all: "\u5168\u90E8",
|
|
136
146
|
sourceGithub: "GitHub",
|
|
137
147
|
sourceMail: "\u90AE\u4EF6",
|
|
@@ -147,6 +157,16 @@ var zh = {
|
|
|
147
157
|
sToken: "Token",
|
|
148
158
|
sTokenEnv: (name) => `\u4ECE\u73AF\u5883\u53D8\u91CF ${name} \u8BFB\u53D6`,
|
|
149
159
|
sTokenGh: "\u7528 GitHub CLI\uFF08gh\uFF09\u767B\u5F55\u7684\u8D26\u53F7\uFF0Ctoken \u5728\u7CFB\u7EDF\u94A5\u5319\u4E32\u91CC",
|
|
160
|
+
sTokenKeychain: "\u4F60\u5728\u8FD9\u91CC\u586B\u7684 token\uFF0C\u5B58\u5728\u7CFB\u7EDF\u94A5\u5319\u4E32\u91CC",
|
|
161
|
+
sTokenPaste: "\u586B\u4E00\u4E2A GitHub token",
|
|
162
|
+
sTokenHow: "\u7528 classic token\uFF0C\u52FE\u9009 notifications\uFF1B\u8981\u770B\u79C1\u6709\u4ED3\u5E93\u518D\u52FE repo\u3002GitHub \u7684\u901A\u77E5\u63A5\u53E3\u4E0D\u8BA4 fine-grained token\u3002token \u53EA\u5B58\u8FDB\u7CFB\u7EDF\u94A5\u5319\u4E32\uFF0C\u4E0D\u5199\u8FDB\u4EFB\u4F55\u6587\u4EF6\u3002",
|
|
163
|
+
sTokenNew: "\u53BB GitHub \u751F\u6210\uFF08\u6743\u9650\u5DF2\u52FE\u597D\uFF09",
|
|
164
|
+
sTokenSave: "\u68C0\u67E5\u5E76\u4FDD\u5B58",
|
|
165
|
+
sTokenSaved: (login) => `\u4FDD\u5B58\u597D\u4E86\uFF0C\u8D26\u53F7 ${login}`,
|
|
166
|
+
sTokenReplace: "\u6362\u4E00\u4E2A token",
|
|
167
|
+
sTokenRemove: "\u79FB\u9664 token",
|
|
168
|
+
sTokenRemoveConfirm: "\u4ECE\u94A5\u5319\u4E32\u91CC\u79FB\u9664\u8FD9\u4E2A token\uFF1F",
|
|
169
|
+
sTokenNoKeychain: (env, why) => `\u8FD9\u53F0\u7535\u8111\u6CA1\u6709\u53EF\u7528\u7684\u7CFB\u7EDF\u94A5\u5319\u4E32\uFF08${why}\uFF09\uFF0Ctoken \u6CA1\u6CD5\u4FDD\u5B58\u3002\u53EF\u4EE5\u5728\u542F\u52A8 DSH \u7684\u73AF\u5883\u91CC\u8BBE\u7F6E ${env}\uFF0C\u518D\u91CD\u542F DSH\u3002`,
|
|
150
170
|
sTest: "\u6D4B\u8BD5\u8FDE\u63A5",
|
|
151
171
|
sTesting: "\u6B63\u5728\u6D4B\u8BD5\u2026",
|
|
152
172
|
sGithubOk: (login) => `\u8FDE\u4E0A\u4E86\uFF0C\u8D26\u53F7 ${login}`,
|
|
@@ -160,7 +180,8 @@ var zh = {
|
|
|
160
180
|
sMail: "\u90AE\u7BB1",
|
|
161
181
|
sMailPrivacy: "\u53EA\u8BFB\uFF1A\u5C0F\u79D8\u4E66\u7528\u53EA\u8BFB\u65B9\u5F0F\u6253\u5F00\u6536\u4EF6\u7BB1\uFF0C\u53EA\u770B\u53D1\u4EF6\u4EBA\u3001\u6536\u4EF6\u4EBA\u3001\u6807\u9898\u548C\u51E0\u4E2A\u90AE\u4EF6\u5934\uFF0C\u4E0D\u8BFB\u6B63\u6587\uFF0C\u4E5F\u4E0D\u4F1A\u628A\u90AE\u4EF6\u6807\u6210\u5DF2\u8BFB\u3002",
|
|
162
182
|
sSecretsAt: (kind) => `\u5BC6\u7801\u4FDD\u5B58\u5728\uFF1A${kind}\uFF08\u52A0\u5BC6\u4FDD\u5B58\uFF0C\u4E0D\u5199\u8FDB\u4EFB\u4F55\u6587\u4EF6\uFF09`,
|
|
163
|
-
sSecretsNone:
|
|
183
|
+
sSecretsNone: (why) => `\u8FD9\u53F0\u7535\u8111\u4E0A\u6CA1\u6709\u53EF\u7528\u7684\u7CFB\u7EDF\u94A5\u5319\u4E32\uFF1A${why}\u3002\u5BC6\u7801\u4E0D\u4F1A\u88AB\u4FDD\u5B58\uFF1B\u8D26\u6237\u7167\u6837\u80FD\u4FDD\u5B58\uFF0C\u5BC6\u7801\u7528\u73AF\u5883\u53D8\u91CF\u63D0\u4F9B\uFF0C\u53D8\u91CF\u540D\u5199\u5728\u6BCF\u4E2A\u8D26\u6237\u4E0B\u9762\u3002`,
|
|
184
|
+
sPasswordTestOnly: "\u6CA1\u6709\u94A5\u5319\u4E32\uFF1A\u8FD9\u91CC\u586B\u7684\u53EA\u7528\u6765\u6D4B\u8BD5\u8FDE\u63A5\uFF0C\u4E0D\u4F1A\u4FDD\u5B58",
|
|
164
185
|
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" },
|
|
165
186
|
sNoAccounts: "\u8FD8\u6CA1\u6709\u6DFB\u52A0\u90AE\u7BB1\u3002",
|
|
166
187
|
sAddAccount: "+ \u6DFB\u52A0\u90AE\u7BB1",
|
|
@@ -180,7 +201,7 @@ var zh = {
|
|
|
180
201
|
sSince: "\u53EA\u770B\u6700\u8FD1\u51E0\u5929\u7684\u672A\u8BFB\u90AE\u4EF6",
|
|
181
202
|
sEnabled: "\u542F\u7528",
|
|
182
203
|
sMailOk: (n) => `\u8FDE\u4E0A\u4E86\uFF0C\u6700\u8FD1\u6709 ${n} \u5C01\u672A\u8BFB\u90AE\u4EF6`,
|
|
183
|
-
sEnvHint: (name) => `\u6CA1\u6709\u94A5\u5319\u4E32\
|
|
204
|
+
sEnvHint: (name) => `\u6CA1\u6709\u627E\u5230\u5BC6\u7801\u3002\u70B9\u300C\u7F16\u8F91\u300D\u91CD\u65B0\u586B\u4E00\u6B21\uFF1B\u8FD9\u53F0\u7535\u8111\u6CA1\u6709\u94A5\u5319\u4E32\u7684\u8BDD\uFF0C\u5728\u542F\u52A8 DSH \u7684\u73AF\u5883\u91CC\u8BBE\u7F6E ${name}=\u6388\u6743\u7801\uFF0C\u518D\u91CD\u542F DSH\u3002`,
|
|
184
205
|
sNeverChecked: "\u8FD8\u6CA1\u6536\u8FC7",
|
|
185
206
|
sFeishu: "\u98DE\u4E66",
|
|
186
207
|
sComing: "\u5373\u5C06\u63A5\u5165",
|
|
@@ -245,6 +266,10 @@ var en = {
|
|
|
245
266
|
resize: "Drag to resize; double-click to make them equal",
|
|
246
267
|
tabBoard: "Board",
|
|
247
268
|
tabSettings: "Settings",
|
|
269
|
+
restart: "Restart DSH",
|
|
270
|
+
restartTip: "After reinstalling or upgrading plugins, use this to restart DSH",
|
|
271
|
+
restartConfirm: "Restart DSH? DSH needs a restart after you install or update plugins.",
|
|
272
|
+
restarting: "Restarting\u2026 the window closes and opens again",
|
|
248
273
|
all: "All",
|
|
249
274
|
sourceGithub: "GitHub",
|
|
250
275
|
sourceMail: "Mail",
|
|
@@ -260,6 +285,16 @@ var en = {
|
|
|
260
285
|
sToken: "Token",
|
|
261
286
|
sTokenEnv: (name) => `Read from the environment variable ${name}`,
|
|
262
287
|
sTokenGh: "The account the GitHub CLI (gh) is signed in to; the token stays in the system keychain",
|
|
288
|
+
sTokenKeychain: "The token you pasted here, kept in the system keychain",
|
|
289
|
+
sTokenPaste: "Paste a GitHub token",
|
|
290
|
+
sTokenHow: "Use a classic token with the notifications scope, plus repo for private repositories. GitHub's notifications API does not take fine-grained tokens. The token goes to the system keychain only, never to a file.",
|
|
291
|
+
sTokenNew: "Create one on GitHub (scopes pre-ticked)",
|
|
292
|
+
sTokenSave: "Check and save",
|
|
293
|
+
sTokenSaved: (login) => `Saved; signed in as ${login}`,
|
|
294
|
+
sTokenReplace: "Use another token",
|
|
295
|
+
sTokenRemove: "Remove token",
|
|
296
|
+
sTokenRemoveConfirm: "Remove this token from the keychain?",
|
|
297
|
+
sTokenNoKeychain: (env, why) => `No usable system keychain on this computer (${why}), so the token cannot be saved. Set ${env} in the environment DSH starts from, then restart DSH.`,
|
|
263
298
|
sTest: "Test connection",
|
|
264
299
|
sTesting: "Testing\u2026",
|
|
265
300
|
sGithubOk: (login) => `Connected as ${login}`,
|
|
@@ -273,7 +308,8 @@ var en = {
|
|
|
273
308
|
sMail: "Mail",
|
|
274
309
|
sMailPrivacy: "Read-only: the secretary opens your inbox read-only and looks at sender, recipients, subject and a few headers only. It never reads bodies and never marks mail read.",
|
|
275
310
|
sSecretsAt: (kind) => `Passwords are kept in: ${kind} (encrypted, never written to a file)`,
|
|
276
|
-
sSecretsNone:
|
|
311
|
+
sSecretsNone: (why) => `No usable system keychain on this computer: ${why}. Passwords are not saved; you can still save accounts and give each password through the environment variable shown under it.`,
|
|
312
|
+
sPasswordTestOnly: "No keychain: used only to test the connection, not saved",
|
|
277
313
|
secretKinds: { macos: "the macOS Keychain", windows: "Windows Credential Manager", linux: "the Linux keyring (Secret Service)", memory: "memory (tests)" },
|
|
278
314
|
sNoAccounts: "No mail account yet.",
|
|
279
315
|
sAddAccount: "+ Add a mail account",
|
|
@@ -293,7 +329,7 @@ var en = {
|
|
|
293
329
|
sSince: "Only look at unread mail from the last \u2026 days",
|
|
294
330
|
sEnabled: "Enabled",
|
|
295
331
|
sMailOk: (n) => `Connected; ${n} unread messages recently`,
|
|
296
|
-
sEnvHint: (name) => `
|
|
332
|
+
sEnvHint: (name) => `No password found. Click Edit and enter it again; if this computer has no keychain, set ${name}=<app password> in the environment DSH starts from, then restart DSH.`,
|
|
297
333
|
sNeverChecked: "Not checked yet",
|
|
298
334
|
sFeishu: "Feishu / Lark",
|
|
299
335
|
sComing: "Coming soon",
|
|
@@ -366,6 +402,7 @@ function MishuSettings({ lang, locale, openChat: openChat2 }) {
|
|
|
366
402
|
const [lead, setLead] = (0, import_react2.useState)({});
|
|
367
403
|
const [githubTest, setGithubTest] = (0, import_react2.useState)();
|
|
368
404
|
const [confirmOn, setConfirmOn] = (0, import_react2.useState)(false);
|
|
405
|
+
const [pasting, setPasting] = (0, import_react2.useState)(false);
|
|
369
406
|
const [editing, setEditing] = (0, import_react2.useState)();
|
|
370
407
|
const take = (result, saved) => {
|
|
371
408
|
if (result.ok) {
|
|
@@ -426,15 +463,27 @@ function MishuSettings({ lang, locale, openChat: openChat2 }) {
|
|
|
426
463
|
] }),
|
|
427
464
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-row-explain", children: [
|
|
428
465
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "mishu-row-label", children: t.sToken }),
|
|
429
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: token.found ? "" : "mishu-danger", children: token.found ? token.from === "env" ? t.sTokenEnv(view.github.tokenEnv) : t.sTokenGh : token.text })
|
|
466
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: token.found ? "" : "mishu-danger", children: token.found ? token.from === "env" ? t.sTokenEnv(view.github.tokenEnv) : token.from === "keychain" ? t.sTokenKeychain : t.sTokenGh : token.text })
|
|
430
467
|
] }),
|
|
431
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-actions", children: [
|
|
468
|
+
token.found ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-actions", children: [
|
|
432
469
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn", onClick: () => {
|
|
433
470
|
setGithubTest(t.sTesting);
|
|
434
471
|
void settingsApi.testGithub(locale()).then((r) => setGithubTest(r.ok ? t.sGithubOk(r.login) : `${t.failed}${r.message}`));
|
|
435
472
|
}, children: t.sTest }),
|
|
473
|
+
token.from === "env" || pasting ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn", onClick: () => setPasting(true), children: t.sTokenReplace }),
|
|
474
|
+
token.from === "keychain" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn mishu-btn-quiet", onClick: () => {
|
|
475
|
+
if (window.confirm(t.sTokenRemoveConfirm)) void settingsApi.deleteToken(locale()).then((r) => {
|
|
476
|
+
setGithubTest(void 0);
|
|
477
|
+
take(r);
|
|
478
|
+
});
|
|
479
|
+
}, children: t.sTokenRemove }) : null,
|
|
436
480
|
githubTest === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "mishu-hint", children: githubTest })
|
|
437
|
-
] }),
|
|
481
|
+
] }) : null,
|
|
482
|
+
!token.found || pasting ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TokenForm, { view, lang, locale, onSaved: (next, login) => {
|
|
483
|
+
setPasting(false);
|
|
484
|
+
setGithubTest(t.sTokenSaved(login));
|
|
485
|
+
take({ ok: true, view: next });
|
|
486
|
+
}, onCancel: token.found ? () => setPasting(false) : void 0 }) : null,
|
|
438
487
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "mishu-field", children: [
|
|
439
488
|
t.sMarkRead,
|
|
440
489
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { value: view.settings.markRead, onChange: (e) => {
|
|
@@ -461,7 +510,7 @@ function MishuSettings({ lang, locale, openChat: openChat2 }) {
|
|
|
461
510
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "mishu-panel", children: [
|
|
462
511
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t.sMail }),
|
|
463
512
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mishu-hint", children: t.sMailPrivacy }),
|
|
464
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: view.secrets.available ? "mishu-hint" : "mishu-hint mishu-warn", children: view.secrets.available ? t.sSecretsAt(secretKind) : t.sSecretsNone }),
|
|
513
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: view.secrets.available ? "mishu-hint" : "mishu-hint mishu-warn", children: view.secrets.available ? t.sSecretsAt(secretKind) : t.sSecretsNone(view.secrets.why ?? secretKind) }),
|
|
465
514
|
view.accounts.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mishu-empty", children: t.sNoAccounts }) : view.accounts.map((account) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
466
515
|
AccountRow,
|
|
467
516
|
{
|
|
@@ -485,6 +534,45 @@ function MishuSettings({ lang, locale, openChat: openChat2 }) {
|
|
|
485
534
|
] })
|
|
486
535
|
] });
|
|
487
536
|
}
|
|
537
|
+
function TokenForm({ view, lang, locale, onSaved, onCancel }) {
|
|
538
|
+
const t = TEXT[lang];
|
|
539
|
+
const [token, setToken] = (0, import_react2.useState)("");
|
|
540
|
+
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
541
|
+
const [message, setMessage] = (0, import_react2.useState)();
|
|
542
|
+
if (!view.secrets.available) {
|
|
543
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mishu-hint mishu-warn", children: t.sTokenNoKeychain(view.github.tokenEnv, view.secrets.why ?? "") });
|
|
544
|
+
}
|
|
545
|
+
const save = async () => {
|
|
546
|
+
setBusy(true);
|
|
547
|
+
setMessage(void 0);
|
|
548
|
+
const r = await settingsApi.saveToken(locale(), token);
|
|
549
|
+
setBusy(false);
|
|
550
|
+
if (r.ok) {
|
|
551
|
+
setToken("");
|
|
552
|
+
onSaved(r.view, r.login);
|
|
553
|
+
} else {
|
|
554
|
+
setMessage(r.message);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-add", children: [
|
|
558
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "mishu-field", children: [
|
|
559
|
+
t.sTokenPaste,
|
|
560
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "password", value: token, autoComplete: "off", spellCheck: false, placeholder: "ghp_\u2026", onChange: (e) => {
|
|
561
|
+
setToken(e.target.value);
|
|
562
|
+
setMessage(void 0);
|
|
563
|
+
} })
|
|
564
|
+
] }),
|
|
565
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mishu-hint", children: t.sTokenHow }),
|
|
566
|
+
message === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mishu-preview mishu-danger", children: message }),
|
|
567
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-actions", children: [
|
|
568
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn", onClick: () => window.open(view.github.newTokenUrl, "_blank", "noopener"), children: t.sTokenNew }),
|
|
569
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn primary", disabled: busy || token.trim() === "", onClick: () => {
|
|
570
|
+
void save();
|
|
571
|
+
}, children: busy ? t.sTesting : t.sTokenSave }),
|
|
572
|
+
onCancel === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn", onClick: onCancel, children: t.sCancel })
|
|
573
|
+
] })
|
|
574
|
+
] });
|
|
575
|
+
}
|
|
488
576
|
function AccountRow({ account, lang, locale, openChat: openChat2, onEdit, onView }) {
|
|
489
577
|
const t = TEXT[lang];
|
|
490
578
|
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
@@ -578,11 +666,12 @@ function AccountForm({ draft, view, lang, locale, openChat: openChat2, onDone })
|
|
|
578
666
|
] }),
|
|
579
667
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "mishu-field", children: [
|
|
580
668
|
t.sPassword,
|
|
581
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "password", value: password, autoComplete: "new-password", placeholder: existing ? t.sPasswordKeep : "", onChange: (e) => {
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "password", value: password, autoComplete: "new-password", placeholder: existing && view.secrets.available ? t.sPasswordKeep : "", onChange: (e) => {
|
|
582
670
|
setPassword(e.target.value);
|
|
583
671
|
setMessage(void 0);
|
|
584
672
|
} })
|
|
585
673
|
] }),
|
|
674
|
+
view.secrets.available ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mishu-hint mishu-warn", children: t.sPasswordTestOnly }),
|
|
586
675
|
custom ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-row", children: [
|
|
587
676
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "mishu-field", children: [
|
|
588
677
|
t.sHost,
|
|
@@ -603,7 +692,10 @@ function AccountForm({ draft, view, lang, locale, openChat: openChat2, onDone })
|
|
|
603
692
|
t.sEnabled
|
|
604
693
|
] })
|
|
605
694
|
] }),
|
|
606
|
-
message === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
695
|
+
message === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `mishu-preview ${message.ok ? "" : "mishu-danger"}`, children: [
|
|
696
|
+
message.text,
|
|
697
|
+
message.ok || !preset.supported ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AskAI, { topic: { kind: "mail-setup", preset: preset.id, error: message.text }, lang, locale, openChat: openChat2, compact: true })
|
|
698
|
+
] }),
|
|
607
699
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mishu-actions", children: [
|
|
608
700
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "mishu-btn", disabled: busy || !preset.supported, onClick: () => {
|
|
609
701
|
void test();
|
|
@@ -796,7 +888,10 @@ function MishuBoard({ locale, openChat: openChat2 }) {
|
|
|
796
888
|
);
|
|
797
889
|
if (board === void 0) {
|
|
798
890
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-page", children: [
|
|
799
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mishu-head", children: /* @__PURE__ */ (0, import_jsx_runtime3.
|
|
891
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mishu-head", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-title", children: [
|
|
892
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: t.title }),
|
|
893
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RestartButton, { lang, locale })
|
|
894
|
+
] }) }),
|
|
800
895
|
error === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mishu-empty", children: t.loading }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-error", children: [
|
|
801
896
|
t.failed,
|
|
802
897
|
error
|
|
@@ -840,7 +935,8 @@ function MishuBoard({ locale, openChat: openChat2 }) {
|
|
|
840
935
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-tabs", role: "tablist", children: [
|
|
841
936
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === "board", className: tab === "board" ? "active" : "", onClick: () => setTab("board"), children: t.tabBoard }),
|
|
842
937
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === "settings", className: tab === "settings" ? "active" : "", onClick: () => setTab("settings"), children: t.tabSettings })
|
|
843
|
-
] })
|
|
938
|
+
] }),
|
|
939
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RestartButton, { lang, locale })
|
|
844
940
|
] }),
|
|
845
941
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-sources", children: [
|
|
846
942
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
@@ -1086,6 +1182,33 @@ function AddForm({ lang, locale, onAdded }) {
|
|
|
1086
1182
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mishu-hint", children: t.chatHint })
|
|
1087
1183
|
] });
|
|
1088
1184
|
}
|
|
1185
|
+
function RestartButton({ lang, locale }) {
|
|
1186
|
+
const t = TEXT[lang];
|
|
1187
|
+
const [why, setWhy] = (0, import_react3.useState)();
|
|
1188
|
+
const [status, setStatus] = (0, import_react3.useState)();
|
|
1189
|
+
(0, import_react3.useEffect)(() => {
|
|
1190
|
+
void restartApi.info(locale()).then((r) => {
|
|
1191
|
+
if (r.ok && !r.supported) setWhy(r.why);
|
|
1192
|
+
});
|
|
1193
|
+
}, [locale]);
|
|
1194
|
+
const restart = async () => {
|
|
1195
|
+
if (why !== void 0) {
|
|
1196
|
+
setStatus({ ok: false, text: why });
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (!window.confirm(t.restartConfirm)) return;
|
|
1200
|
+
setStatus({ ok: true, text: t.restarting });
|
|
1201
|
+
const r = await restartApi.restart(locale());
|
|
1202
|
+
if (!r.ok) setStatus({ ok: false, text: r.message });
|
|
1203
|
+
};
|
|
1204
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1205
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mishu-btn", title: why === void 0 ? t.restartTip : `${t.restartTip}
|
|
1206
|
+
${why}`, disabled: status?.ok === true, onClick: () => {
|
|
1207
|
+
void restart();
|
|
1208
|
+
}, children: t.restart }),
|
|
1209
|
+
status === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: status.ok ? "mishu-hint" : "mishu-hint mishu-warn", children: status.text })
|
|
1210
|
+
] });
|
|
1211
|
+
}
|
|
1089
1212
|
|
|
1090
1213
|
// src/client/styles.ts
|
|
1091
1214
|
var v = (name, fallback) => `var(--dsw-alias-${name}, ${fallback})`;
|
|
@@ -1160,6 +1283,8 @@ var STYLES = `
|
|
|
1160
1283
|
.mishu-field input,.mishu-field select{font:inherit;font-size:13px;color:${TEXT2};background:${LAYER2};border:1px solid ${LINE_STRONG};border-radius:6px;padding:4px 6px;width:100%}
|
|
1161
1284
|
.mishu-row{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
|
1162
1285
|
.mishu-preview{white-space:pre-wrap;font-size:12px;background:${LAYER2};border-radius:6px;padding:6px 8px;margin:4px 0 6px;color:${TEXT2}}
|
|
1286
|
+
.mishu-preview.mishu-danger{color:${DANGER}}
|
|
1287
|
+
.mishu-add>.mishu-hint{margin-bottom:8px}
|
|
1163
1288
|
.mishu-title{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
|
1164
1289
|
.mishu-tabs{display:flex;gap:2px;background:${LAYER1};border-radius:8px;padding:2px}
|
|
1165
1290
|
.mishu-tabs button{font:inherit;font-size:13px;color:${MUTED};background:transparent;border:0;border-radius:6px;padding:3px 12px;cursor:pointer}
|
package/lib/core/ask.js
CHANGED
|
@@ -125,8 +125,8 @@ export function buildAsk(store, request, options) {
|
|
|
125
125
|
case 'mail-setup': {
|
|
126
126
|
const preset = presetById(request.preset);
|
|
127
127
|
return zh
|
|
128
|
-
? `我想让小秘书只读地查看我的「${preset.label.zh}」收件箱,需要开通 IMAP 并拿到客户端授权码(或应用专用密码)。请一步步教我怎么做,包括在网页上点哪里、要注意什么。\n\n小秘书给的说明:${preset.howTo.zh}\nIMAP 服务器:${preset.host || '(自己填)'}:${preset.port}`
|
|
129
|
-
: `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}`;
|
|
128
|
+
? `我想让小秘书只读地查看我的「${preset.label.zh}」收件箱,需要开通 IMAP 并拿到客户端授权码(或应用专用密码)。请一步步教我怎么做,包括在网页上点哪里、要注意什么。\n\n小秘书给的说明:${preset.howTo.zh}\nIMAP 服务器:${preset.host || '(自己填)'}:${preset.port}${lines('\n测试连接时报错:', request.error)}`
|
|
129
|
+
: `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
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
package/lib/core/exec.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running the few helper programs the secretary uses (`security`,
|
|
3
|
+
* `secret-tool`, PowerShell, `gh`).
|
|
4
|
+
*
|
|
5
|
+
* An app started from the Dock, the Start menu or a service manager often
|
|
6
|
+
* gets a short PATH that misses where package managers put programs, so a
|
|
7
|
+
* program installed with Homebrew, winget or `~/.local/bin` looks missing.
|
|
8
|
+
* `findProgram` looks on PATH first, then in those usual places.
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import { accessSync, constants } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { posix, win32 } from 'node:path';
|
|
14
|
+
/** The usual install places of command-line programs, after PATH. */
|
|
15
|
+
export function usualDirs({ platform = process.platform, env = process.env, home = homedir() } = {}) {
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
const join = win32.join;
|
|
18
|
+
const local = env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
|
|
19
|
+
return [
|
|
20
|
+
join(env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0'),
|
|
21
|
+
join(env.ProgramFiles ?? 'C:\\Program Files', 'GitHub CLI'),
|
|
22
|
+
join(local, 'Programs', 'GitHub CLI'),
|
|
23
|
+
join(local, 'Microsoft', 'WinGet', 'Links'),
|
|
24
|
+
join(home, 'scoop', 'shims'),
|
|
25
|
+
join(env.ProgramData ?? 'C:\\ProgramData', 'chocolatey', 'bin'),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
const join = posix.join;
|
|
29
|
+
const common = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', join(home, '.local', 'bin'), join(home, 'bin')];
|
|
30
|
+
return platform === 'darwin'
|
|
31
|
+
? ['/opt/homebrew/bin', ...common, '/opt/local/bin']
|
|
32
|
+
: [...common, '/snap/bin', '/home/linuxbrew/.linuxbrew/bin', join(home, '.linuxbrew', 'bin')];
|
|
33
|
+
}
|
|
34
|
+
/** The full path of a program, or `undefined` when it is not installed anywhere we know of. */
|
|
35
|
+
export function findProgram(name, where = {}) {
|
|
36
|
+
const platform = where.platform ?? process.platform;
|
|
37
|
+
const env = where.env ?? process.env;
|
|
38
|
+
const path = platform === 'win32' ? win32 : posix;
|
|
39
|
+
const pathDirs = (env.PATH ?? env.Path ?? '').split(path.delimiter).filter(Boolean);
|
|
40
|
+
const names = platform === 'win32' && !/\.\w+$/.test(name) ? [`${name}.exe`, `${name}.cmd`] : [name];
|
|
41
|
+
for (const dir of [...pathDirs, ...usualDirs(where)]) {
|
|
42
|
+
for (const file of names) {
|
|
43
|
+
const full = path.join(dir, file);
|
|
44
|
+
try {
|
|
45
|
+
accessSync(full, platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
46
|
+
return full;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// not here
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Run a program; `input` goes to its stdin (secrets never go on the command line). Never rejects. */
|
|
56
|
+
export function runProgram(file, args, input, timeout = 15_000) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const child = execFile(file, args, { timeout, windowsHide: true, maxBuffer: 1 << 20 }, (error, stdout, stderr) => {
|
|
59
|
+
const code = error === null ? 0 : typeof error.code === 'number' ? error.code : 1;
|
|
60
|
+
const result = { code, stdout: String(stdout), stderr: String(stderr) };
|
|
61
|
+
if (error !== null && typeof error.code !== 'number')
|
|
62
|
+
result.failure = error.message;
|
|
63
|
+
resolve(result);
|
|
64
|
+
});
|
|
65
|
+
child.stdin?.on('error', () => { });
|
|
66
|
+
child.stdin?.end(input ?? '');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -89,6 +89,16 @@ export class GitHubClient {
|
|
|
89
89
|
user() {
|
|
90
90
|
return this.get('/user');
|
|
91
91
|
}
|
|
92
|
+
/** Whether this token may read notifications (fine-grained tokens get 403). */
|
|
93
|
+
async canReadNotifications() {
|
|
94
|
+
const url = `${API}/notifications?per_page=1`;
|
|
95
|
+
const { status, body } = await this.raw('GET', url);
|
|
96
|
+
if (status >= 200 && status < 300)
|
|
97
|
+
return true;
|
|
98
|
+
if (status === 403 || status === 404)
|
|
99
|
+
return false;
|
|
100
|
+
throw new GitHubError(status, url, body?.message ?? 'request failed');
|
|
101
|
+
}
|
|
92
102
|
repo(fullName) {
|
|
93
103
|
return this.get(`/repos/${fullName}`);
|
|
94
104
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the GitHub token comes from, in this order:
|
|
3
|
+
*
|
|
4
|
+
* 1. the environment variable named in the config (GITHUB_TOKEN)
|
|
5
|
+
* 2. a token you pasted on the settings page, kept in the system keychain
|
|
6
|
+
* 3. the GitHub CLI (`gh auth token`), which keeps its own in the keychain
|
|
7
|
+
*
|
|
8
|
+
* A token is never written to the notebook or a file.
|
|
9
|
+
*/
|
|
10
|
+
import { findProgram, runProgram } from "../exec.js";
|
|
11
|
+
import { bi, MishuError } from "../i18n.js";
|
|
12
|
+
import { GitHubClient, GitHubError } from "./client.js";
|
|
13
|
+
/** The keychain entry for a token pasted on the settings page. */
|
|
14
|
+
export const GITHUB_SECRET = 'github';
|
|
15
|
+
/** Opens GitHub's "new classic token" page with the two scopes ticked. */
|
|
16
|
+
export const NEW_TOKEN_URL = 'https://github.com/settings/tokens/new?scopes=notifications,repo&description=DSH%20mishu';
|
|
17
|
+
/** `ghPath` is for tests: a fixed path, or `null` for "not installed". */
|
|
18
|
+
export async function resolveGithubToken(options) {
|
|
19
|
+
const { envName, useGhCli, secrets } = options;
|
|
20
|
+
const fromEnv = envName === '' ? undefined : process.env[envName]?.trim();
|
|
21
|
+
if (fromEnv)
|
|
22
|
+
return { token: fromEnv, from: 'env' };
|
|
23
|
+
const stored = await secrets?.get(GITHUB_SECRET).catch(() => undefined);
|
|
24
|
+
if (stored)
|
|
25
|
+
return { token: stored, from: 'keychain' };
|
|
26
|
+
const none = bi(envName === '' ? '没有 GitHub token' : `没有 GitHub token(环境变量 ${envName} 是空的,钥匙串里也没有存)`, envName === '' ? 'no GitHub token' : `no GitHub token (environment variable ${envName} is empty and none is saved in the keychain)`);
|
|
27
|
+
const paste = bi('在小秘书的设置页填一个 token', 'paste a token on the secretary\'s settings page');
|
|
28
|
+
if (!useGhCli)
|
|
29
|
+
return { error: bi(`${none.zh}。${paste.zh}。`, `${none.en}. ${cap(paste.en)}.`) };
|
|
30
|
+
const gh = options.ghPath === undefined ? findProgram('gh') : options.ghPath ?? undefined;
|
|
31
|
+
if (gh === undefined) {
|
|
32
|
+
return {
|
|
33
|
+
error: bi(`${none.zh},这台电脑上也没找到 GitHub CLI(gh)。${paste.zh},或者装上 gh 后运行 gh auth login。`, `${none.en}, and the GitHub CLI (gh) is not installed on this computer. ${cap(paste.en)}, or install gh and run gh auth login.`),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const r = await runProgram(gh, ['auth', 'token'], undefined, 5000);
|
|
37
|
+
const token = r.stdout.trim();
|
|
38
|
+
if (r.code === 0 && token !== '')
|
|
39
|
+
return { token, from: 'gh' };
|
|
40
|
+
const why = (r.stderr.trim() || r.failure || `exit ${r.code}`).split('\n')[0].slice(0, 200);
|
|
41
|
+
return {
|
|
42
|
+
error: bi(`${none.zh},gh 也没给出 token(${why})。运行 gh auth login 登录,或者${paste.zh}。`, `${none.en}, and gh gave no token (${why}). Run gh auth login, or ${paste.en}.`),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function cap(text) {
|
|
46
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check a pasted token with GitHub, then keep it in the keychain. It must
|
|
50
|
+
* read notifications, which only classic tokens with the `notifications`
|
|
51
|
+
* scope can (fine-grained tokens cannot reach that API).
|
|
52
|
+
*/
|
|
53
|
+
export async function saveGithubToken(secrets, raw, fetch) {
|
|
54
|
+
const token = raw.trim();
|
|
55
|
+
if (!/^[\w-]{20,255}$/.test(token)) {
|
|
56
|
+
throw new MishuError(bi('这不像 GitHub token:应该是一串没有空格的字母数字,classic token 以 ghp_ 开头', 'That does not look like a GitHub token: it should be letters and digits without spaces; classic tokens start with ghp_'));
|
|
57
|
+
}
|
|
58
|
+
const status = await secrets.status();
|
|
59
|
+
if (!status.available)
|
|
60
|
+
throw new MishuError(bi(`没法保存:${status.why.zh}`, `Cannot save it: ${status.why.en}`));
|
|
61
|
+
const gh = new GitHubClient({ token, ...fetch === undefined ? {} : { fetch } });
|
|
62
|
+
let login;
|
|
63
|
+
try {
|
|
64
|
+
login = (await gh.user()).login;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof GitHubError && error.status === 401)
|
|
68
|
+
throw new MishuError(bi('GitHub 不认这个 token(可能填错了、过期了或被撤销了)', 'GitHub does not accept this token (mistyped, expired or revoked)'));
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (!await gh.canReadNotifications()) {
|
|
72
|
+
throw new MishuError(bi(`这个 token 属于 ${login},但读不了通知。GitHub 的通知接口只认 classic token:生成时勾选 notifications(要看私有仓库再勾 repo)。`, `This token belongs to ${login} but cannot read notifications. GitHub's notifications API only takes classic tokens: tick notifications when you create it (and repo for private repositories).`));
|
|
73
|
+
}
|
|
74
|
+
await secrets.set(GITHUB_SECRET, token);
|
|
75
|
+
return { login };
|
|
76
|
+
}
|
|
77
|
+
export async function deleteGithubToken(secrets) {
|
|
78
|
+
await secrets.delete(GITHUB_SECRET);
|
|
79
|
+
}
|
package/lib/core/index.js
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
* mishu core — everything that does not depend on DSH.
|
|
3
3
|
* Nothing under `src/core` may import DSH; `tests/boundary.test.ts` enforces it.
|
|
4
4
|
*/
|
|
5
|
-
import { execFile } from 'node:child_process';
|
|
6
5
|
import { homedir } from 'node:os';
|
|
7
6
|
import { join } from 'node:path';
|
|
8
|
-
import { bi } from "./i18n.js";
|
|
9
7
|
export { buildAsk, detailText } from "./ask.js";
|
|
10
8
|
export { buildBoard, buildDetail } from "./board.js";
|
|
11
9
|
export { renderBrief, renderClosed, renderMatter, reasonLabel, sortOpen } from "./brief.js";
|
|
@@ -13,6 +11,7 @@ export { GitHubClient, GitHubError } from "./github/client.js";
|
|
|
13
11
|
export { gatherFacts, Lookups, parseCheckSuiteTitle } from "./github/facts.js";
|
|
14
12
|
export { CLOSING_REASONS, judge } from "./github/rules.js";
|
|
15
13
|
export { syncGithub } from "./github/sync.js";
|
|
14
|
+
export { deleteGithubToken, GITHUB_SECRET, NEW_TOKEN_URL, resolveGithubToken, saveGithubToken } from "./github/token.js";
|
|
16
15
|
export { bi, decode, detectLang, errorText, LANGS, MishuError, pick } from "./i18n.js";
|
|
17
16
|
export { addSeries, describeActiveSeries, describeSeries, resolveSeries, stopSeries, tickManual, upcoming, } from "./manual.js";
|
|
18
17
|
export { DEFAULT_DEMOTION, isActive, isDemoted, setStatusByUser, snoozeByUser } from "./matters.js";
|
|
@@ -23,6 +22,8 @@ export { MAIL_PRESETS, presetById, secretAccount } from "./mail/account.js";
|
|
|
23
22
|
export { ImapFetcher } from "./mail/imap.js";
|
|
24
23
|
export { judgeMail } from "./mail/rules.js";
|
|
25
24
|
export { describeMailError, MAIL_SOURCE, mailReportKey, syncMail } from "./mail/sync.js";
|
|
25
|
+
export { findProgram } from "./exec.js";
|
|
26
|
+
export { restartPlan, scheduleRestart } from "./restart.js";
|
|
26
27
|
export { MemorySecrets, secretEnvName, secretFromEnv, systemSecrets } from "./secrets.js";
|
|
27
28
|
export { deleteMailAccount, effectiveSettings, mailAccounts, saveMailAccount, saveSettings } from "./settings.js";
|
|
28
29
|
export const DEFAULT_HOME = '~/.mishu';
|
|
@@ -38,28 +39,3 @@ export function mishuPaths(home) {
|
|
|
38
39
|
const root = expandHome(home);
|
|
39
40
|
return { root, db: join(root, 'mishu.db') };
|
|
40
41
|
}
|
|
41
|
-
/**
|
|
42
|
-
* A GitHub token without storing one: the named environment variable, else
|
|
43
|
-
* the GitHub CLI (`gh auth token`, which reads the system credential store).
|
|
44
|
-
*/
|
|
45
|
-
export async function resolveGithubToken(envName, useGhCli) {
|
|
46
|
-
const fromEnv = envName === '' ? undefined : process.env[envName]?.trim();
|
|
47
|
-
if (fromEnv)
|
|
48
|
-
return { token: fromEnv, from: 'env' };
|
|
49
|
-
if (!useGhCli)
|
|
50
|
-
return { error: bi(`没有 GitHub token:环境变量 ${envName} 是空的`, `no GitHub token: environment variable ${envName} is empty`) };
|
|
51
|
-
return new Promise((resolve) => {
|
|
52
|
-
execFile('gh', ['auth', 'token'], { timeout: 5000, windowsHide: true }, (error, stdout) => {
|
|
53
|
-
const token = stdout?.trim();
|
|
54
|
-
if (error !== null || !token) {
|
|
55
|
-
const why = error?.message ?? 'empty output';
|
|
56
|
-
resolve({
|
|
57
|
-
error: bi(`没有 GitHub token:环境变量 ${envName} 是空的,gh auth token 也没拿到(${why})。先运行 gh auth login,或设置 ${envName}`, `no GitHub token: environment variable ${envName} is empty and gh auth token failed (${why}). Run gh auth login, or set ${envName}`),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
resolve({ token, from: 'gh' });
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
package/lib/core/mail/account.js
CHANGED
|
@@ -9,7 +9,7 @@ import { bi, MishuError } from "../i18n.js";
|
|
|
9
9
|
export const MAIL_PRESETS = [
|
|
10
10
|
{
|
|
11
11
|
id: 'qq', label: bi('QQ 邮箱', 'QQ Mail'), host: 'imap.qq.com', port: 993, secure: true, supported: true,
|
|
12
|
-
howTo: bi('在 QQ 邮箱网页版:设置 →
|
|
12
|
+
howTo: bi('在 QQ 邮箱网页版:设置 → 账号与安全 → 安全设置(旧版界面是 设置 → 账户),开启「IMAP/SMTP 服务」,按提示验证后生成 16 位授权码。这里填授权码,不是 QQ 密码;改过 QQ 密码后,旧授权码会失效。', 'In QQ Mail on the web: Settings → Account and Security → Security (Settings → Account in the older layout), turn on "IMAP/SMTP", verify, and generate a 16-character authorization code. Enter that code here, not your QQ password; changing your QQ password expires old codes.'),
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
id: '163', label: bi('网易 163 邮箱', 'NetEase 163'), host: 'imap.163.com', port: 993, secure: true, supported: true,
|
|
@@ -53,8 +53,10 @@ export function resolveAccount(input, id) {
|
|
|
53
53
|
if (!preset.supported)
|
|
54
54
|
throw new MishuError(bi(`${preset.label.zh}暂时不支持:${preset.howTo.zh}`, `${preset.label.en} is not supported yet: ${preset.howTo.en}`));
|
|
55
55
|
const host = preset.id === 'custom' ? String(input.host ?? '').trim() : preset.host;
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// A name with a dot (imap.example.com), a full IPv4 address, or localhost; a bare number is a port typed in the wrong box.
|
|
57
|
+
if (!/^[\w-]+(\.[\w-]+)+$/.test(host) && host !== 'localhost' || /^\d+$/.test(host)) {
|
|
58
|
+
throw new MishuError(bi(`IMAP 服务器地址不对:${host || '(空)'},应该像 imap.example.com`, `invalid IMAP server: ${host || '(empty)'}; it should look like imap.example.com`));
|
|
59
|
+
}
|
|
58
60
|
const port = preset.id === 'custom' ? Number(input.port ?? 993) : preset.port;
|
|
59
61
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
60
62
|
throw new MishuError(bi('端口要是 1 到 65535 的整数', 'the port must be a whole number from 1 to 65535'));
|