dsh-theone 0.3.15 → 0.3.17
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 +2 -1
- package/README.zh.md +2 -1
- package/dist/client-locales.d.ts +17 -0
- package/dist/client-locales.js +34 -0
- package/dist/client.js +252 -10
- package/dist/index.d.ts +6 -0
- package/dist/index.js +34 -3
- package/dist/notices.d.ts +39 -0
- package/dist/notices.js +105 -0
- package/dist/settings-types.d.ts +3 -1
- package/dist/settings-types.js +2 -2
- package/dist/settings.js +3 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.js +8 -0
- package/dist/update.d.ts +21 -1
- package/dist/update.js +92 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Each project gets its own background session that reasons, runs tools and compac
|
|
|
57
57
|
2. Open **Plugins → Add plugin**, paste `https://github.com/YunongDai2005/dsh-theone`, and install.
|
|
58
58
|
3. Click **TheOne · Main chat** in the sidebar and start talking.
|
|
59
59
|
|
|
60
|
-
From the command line: `dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`. Works with DSH `0.2.0-rc.2` and Node.js 24; the interface follows DSH's language (English / Simplified Chinese). Installing `dsh-theone` from npm also works, but DSH's package manager only accepts a version once it has been published for 24 hours, so the GitHub address gets new versions first.
|
|
60
|
+
From the command line: `dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`. Works with DSH `0.2.0-rc.2` and Node.js 24; the interface follows DSH's language (English / Simplified Chinese). Installing `dsh-theone` from npm also works, but DSH's package manager only accepts a version once it has been published for 24 hours, so the GitHub address gets new versions first. When an update is newer than that, the Update button explains it and can exempt TheOne alone so it installs right away.
|
|
61
61
|
|
|
62
62
|
> Unofficial community project, maintained independently. It is not affiliated with or endorsed by DeepSeek.
|
|
63
63
|
|
|
@@ -104,6 +104,7 @@ The database location and entry identifier switch TheOne to different data, so t
|
|
|
104
104
|
- DSH keeps the original conversations and tool results; TheOne keeps only its catalog, summaries, links and routing records in its own SQLite database (`$DSH_HOME/theone/contexts.db`, `~/.dsh/theone/` by default).
|
|
105
105
|
- Text sent to the router or written into briefings has API keys, passwords and similar secrets removed.
|
|
106
106
|
- A topic marked **Do not share** never appears in other topics' briefings, recent-chat excerpts or lookups.
|
|
107
|
+
- Notices from the author are read from `https://yulid.org/theone/notice.json` with a plain request that sends none of your data; turn them off in Settings.
|
|
107
108
|
- When main chat grows long, DSH compacts it through TheOne: frequently used topics keep longer summaries and their latest turns, rarely used ones keep a short status. This makes no model call.
|
|
108
109
|
- When a new version is out, an **Update** button appears on the right of the TheOne entry in the sidebar: one click installs it through DSH's plugin manager and reloads TheOne in place, without restarting DSH (a DSH without plugin hot reload applies it at the next restart). Your topics stay in the database.
|
|
109
110
|
</details>
|
package/README.zh.md
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
2. 打开 **插件 → 添加插件**,粘贴 `https://github.com/YunongDai2005/dsh-theone`,点击安装。
|
|
58
58
|
3. 点击左侧 **TheOne · 主聊天**,开始说话。
|
|
59
59
|
|
|
60
|
-
命令行安装:`dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`。兼容 DSH `0.2.0-rc.2` 与 Node.js 24;界面跟随 DSH 的语言(简体中文 / English)。也可以从 npm 安装 `dsh-theone`,但 DSH 的包管理器只接受发布满 24 小时的版本,所以用 GitHub
|
|
60
|
+
命令行安装:`dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`。兼容 DSH `0.2.0-rc.2` 与 Node.js 24;界面跟随 DSH 的语言(简体中文 / English)。也可以从 npm 安装 `dsh-theone`,但 DSH 的包管理器只接受发布满 24 小时的版本,所以用 GitHub 地址安装能最早拿到新版本。遇到这种情况,「更新」按钮会说明原因,并可以只为 TheOne 放行、立即安装。
|
|
61
61
|
|
|
62
62
|
> 非官方社区项目,由社区成员独立维护,与 DeepSeek 不存在隶属或背书关系。
|
|
63
63
|
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
- 原始对话和工具结果由 DSH 保存;TheOne 只在自己的 SQLite 数据库(`$DSH_HOME/theone/contexts.db`,默认 `~/.dsh/theone/`)里保存目录、摘要、关联和路由记录。
|
|
105
105
|
- 发给路由和写进参考简报的内容会去掉 API Key、密码等敏感信息。
|
|
106
106
|
- 标记为「不共享」的话题,不会出现在其他话题的简报、最近对话和查阅结果里。
|
|
107
|
+
- 作者发布的公告从 `https://yulid.org/theone/notice.json` 读取,只是一次普通的下载,不发送你的任何数据;可以在设置里关闭。
|
|
107
108
|
- 主聊天变长时,DSH 通过 TheOne 压缩它:常用话题保留较长摘要和最近几轮,不常用的只保留简短状态,不额外调用模型。
|
|
108
109
|
- 有新版本时,左侧 TheOne 入口右边会出现 **更新** 按钮,点一下由 DSH 的插件管理器安装,并就地重新加载 TheOne,不用重启 DSH(不支持插件热加载的 DSH 会在下次启动时生效);话题都保存在数据库里,不受影响。
|
|
109
110
|
</details>
|
package/dist/client-locales.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export declare const zh: {
|
|
|
5
5
|
readonly 'gateway.opening': "正在打开 TheOne 主聊天…";
|
|
6
6
|
readonly 'gateway.error': "主聊天暂时无法打开,请检查 DSH 连接和 TheOne 插件状态。";
|
|
7
7
|
readonly retry: "重试";
|
|
8
|
+
readonly 'settings.other': "其他";
|
|
9
|
+
readonly 'settings.notices': "显示公告";
|
|
10
|
+
readonly 'settings.help.notices': "显示 TheOne 作者发布的公告(新版本、重要提醒等)。只从 yulid.org 读取一个公告文件,不发送任何你的数据。";
|
|
11
|
+
readonly 'notice.label': "公告";
|
|
12
|
+
readonly 'notice.more': "查看详情";
|
|
13
|
+
readonly 'notice.ok': "知道了";
|
|
14
|
+
readonly 'notice.close': "关闭这条公告";
|
|
8
15
|
readonly 'bg.label': "后台";
|
|
9
16
|
readonly 'bg.follow': "默认";
|
|
10
17
|
readonly 'bg.title': "后台模型:分配话题和干活都用它";
|
|
@@ -17,6 +24,16 @@ export declare const zh: {
|
|
|
17
24
|
readonly 'update.available': "更新";
|
|
18
25
|
readonly 'update.installing': "更新中…";
|
|
19
26
|
readonly 'update.restart': "重启生效";
|
|
27
|
+
readonly 'update.waiting': "新版本";
|
|
28
|
+
readonly 'update.waitingHint': "有新版本 {latest},发布还不满 24 小时。点击查看如何现在安装。";
|
|
29
|
+
readonly 'age.title': "新版本发布还不满 24 小时";
|
|
30
|
+
readonly 'age.why': "DSH 用 pnpm 安装插件,它默认只安装发布满 24 小时的版本,用来防范被篡改的新包。TheOne {version} 刚发布不久,所以暂时装不上。";
|
|
31
|
+
readonly 'age.allowHow': "你可以只为 TheOne 放行:在这个 DSH 配置的 pnpm 设置(pnpm-workspace.yaml)里把 dsh-theone 加入例外,然后立即安装。其他插件仍然受 24 小时规则保护。";
|
|
32
|
+
readonly 'age.cannot': "当前无法自动修改这个 DSH 配置的 pnpm 设置。可以等它满 24 小时,或改用 GitHub 地址重新安装。";
|
|
33
|
+
readonly 'age.readyAt': "不放行的话,{time} 之后就能直接更新。";
|
|
34
|
+
readonly 'age.wait': "等满 24 小时";
|
|
35
|
+
readonly 'age.allow': "放行并更新";
|
|
36
|
+
readonly 'age.ok': "知道了";
|
|
20
37
|
readonly 'update.reloading': "正在重新加载…";
|
|
21
38
|
readonly 'update.reloadingHint': "TheOne 正在用新版本重新加载,DSH 不需要重启;完成后页面会自动刷新。";
|
|
22
39
|
readonly 'update.busy': "有回复正在进行,等它结束后再点更新。";
|
package/dist/client-locales.js
CHANGED
|
@@ -5,6 +5,13 @@ export const zh = {
|
|
|
5
5
|
'gateway.opening': '正在打开 TheOne 主聊天…',
|
|
6
6
|
'gateway.error': '主聊天暂时无法打开,请检查 DSH 连接和 TheOne 插件状态。',
|
|
7
7
|
'retry': '重试',
|
|
8
|
+
'settings.other': "其他",
|
|
9
|
+
'settings.notices': "显示公告",
|
|
10
|
+
'settings.help.notices': "显示 TheOne 作者发布的公告(新版本、重要提醒等)。只从 yulid.org 读取一个公告文件,不发送任何你的数据。",
|
|
11
|
+
'notice.label': "公告",
|
|
12
|
+
'notice.more': "查看详情",
|
|
13
|
+
'notice.ok': "知道了",
|
|
14
|
+
'notice.close': "关闭这条公告",
|
|
8
15
|
'bg.label': '后台',
|
|
9
16
|
'bg.follow': '默认',
|
|
10
17
|
'bg.title': '后台模型:分配话题和干活都用它',
|
|
@@ -17,6 +24,16 @@ export const zh = {
|
|
|
17
24
|
'update.available': '更新',
|
|
18
25
|
'update.installing': '更新中…',
|
|
19
26
|
'update.restart': '重启生效',
|
|
27
|
+
'update.waiting': "新版本",
|
|
28
|
+
'update.waitingHint': "有新版本 {latest},发布还不满 24 小时。点击查看如何现在安装。",
|
|
29
|
+
'age.title': "新版本发布还不满 24 小时",
|
|
30
|
+
'age.why': "DSH 用 pnpm 安装插件,它默认只安装发布满 24 小时的版本,用来防范被篡改的新包。TheOne {version} 刚发布不久,所以暂时装不上。",
|
|
31
|
+
'age.allowHow': "你可以只为 TheOne 放行:在这个 DSH 配置的 pnpm 设置(pnpm-workspace.yaml)里把 dsh-theone 加入例外,然后立即安装。其他插件仍然受 24 小时规则保护。",
|
|
32
|
+
'age.cannot': "当前无法自动修改这个 DSH 配置的 pnpm 设置。可以等它满 24 小时,或改用 GitHub 地址重新安装。",
|
|
33
|
+
'age.readyAt': "不放行的话,{time} 之后就能直接更新。",
|
|
34
|
+
'age.wait': "等满 24 小时",
|
|
35
|
+
'age.allow': "放行并更新",
|
|
36
|
+
'age.ok': "知道了",
|
|
20
37
|
'update.reloading': '正在重新加载…',
|
|
21
38
|
'update.reloadingHint': 'TheOne 正在用新版本重新加载,DSH 不需要重启;完成后页面会自动刷新。',
|
|
22
39
|
'update.busy': '有回复正在进行,等它结束后再点更新。',
|
|
@@ -197,6 +214,13 @@ export const en = {
|
|
|
197
214
|
'gateway.opening': 'Opening TheOne main chat…',
|
|
198
215
|
'gateway.error': 'Main chat could not open. Check your DSH connection and TheOne plugin status.',
|
|
199
216
|
'retry': 'Retry',
|
|
217
|
+
'settings.other': "Other",
|
|
218
|
+
'settings.notices': "Show notices",
|
|
219
|
+
'settings.help.notices': "Show notices from TheOne's author (new versions, important reminders). TheOne only reads one notice file from yulid.org and sends none of your data.",
|
|
220
|
+
'notice.label': "Notice",
|
|
221
|
+
'notice.more': "Read more",
|
|
222
|
+
'notice.ok': "Got it",
|
|
223
|
+
'notice.close': "Close this notice",
|
|
200
224
|
'bg.label': 'Background',
|
|
201
225
|
'bg.follow': 'Default',
|
|
202
226
|
'bg.title': 'Background model: routes and does the work',
|
|
@@ -209,6 +233,16 @@ export const en = {
|
|
|
209
233
|
'update.available': 'Update',
|
|
210
234
|
'update.installing': 'Updating…',
|
|
211
235
|
'update.restart': 'Restart to apply',
|
|
236
|
+
'update.waiting': "New version",
|
|
237
|
+
'update.waitingHint': "{latest} is out but less than 24 hours old. Click to see how to install it now.",
|
|
238
|
+
'age.title': "This version is less than 24 hours old",
|
|
239
|
+
'age.why': "DSH installs plugins with pnpm, which by default only installs versions published at least 24 hours ago, as a guard against tampered packages. TheOne {version} was published recently, so it cannot be installed yet.",
|
|
240
|
+
'age.allowHow': "You can exempt TheOne only: dsh-theone is added to the exceptions in this DSH profile's pnpm settings (pnpm-workspace.yaml) and installed now. Other plugins keep the 24-hour rule.",
|
|
241
|
+
'age.cannot': "This DSH profile's pnpm settings cannot be changed from here. Wait 24 hours, or reinstall from the GitHub address.",
|
|
242
|
+
'age.readyAt': "Without the exemption, you can update after {time}.",
|
|
243
|
+
'age.wait': "Wait 24 hours",
|
|
244
|
+
'age.allow': "Exempt and update",
|
|
245
|
+
'age.ok': "OK",
|
|
212
246
|
'update.reloading': 'Reloading…',
|
|
213
247
|
'update.reloadingHint': 'TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.',
|
|
214
248
|
'update.busy': 'A reply is in progress. Update once it finishes.',
|
package/dist/client.js
CHANGED
|
@@ -38,7 +38,8 @@ var EDITABLE_SETTINGS_KEYS = [
|
|
|
38
38
|
"maxResponseChars",
|
|
39
39
|
"linkScope",
|
|
40
40
|
"routeNotice",
|
|
41
|
-
"contextsPath"
|
|
41
|
+
"contextsPath",
|
|
42
|
+
"notices"
|
|
42
43
|
];
|
|
43
44
|
|
|
44
45
|
// src/client-navigation.ts
|
|
@@ -88,6 +89,13 @@ var zh = {
|
|
|
88
89
|
"gateway.opening": "\u6B63\u5728\u6253\u5F00 TheOne \u4E3B\u804A\u5929\u2026",
|
|
89
90
|
"gateway.error": "\u4E3B\u804A\u5929\u6682\u65F6\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u68C0\u67E5 DSH \u8FDE\u63A5\u548C TheOne \u63D2\u4EF6\u72B6\u6001\u3002",
|
|
90
91
|
"retry": "\u91CD\u8BD5",
|
|
92
|
+
"settings.other": "\u5176\u4ED6",
|
|
93
|
+
"settings.notices": "\u663E\u793A\u516C\u544A",
|
|
94
|
+
"settings.help.notices": "\u663E\u793A TheOne \u4F5C\u8005\u53D1\u5E03\u7684\u516C\u544A\uFF08\u65B0\u7248\u672C\u3001\u91CD\u8981\u63D0\u9192\u7B49\uFF09\u3002\u53EA\u4ECE yulid.org \u8BFB\u53D6\u4E00\u4E2A\u516C\u544A\u6587\u4EF6\uFF0C\u4E0D\u53D1\u9001\u4EFB\u4F55\u4F60\u7684\u6570\u636E\u3002",
|
|
95
|
+
"notice.label": "\u516C\u544A",
|
|
96
|
+
"notice.more": "\u67E5\u770B\u8BE6\u60C5",
|
|
97
|
+
"notice.ok": "\u77E5\u9053\u4E86",
|
|
98
|
+
"notice.close": "\u5173\u95ED\u8FD9\u6761\u516C\u544A",
|
|
91
99
|
"bg.label": "\u540E\u53F0",
|
|
92
100
|
"bg.follow": "\u9ED8\u8BA4",
|
|
93
101
|
"bg.title": "\u540E\u53F0\u6A21\u578B\uFF1A\u5206\u914D\u8BDD\u9898\u548C\u5E72\u6D3B\u90FD\u7528\u5B83",
|
|
@@ -100,6 +108,16 @@ var zh = {
|
|
|
100
108
|
"update.available": "\u66F4\u65B0",
|
|
101
109
|
"update.installing": "\u66F4\u65B0\u4E2D\u2026",
|
|
102
110
|
"update.restart": "\u91CD\u542F\u751F\u6548",
|
|
111
|
+
"update.waiting": "\u65B0\u7248\u672C",
|
|
112
|
+
"update.waitingHint": "\u6709\u65B0\u7248\u672C {latest}\uFF0C\u53D1\u5E03\u8FD8\u4E0D\u6EE1 24 \u5C0F\u65F6\u3002\u70B9\u51FB\u67E5\u770B\u5982\u4F55\u73B0\u5728\u5B89\u88C5\u3002",
|
|
113
|
+
"age.title": "\u65B0\u7248\u672C\u53D1\u5E03\u8FD8\u4E0D\u6EE1 24 \u5C0F\u65F6",
|
|
114
|
+
"age.why": "DSH \u7528 pnpm \u5B89\u88C5\u63D2\u4EF6\uFF0C\u5B83\u9ED8\u8BA4\u53EA\u5B89\u88C5\u53D1\u5E03\u6EE1 24 \u5C0F\u65F6\u7684\u7248\u672C\uFF0C\u7528\u6765\u9632\u8303\u88AB\u7BE1\u6539\u7684\u65B0\u5305\u3002TheOne {version} \u521A\u53D1\u5E03\u4E0D\u4E45\uFF0C\u6240\u4EE5\u6682\u65F6\u88C5\u4E0D\u4E0A\u3002",
|
|
115
|
+
"age.allowHow": "\u4F60\u53EF\u4EE5\u53EA\u4E3A TheOne \u653E\u884C\uFF1A\u5728\u8FD9\u4E2A DSH \u914D\u7F6E\u7684 pnpm \u8BBE\u7F6E\uFF08pnpm-workspace.yaml\uFF09\u91CC\u628A dsh-theone \u52A0\u5165\u4F8B\u5916\uFF0C\u7136\u540E\u7ACB\u5373\u5B89\u88C5\u3002\u5176\u4ED6\u63D2\u4EF6\u4ECD\u7136\u53D7 24 \u5C0F\u65F6\u89C4\u5219\u4FDD\u62A4\u3002",
|
|
116
|
+
"age.cannot": "\u5F53\u524D\u65E0\u6CD5\u81EA\u52A8\u4FEE\u6539\u8FD9\u4E2A DSH \u914D\u7F6E\u7684 pnpm \u8BBE\u7F6E\u3002\u53EF\u4EE5\u7B49\u5B83\u6EE1 24 \u5C0F\u65F6\uFF0C\u6216\u6539\u7528 GitHub \u5730\u5740\u91CD\u65B0\u5B89\u88C5\u3002",
|
|
117
|
+
"age.readyAt": "\u4E0D\u653E\u884C\u7684\u8BDD\uFF0C{time} \u4E4B\u540E\u5C31\u80FD\u76F4\u63A5\u66F4\u65B0\u3002",
|
|
118
|
+
"age.wait": "\u7B49\u6EE1 24 \u5C0F\u65F6",
|
|
119
|
+
"age.allow": "\u653E\u884C\u5E76\u66F4\u65B0",
|
|
120
|
+
"age.ok": "\u77E5\u9053\u4E86",
|
|
103
121
|
"update.reloading": "\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u2026",
|
|
104
122
|
"update.reloadingHint": "TheOne \u6B63\u5728\u7528\u65B0\u7248\u672C\u91CD\u65B0\u52A0\u8F7D\uFF0CDSH \u4E0D\u9700\u8981\u91CD\u542F\uFF1B\u5B8C\u6210\u540E\u9875\u9762\u4F1A\u81EA\u52A8\u5237\u65B0\u3002",
|
|
105
123
|
"update.busy": "\u6709\u56DE\u590D\u6B63\u5728\u8FDB\u884C\uFF0C\u7B49\u5B83\u7ED3\u675F\u540E\u518D\u70B9\u66F4\u65B0\u3002",
|
|
@@ -280,6 +298,13 @@ var en = {
|
|
|
280
298
|
"gateway.opening": "Opening TheOne main chat\u2026",
|
|
281
299
|
"gateway.error": "Main chat could not open. Check your DSH connection and TheOne plugin status.",
|
|
282
300
|
"retry": "Retry",
|
|
301
|
+
"settings.other": "Other",
|
|
302
|
+
"settings.notices": "Show notices",
|
|
303
|
+
"settings.help.notices": "Show notices from TheOne's author (new versions, important reminders). TheOne only reads one notice file from yulid.org and sends none of your data.",
|
|
304
|
+
"notice.label": "Notice",
|
|
305
|
+
"notice.more": "Read more",
|
|
306
|
+
"notice.ok": "Got it",
|
|
307
|
+
"notice.close": "Close this notice",
|
|
283
308
|
"bg.label": "Background",
|
|
284
309
|
"bg.follow": "Default",
|
|
285
310
|
"bg.title": "Background model: routes and does the work",
|
|
@@ -292,6 +317,16 @@ var en = {
|
|
|
292
317
|
"update.available": "Update",
|
|
293
318
|
"update.installing": "Updating\u2026",
|
|
294
319
|
"update.restart": "Restart to apply",
|
|
320
|
+
"update.waiting": "New version",
|
|
321
|
+
"update.waitingHint": "{latest} is out but less than 24 hours old. Click to see how to install it now.",
|
|
322
|
+
"age.title": "This version is less than 24 hours old",
|
|
323
|
+
"age.why": "DSH installs plugins with pnpm, which by default only installs versions published at least 24 hours ago, as a guard against tampered packages. TheOne {version} was published recently, so it cannot be installed yet.",
|
|
324
|
+
"age.allowHow": "You can exempt TheOne only: dsh-theone is added to the exceptions in this DSH profile's pnpm settings (pnpm-workspace.yaml) and installed now. Other plugins keep the 24-hour rule.",
|
|
325
|
+
"age.cannot": "This DSH profile's pnpm settings cannot be changed from here. Wait 24 hours, or reinstall from the GitHub address.",
|
|
326
|
+
"age.readyAt": "Without the exemption, you can update after {time}.",
|
|
327
|
+
"age.wait": "Wait 24 hours",
|
|
328
|
+
"age.allow": "Exempt and update",
|
|
329
|
+
"age.ok": "OK",
|
|
295
330
|
"update.reloading": "Reloading\u2026",
|
|
296
331
|
"update.reloadingHint": "TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.",
|
|
297
332
|
"update.busy": "A reply is in progress. Update once it finishes.",
|
|
@@ -555,9 +590,14 @@ function apply(ctx) {
|
|
|
555
590
|
updateListeners.delete(listener);
|
|
556
591
|
};
|
|
557
592
|
};
|
|
558
|
-
const readUpdate = async (method = "GET") => {
|
|
593
|
+
const readUpdate = async (method = "GET", body) => {
|
|
559
594
|
try {
|
|
560
|
-
const response = await fetch("/api/theone/update", {
|
|
595
|
+
const response = await fetch("/api/theone/update", {
|
|
596
|
+
method,
|
|
597
|
+
signal: lifetime.signal,
|
|
598
|
+
cache: "no-store",
|
|
599
|
+
...body ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
|
|
600
|
+
});
|
|
561
601
|
if (response.ok || response.status === 409) setUpdate(await response.json());
|
|
562
602
|
if (update?.state === "reloading") void awaitReload(update.current);
|
|
563
603
|
} catch {
|
|
@@ -584,16 +624,78 @@ function apply(ctx) {
|
|
|
584
624
|
}, 6 * 36e5);
|
|
585
625
|
return () => clearInterval(timer);
|
|
586
626
|
});
|
|
627
|
+
function openReleaseAgeDialog(status) {
|
|
628
|
+
const backdrop = document.createElement("div");
|
|
629
|
+
backdrop.className = "theone-dialog-backdrop";
|
|
630
|
+
backdrop.setAttribute("translate", "no");
|
|
631
|
+
const card = document.createElement("div");
|
|
632
|
+
card.className = "theone-dialog";
|
|
633
|
+
card.setAttribute("role", "dialog");
|
|
634
|
+
card.setAttribute("aria-modal", "true");
|
|
635
|
+
const title = document.createElement("h2");
|
|
636
|
+
title.id = "theone-dialog-title";
|
|
637
|
+
title.textContent = t("age.title");
|
|
638
|
+
card.setAttribute("aria-labelledby", title.id);
|
|
639
|
+
const paragraph = (text, className) => {
|
|
640
|
+
const p = document.createElement("p");
|
|
641
|
+
p.textContent = text;
|
|
642
|
+
if (className) p.className = className;
|
|
643
|
+
return p;
|
|
644
|
+
};
|
|
645
|
+
const version = status.waiting?.version ?? status.latest ?? "";
|
|
646
|
+
card.append(title, paragraph(t("age.why", { version })), paragraph(t(status.canExempt ? "age.allowHow" : "age.cannot")));
|
|
647
|
+
if (status.waiting) card.append(paragraph(t("age.readyAt", { time: new Date(status.waiting.readyAt).toLocaleString(localeSnapshot().active, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) }), "theone-dialog-note"));
|
|
648
|
+
const footer = document.createElement("div");
|
|
649
|
+
footer.className = "theone-dialog-actions";
|
|
650
|
+
const button = (text, primary, act) => {
|
|
651
|
+
const element = document.createElement("button");
|
|
652
|
+
element.type = "button";
|
|
653
|
+
element.textContent = text;
|
|
654
|
+
if (primary) element.className = "theone-dialog-primary";
|
|
655
|
+
element.addEventListener("click", act);
|
|
656
|
+
footer.append(element);
|
|
657
|
+
return element;
|
|
658
|
+
};
|
|
659
|
+
const close = () => {
|
|
660
|
+
backdrop.remove();
|
|
661
|
+
document.removeEventListener("keydown", keydown, true);
|
|
662
|
+
};
|
|
663
|
+
const keydown = (event) => {
|
|
664
|
+
if (event.key === "Escape") {
|
|
665
|
+
event.preventDefault();
|
|
666
|
+
close();
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
button(t(status.canExempt ? "age.wait" : "age.ok"), !status.canExempt, close);
|
|
670
|
+
const allow = status.canExempt ? button(t("age.allow"), true, () => {
|
|
671
|
+
close();
|
|
672
|
+
setUpdate({ ...status, state: "installing", error: void 0 });
|
|
673
|
+
void readUpdate("POST", { allowFresh: true });
|
|
674
|
+
}) : void 0;
|
|
675
|
+
card.append(footer);
|
|
676
|
+
backdrop.append(card);
|
|
677
|
+
backdrop.addEventListener("pointerdown", (event) => {
|
|
678
|
+
if (event.target === backdrop) close();
|
|
679
|
+
});
|
|
680
|
+
document.addEventListener("keydown", keydown, true);
|
|
681
|
+
document.body.append(backdrop);
|
|
682
|
+
(allow ?? footer.querySelector("button"))?.focus();
|
|
683
|
+
}
|
|
587
684
|
function UpdateButton() {
|
|
588
685
|
const t2 = useText();
|
|
589
686
|
const status = (0, import_react.useSyncExternalStore)(subscribeUpdate, () => update);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
687
|
+
const waiting = !!status?.waiting && !status.available && !status.state;
|
|
688
|
+
if (!status || !status.available && !status.state && !waiting) return null;
|
|
689
|
+
const label = waiting ? t2("update.waiting") : status.state === "installing" ? t2("update.installing") : status.state === "reloading" ? t2("update.reloading") : status.state === "restart" ? t2("update.restart") : status.state === "failed" ? t2("update.failed") : t2("update.available");
|
|
690
|
+
const title = waiting ? t2("update.waitingHint", { latest: status.waiting.version }) : status.error === "GATEWAY_BUSY" ? t2("update.busy") : status.state === "reloading" ? t2("update.reloadingHint") : status.state === "restart" ? t2("update.restartHint") : status.state === "failed" ? status.error === "MINIMUM_RELEASE_AGE" ? t2("update.tooNew") : status.error === "NETWORK" ? t2("update.network") : t2("update.failedHint", { error: status.error ?? "" }) : status.installable ? t2("update.hint", { current: status.current, latest: status.latest ?? "" }) : t2("update.manualHint", { current: status.current, latest: status.latest ?? "" });
|
|
593
691
|
const act = (event) => {
|
|
594
692
|
event.preventDefault();
|
|
595
693
|
event.stopPropagation();
|
|
596
694
|
if (status.state === "installing" || status.state === "reloading" || status.state === "restart") return;
|
|
695
|
+
if (waiting || status.state === "failed" && status.error === "MINIMUM_RELEASE_AGE") {
|
|
696
|
+
openReleaseAgeDialog(status);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
597
699
|
if (!status.installable) {
|
|
598
700
|
window.open("https://github.com/YunongDai2005/dsh-theone#readme", "_blank", "noopener");
|
|
599
701
|
return;
|
|
@@ -609,7 +711,7 @@ function apply(ctx) {
|
|
|
609
711
|
tabIndex: 0,
|
|
610
712
|
title,
|
|
611
713
|
"aria-label": title,
|
|
612
|
-
"data-state": status.state ?? "available",
|
|
714
|
+
"data-state": waiting ? "waiting" : status.state ?? "available",
|
|
613
715
|
onClick: act,
|
|
614
716
|
onPointerDown: (event) => event.stopPropagation(),
|
|
615
717
|
onKeyDown: (event) => {
|
|
@@ -618,7 +720,7 @@ function apply(ctx) {
|
|
|
618
720
|
},
|
|
619
721
|
status.state === "installing" || status.state === "reloading" ? (0, import_react.createElement)("span", { className: "theone-update-spin", "aria-hidden": true }) : null,
|
|
620
722
|
label,
|
|
621
|
-
!status.state && status.latest ? (0, import_react.createElement)("small", null, `v${status.latest}`) : null
|
|
723
|
+
!status.state && (waiting ? status.waiting.version : status.latest) ? (0, import_react.createElement)("small", null, `v${waiting ? status.waiting.version : status.latest}`) : null
|
|
622
724
|
);
|
|
623
725
|
}
|
|
624
726
|
const symbol = () => (0, import_react.createElement)(
|
|
@@ -952,6 +1054,118 @@ function apply(ctx) {
|
|
|
952
1054
|
(0, import_react.createElement)("span", { className: "theone-bg-chevron", "aria-hidden": true }, saving ? "\u2026" : "\u2303")
|
|
953
1055
|
);
|
|
954
1056
|
}
|
|
1057
|
+
let notices = [];
|
|
1058
|
+
const noticeListeners = /* @__PURE__ */ new Set();
|
|
1059
|
+
const setNotices = (value) => {
|
|
1060
|
+
notices = value;
|
|
1061
|
+
for (const listener of noticeListeners) listener();
|
|
1062
|
+
};
|
|
1063
|
+
const subscribeNotices = (listener) => {
|
|
1064
|
+
noticeListeners.add(listener);
|
|
1065
|
+
return () => {
|
|
1066
|
+
noticeListeners.delete(listener);
|
|
1067
|
+
};
|
|
1068
|
+
};
|
|
1069
|
+
const localized = (value) => localeSnapshot().active.startsWith("zh") ? value.zh ?? value.en ?? "" : value.en ?? value.zh ?? "";
|
|
1070
|
+
const shownDialogs = /* @__PURE__ */ new Set();
|
|
1071
|
+
const dismissNotice = (id) => {
|
|
1072
|
+
setNotices(notices.filter((notice) => notice.id !== id));
|
|
1073
|
+
void fetch("/api/theone/notices", {
|
|
1074
|
+
method: "POST",
|
|
1075
|
+
signal: lifetime.signal,
|
|
1076
|
+
cache: "no-store",
|
|
1077
|
+
headers: { "content-type": "application/json" },
|
|
1078
|
+
body: JSON.stringify({ dismiss: id })
|
|
1079
|
+
}).catch(() => {
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
function openNoticeDialog(notice) {
|
|
1083
|
+
shownDialogs.add(notice.id);
|
|
1084
|
+
const backdrop = document.createElement("div");
|
|
1085
|
+
backdrop.className = "theone-dialog-backdrop";
|
|
1086
|
+
backdrop.setAttribute("translate", "no");
|
|
1087
|
+
const card = document.createElement("div");
|
|
1088
|
+
card.className = "theone-dialog";
|
|
1089
|
+
card.setAttribute("role", "dialog");
|
|
1090
|
+
card.setAttribute("aria-modal", "true");
|
|
1091
|
+
const title = document.createElement("h2");
|
|
1092
|
+
title.id = "theone-notice-title";
|
|
1093
|
+
title.textContent = localized(notice.title);
|
|
1094
|
+
card.setAttribute("aria-labelledby", title.id);
|
|
1095
|
+
const body = document.createElement("p");
|
|
1096
|
+
body.className = "theone-dialog-body";
|
|
1097
|
+
body.textContent = localized(notice.body);
|
|
1098
|
+
const footer = document.createElement("div");
|
|
1099
|
+
footer.className = "theone-dialog-actions";
|
|
1100
|
+
const close = () => {
|
|
1101
|
+
backdrop.remove();
|
|
1102
|
+
document.removeEventListener("keydown", keydown, true);
|
|
1103
|
+
dismissNotice(notice.id);
|
|
1104
|
+
};
|
|
1105
|
+
const keydown = (event) => {
|
|
1106
|
+
if (event.key === "Escape") {
|
|
1107
|
+
event.preventDefault();
|
|
1108
|
+
close();
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
if (notice.link) {
|
|
1112
|
+
const more = document.createElement("button");
|
|
1113
|
+
more.type = "button";
|
|
1114
|
+
more.textContent = t("notice.more");
|
|
1115
|
+
more.addEventListener("click", () => {
|
|
1116
|
+
window.open(notice.link, "_blank", "noopener");
|
|
1117
|
+
close();
|
|
1118
|
+
});
|
|
1119
|
+
footer.append(more);
|
|
1120
|
+
}
|
|
1121
|
+
const ok = document.createElement("button");
|
|
1122
|
+
ok.type = "button";
|
|
1123
|
+
ok.className = "theone-dialog-primary";
|
|
1124
|
+
ok.textContent = t("notice.ok");
|
|
1125
|
+
ok.addEventListener("click", close);
|
|
1126
|
+
footer.append(ok);
|
|
1127
|
+
card.append(title, body, footer);
|
|
1128
|
+
backdrop.append(card);
|
|
1129
|
+
document.addEventListener("keydown", keydown, true);
|
|
1130
|
+
document.body.append(backdrop);
|
|
1131
|
+
ok.focus();
|
|
1132
|
+
}
|
|
1133
|
+
const readNotices = async () => {
|
|
1134
|
+
try {
|
|
1135
|
+
const response = await fetch("/api/theone/notices", { signal: lifetime.signal, cache: "no-store" });
|
|
1136
|
+
if (!response.ok) return;
|
|
1137
|
+
setNotices((await response.json()).notices);
|
|
1138
|
+
const important = notices.find((notice) => notice.level === "important" && !shownDialogs.has(notice.id));
|
|
1139
|
+
if (important && !document.querySelector(".theone-dialog-backdrop")) openNoticeDialog(important);
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
ctx.effect(() => {
|
|
1144
|
+
void readNotices();
|
|
1145
|
+
const timer = setInterval(() => {
|
|
1146
|
+
void readNotices();
|
|
1147
|
+
}, 3 * 36e5);
|
|
1148
|
+
return () => clearInterval(timer);
|
|
1149
|
+
});
|
|
1150
|
+
function NoticeStrip({ directory }) {
|
|
1151
|
+
const t2 = useText();
|
|
1152
|
+
const state = (0, import_react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
|
|
1153
|
+
const list = (0, import_react.useSyncExternalStore)(subscribeNotices, () => notices);
|
|
1154
|
+
const notice = list.find((item) => item.level === "info");
|
|
1155
|
+
if ((state.pending ?? state.current)?.provider !== "theone" || !notice) return null;
|
|
1156
|
+
const title = localized(notice.title), body = localized(notice.body);
|
|
1157
|
+
return (0, import_react.createElement)(
|
|
1158
|
+
"div",
|
|
1159
|
+
{ className: "theone-notice", role: "status", translate: "no" },
|
|
1160
|
+
(0, import_react.createElement)("span", { className: "theone-notice-tag" }, t2("notice.label")),
|
|
1161
|
+
(0, import_react.createElement)("span", { className: "theone-notice-text", title: `${title}
|
|
1162
|
+
${body}` }, (0, import_react.createElement)("strong", null, title), " ", body),
|
|
1163
|
+
notice.link ? (0, import_react.createElement)("button", { type: "button", className: "theone-notice-link", onClick: () => {
|
|
1164
|
+
window.open(notice.link, "_blank", "noopener");
|
|
1165
|
+
} }, t2("notice.more")) : null,
|
|
1166
|
+
(0, import_react.createElement)("button", { type: "button", className: "theone-notice-close", "aria-label": t2("notice.close"), title: t2("notice.close"), onClick: () => dismissNotice(notice.id) }, "\xD7")
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
955
1169
|
ctx.inject(["slots", "modelDirectories"], (scope) => {
|
|
956
1170
|
const slots = scope.slots;
|
|
957
1171
|
slots.inject("conversation.input.right", () => slots.register({
|
|
@@ -960,6 +1174,12 @@ function apply(ctx) {
|
|
|
960
1174
|
order: 100,
|
|
961
1175
|
inject: (sessionId) => ({ directory: scope.modelDirectories.directoryFor(sessionId).store })
|
|
962
1176
|
}, BackgroundModel));
|
|
1177
|
+
slots.inject("conversation.composer.dock", () => slots.register({
|
|
1178
|
+
name: "conversation.composer.dock",
|
|
1179
|
+
id: "theone.notice",
|
|
1180
|
+
order: 100,
|
|
1181
|
+
inject: (sessionId) => ({ directory: scope.modelDirectories.directoryFor(sessionId).store })
|
|
1182
|
+
}, NoticeStrip));
|
|
963
1183
|
});
|
|
964
1184
|
function GatewayPanel() {
|
|
965
1185
|
const t2 = useText();
|
|
@@ -1020,7 +1240,8 @@ function apply(ctx) {
|
|
|
1020
1240
|
["history", ["historyCatalog", "catalogIntervalMs"]],
|
|
1021
1241
|
["linkage", ["linkScope", "routeNotice"]],
|
|
1022
1242
|
["limits", ["maxDescriptorChars", "maxResponseChars"]],
|
|
1023
|
-
["storage", ["contextsPath", "databasePath", "gatewayKey"]]
|
|
1243
|
+
["storage", ["contextsPath", "databasePath", "gatewayKey"]],
|
|
1244
|
+
["other", ["notices"]]
|
|
1024
1245
|
];
|
|
1025
1246
|
const display = (key) => {
|
|
1026
1247
|
const value = snapshot.values[key];
|
|
@@ -1077,7 +1298,7 @@ function apply(ctx) {
|
|
|
1077
1298
|
{ ...props, value, required: field === "workerModel" && !!draft.workerProvider, onChange: (event) => selectValue(event.target.value) },
|
|
1078
1299
|
...choices.map((choice) => (0, import_react.createElement)("option", { key: choice.value, value: choice.value }, choice.label))
|
|
1079
1300
|
);
|
|
1080
|
-
if (field === "historyCatalog") return select(String(draft
|
|
1301
|
+
if (field === "historyCatalog" || field === "notices") return select(String(draft[field]), [{ value: "true", label: t2("settings.on") }, { value: "false", label: t2("settings.off") }], (v) => change(field, v === "true"));
|
|
1081
1302
|
if (field === "linkScope" || field === "routeNotice") {
|
|
1082
1303
|
const values = field === "linkScope" ? ["auto", "workspace", "off"] : ["switch", "hidden", "all"];
|
|
1083
1304
|
return select(draft[field], values.map((value) => ({ value, label: t2(`settings.${field === "linkScope" ? "scope" : "notice"}.${value}`) })), (v) => change(field, v));
|
|
@@ -1718,6 +1939,27 @@ button:has(.theone-nav[data-wide=true])>span:has(.theone-nav){flex:1;min-width:0
|
|
|
1718
1939
|
.theone-update small{font-size:11px;opacity:.75}
|
|
1719
1940
|
.theone-update[data-state=installing],.theone-update[data-state=reloading],.theone-update[data-state=restart]{cursor:default}
|
|
1720
1941
|
.theone-update[data-state=failed]{color:#d9480f;border-color:#d9480f66;background:#d9480f14}
|
|
1942
|
+
.theone-update[data-state=waiting]{opacity:.8;border-style:dashed}
|
|
1943
|
+
.theone-dialog-backdrop{position:fixed;inset:0;z-index:10001;display:flex;align-items:center;justify-content:center;padding:16px;background:#0008}
|
|
1944
|
+
.theone-dialog{width:min(440px,100%);box-sizing:border-box;padding:22px 22px 18px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:16px;background:var(--dsw-specific-sidebar-fill,#232326);color:var(--dsw-alias-label-primary,#e8e8ea);box-shadow:0 16px 48px #0005;font:inherit;font-size:14px;line-height:1.65}
|
|
1945
|
+
.theone-dialog h2{margin:0 0 10px;font-size:17px}
|
|
1946
|
+
.theone-dialog p{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#a0a0a6)}
|
|
1947
|
+
.theone-dialog .theone-dialog-note{font-size:13px}
|
|
1948
|
+
.theone-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:16px}
|
|
1949
|
+
.theone-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:9px;background:transparent;color:inherit;font:inherit;cursor:pointer}
|
|
1950
|
+
.theone-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff12)}
|
|
1951
|
+
.theone-dialog-actions .theone-dialog-primary{border-color:transparent;background:#3b6fb0;color:#fff}
|
|
1952
|
+
.theone-dialog-actions .theone-dialog-primary:hover{background:#4a7fc0}
|
|
1953
|
+
.theone-dialog-actions button:focus-visible{outline:2px solid #4a7fc0;outline-offset:2px}
|
|
1954
|
+
.theone-dialog .theone-dialog-body{white-space:pre-wrap;color:var(--dsw-alias-label-primary,#e8e8ea)}
|
|
1955
|
+
.theone-notice{display:flex;align-items:center;gap:8px;margin:6px 4px 0;padding:6px 8px 6px 10px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:10px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#a0a0a6);min-width:0}
|
|
1956
|
+
.theone-notice-tag{flex:none;padding:0 6px;border-radius:6px;background:#3b6fb033;color:#7fa9dd}
|
|
1957
|
+
.theone-notice-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1958
|
+
.theone-notice-text strong{color:var(--dsw-alias-label-primary,#e8e8ea);font-weight:500}
|
|
1959
|
+
.theone-notice button{flex:none;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;padding:2px 6px;border-radius:6px}
|
|
1960
|
+
.theone-notice button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff12)}
|
|
1961
|
+
.theone-notice-link{color:#7fa9dd!important}
|
|
1962
|
+
.theone-notice-close{font-size:15px;line-height:1}
|
|
1721
1963
|
.theone-update-spin{width:10px;height:10px;border-radius:50%;border:1.5px solid currentColor;border-right-color:transparent;animation:theone-spin 800ms linear infinite}
|
|
1722
1964
|
@keyframes theone-spin{to{transform:rotate(360deg)}}
|
|
1723
1965
|
@media(prefers-reduced-motion:reduce){.theone-update-spin{animation:none}}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { LinkageSnapshot } from './catalog-types.ts';
|
|
|
10
10
|
import { type SettingsSnapshot } from './settings-types.ts';
|
|
11
11
|
import { type LinkScope } from './linkage.ts';
|
|
12
12
|
import { Updater } from './update.ts';
|
|
13
|
+
import { NoticeBoard } from './notices.ts';
|
|
13
14
|
export interface Config {
|
|
14
15
|
databasePath?: string;
|
|
15
16
|
contextsPath?: string;
|
|
@@ -28,6 +29,10 @@ export interface Config {
|
|
|
28
29
|
routerApiKeyEnv?: string;
|
|
29
30
|
linkScope?: LinkScope;
|
|
30
31
|
routeNotice?: 'hidden' | 'switch' | 'all';
|
|
32
|
+
/** Show notices the maintainer publishes (read from a static file; nothing is sent). */
|
|
33
|
+
notices?: boolean;
|
|
34
|
+
/** Where notices are read from; for testing. */
|
|
35
|
+
noticeUrl?: string;
|
|
31
36
|
}
|
|
32
37
|
declare module '@deepseek-ai/cordis' {
|
|
33
38
|
interface Context {
|
|
@@ -79,6 +84,7 @@ export default class TheOne extends Service {
|
|
|
79
84
|
private adapter?;
|
|
80
85
|
/** Update checks and one-click install through DSH's plugin manager. */
|
|
81
86
|
readonly updater: Updater;
|
|
87
|
+
readonly noticeBoard: NoticeBoard;
|
|
82
88
|
/** Sessions whose current step answers through TheOne; only these refuse to run tools themselves. */
|
|
83
89
|
private readonly throughTheOne;
|
|
84
90
|
constructor(ctx: Context, config: Config);
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { validateSettings } from "./settings.js";
|
|
|
21
21
|
import { RESTART_CODE, WorkerRun } from "./run.js";
|
|
22
22
|
import { buildBriefing, LINK_SIGNAL, relatedTopics } from "./linkage.js";
|
|
23
23
|
import { PACKAGE_NAME, Updater } from "./update.js";
|
|
24
|
+
import { NOTICE_URL, NoticeBoard } from "./notices.js";
|
|
24
25
|
function redactDescriptor(text) {
|
|
25
26
|
return text.replace(/\bsk-[a-zA-Z0-9_-]{16,}|\bBearer\s+\S+/gi, '[REDACTED]')
|
|
26
27
|
.replace(/((?:api[_ -]?key|password|密码|密钥)\s*[:=:]\s*)\S+/gi, '$1[REDACTED]');
|
|
@@ -155,6 +156,8 @@ export default class TheOne extends Service {
|
|
|
155
156
|
routerBaseUrl: z.string(), routerModel: z.string(), routerApiKeyEnv: z.string(),
|
|
156
157
|
linkScope: z.union([z.const('off'), z.const('workspace'), z.const('auto')]).default('auto'),
|
|
157
158
|
routeNotice: z.union([z.const('hidden'), z.const('switch'), z.const('all')]).default('switch'),
|
|
159
|
+
notices: z.boolean().default(true),
|
|
160
|
+
noticeUrl: z.string(),
|
|
158
161
|
});
|
|
159
162
|
store;
|
|
160
163
|
catalog;
|
|
@@ -173,6 +176,7 @@ export default class TheOne extends Service {
|
|
|
173
176
|
adapter;
|
|
174
177
|
/** Update checks and one-click install through DSH's plugin manager. */
|
|
175
178
|
updater;
|
|
179
|
+
noticeBoard;
|
|
176
180
|
/** Sessions whose current step answers through TheOne; only these refuse to run tools themselves. */
|
|
177
181
|
throughTheOne = new WeakMap();
|
|
178
182
|
constructor(ctx, config) {
|
|
@@ -205,9 +209,10 @@ export default class TheOne extends Service {
|
|
|
205
209
|
}
|
|
206
210
|
descriptors ??= config.contextsPath ? readDescriptors(config.contextsPath) : [];
|
|
207
211
|
this.gatewayDirectory = resolve(dirname(databasePath), 'gateway');
|
|
212
|
+
const profileDir = () => ctx.profileContext?.dir;
|
|
208
213
|
this.updater = new Updater(readOwnVersion(), () => {
|
|
209
214
|
// The profile's package.json records how this plugin was installed (GitHub, npm or a local path).
|
|
210
|
-
const dir =
|
|
215
|
+
const dir = profileDir();
|
|
211
216
|
if (!dir)
|
|
212
217
|
return undefined;
|
|
213
218
|
try {
|
|
@@ -216,7 +221,8 @@ export default class TheOne extends Service {
|
|
|
216
221
|
catch {
|
|
217
222
|
return undefined;
|
|
218
223
|
}
|
|
219
|
-
});
|
|
224
|
+
}, fetch, Date.now, () => { const dir = profileDir(); return dir ? join(dir, 'pnpm-workspace.yaml') : undefined; });
|
|
225
|
+
this.noticeBoard = new NoticeBoard(this.updater.current, config.noticeUrl || NOTICE_URL);
|
|
220
226
|
ctx.effect(() => async () => {
|
|
221
227
|
try {
|
|
222
228
|
await Promise.all([...this.workers.values()].map(handle => handle.dispose()));
|
|
@@ -628,11 +634,36 @@ export default class TheOne extends Service {
|
|
|
628
634
|
const manager = this.ctx.get('pluginManager');
|
|
629
635
|
// With DSH's hot reload, switching the bundle off and on loads the new version without a DSH restart.
|
|
630
636
|
const live = !!this.ctx.get('hmr') && typeof manager?.setBundleEnabled === 'function';
|
|
637
|
+
if (request.method === 'POST') {
|
|
638
|
+
// The user chose to let TheOne skip pnpm's one-day wait; only this package is exempted.
|
|
639
|
+
const body = await request.json().catch(() => ({}));
|
|
640
|
+
if (body?.allowFresh === true) {
|
|
641
|
+
try {
|
|
642
|
+
this.updater.allowFresh();
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
return Response.json({ ...await this.updater.status(), error: error instanceof Error && /^[A-Z_]+$/.test(error.message) ? error.message : 'EXEMPT_FAILED' }, { status: 400 });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
631
649
|
const status = request.method === 'POST'
|
|
632
650
|
? await this.updater.install(manager, live ? bundle => reloadBundle(manager, bundle) : undefined)
|
|
633
651
|
: await this.updater.status(new URL(request.url).searchParams.has('force'));
|
|
634
652
|
return Response.json(status, { headers: { 'cache-control': 'no-store' } });
|
|
635
653
|
} }));
|
|
654
|
+
// Notices from TheOne's maintainer; the user can close each one, or turn them all off in Settings.
|
|
655
|
+
child.effect(() => connection.fetch.register({ path: '/api/theone/notices', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: async (request) => {
|
|
656
|
+
if (request.method === 'POST') {
|
|
657
|
+
const body = await request.json().catch(() => ({}));
|
|
658
|
+
if (typeof body?.dismiss !== 'string' || !/^[\w.-]{1,64}$/.test(body.dismiss))
|
|
659
|
+
return Response.json({ error: 'INVALID_INPUT' }, { status: 400 });
|
|
660
|
+
this.store.dismissNotice(body.dismiss);
|
|
661
|
+
}
|
|
662
|
+
if (this.config.notices === false)
|
|
663
|
+
return Response.json({ notices: [] }, { headers: { 'cache-control': 'no-store' } });
|
|
664
|
+
const dismissed = this.store.dismissedNotices();
|
|
665
|
+
return Response.json({ notices: (await this.noticeBoard.notices()).filter(notice => !dismissed.has(notice.id)) }, { headers: { 'cache-control': 'no-store' } });
|
|
666
|
+
} }));
|
|
636
667
|
child.effect(() => connection.fetch.register({ path: '/api/theone/sessions', methods: ['GET'], requestBody: 'buffered', fetch: async () => Response.json({ sessions: await this.attachableSessions() }, { headers: { 'cache-control': 'no-store' } }) }));
|
|
637
668
|
// The user's corrections to topic linking always take precedence over what was learned.
|
|
638
669
|
child.effect(() => connection.fetch.register({ path: '/api/theone/links', methods: ['POST'], requestBody: 'buffered', fetch: async (request) => {
|
|
@@ -792,7 +823,7 @@ export default class TheOne extends Service {
|
|
|
792
823
|
workerProvider: c.workerProvider ? redactDescriptor(c.workerProvider) : null, workerModel: c.workerModel ? redactDescriptor(c.workerModel) : null,
|
|
793
824
|
maxDescriptorChars: c.maxDescriptorChars, maxResponseChars: c.maxResponseChars,
|
|
794
825
|
routerMode: c.routerMode ?? 'llm',
|
|
795
|
-
linkScope: c.linkScope ?? 'auto', routeNotice: c.routeNotice ?? 'switch',
|
|
826
|
+
linkScope: c.linkScope ?? 'auto', routeNotice: c.routeNotice ?? 'switch', notices: c.notices ?? true,
|
|
796
827
|
};
|
|
797
828
|
const activeEditable = Object.fromEntries(EDITABLE_SETTINGS_KEYS.map(key => [key, key === 'contextsPath' ? c.contextsPath ?? null : values[key]]));
|
|
798
829
|
let savedValues = activeEditable;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Where TheOne's maintainer publishes notices: a static JSON file, read with a plain GET. */
|
|
2
|
+
export declare const NOTICE_URL = "https://yulid.org/theone/notice.json";
|
|
3
|
+
/** Text in each interface language; the reader's language is chosen in the client. */
|
|
4
|
+
export interface LocalizedText {
|
|
5
|
+
zh?: string;
|
|
6
|
+
en?: string;
|
|
7
|
+
}
|
|
8
|
+
/** One notice as the client shows it. */
|
|
9
|
+
export interface Notice {
|
|
10
|
+
id: string;
|
|
11
|
+
/** `important` opens a dialog once; `info` shows a strip under the main chat input. */
|
|
12
|
+
level: 'info' | 'important';
|
|
13
|
+
title: LocalizedText;
|
|
14
|
+
body: LocalizedText;
|
|
15
|
+
/** An https page to read more; opened only when the reader clicks it. */
|
|
16
|
+
link?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether `version` falls within a range such as "<0.3.16", ">=0.3.10", "0.3.12", "*", or several
|
|
20
|
+
* of these separated by spaces (all must hold). An unreadable range matches nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function versionMatches(range: unknown, version: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The notices in a published file that apply to this version now, at most five. Anything that does
|
|
25
|
+
* not fit the format is skipped rather than shown; text is plain, links must be https.
|
|
26
|
+
*/
|
|
27
|
+
export declare function applicableNotices(file: unknown, version: string, now?: number): Notice[];
|
|
28
|
+
/** Reads the notice file at most every three hours; a failure simply means no notices. */
|
|
29
|
+
export declare class NoticeBoard {
|
|
30
|
+
private readonly version;
|
|
31
|
+
private readonly url;
|
|
32
|
+
private readonly fetcher;
|
|
33
|
+
private readonly now;
|
|
34
|
+
private cache?;
|
|
35
|
+
private pending?;
|
|
36
|
+
constructor(version: string, url?: string, fetcher?: typeof fetch, now?: () => number);
|
|
37
|
+
notices(): Promise<Notice[]>;
|
|
38
|
+
private read;
|
|
39
|
+
}
|
package/dist/notices.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { compareVersions } from "./update.js";
|
|
2
|
+
/** Where TheOne's maintainer publishes notices: a static JSON file, read with a plain GET. */
|
|
3
|
+
export const NOTICE_URL = 'https://yulid.org/theone/notice.json';
|
|
4
|
+
const HOUR = 3600000;
|
|
5
|
+
const text = (value, max) => {
|
|
6
|
+
if (typeof value === 'string')
|
|
7
|
+
return value.trim() ? { zh: value.trim().slice(0, max), en: value.trim().slice(0, max) } : undefined;
|
|
8
|
+
if (!value || typeof value !== 'object')
|
|
9
|
+
return undefined;
|
|
10
|
+
const row = value;
|
|
11
|
+
const pick = (key) => typeof row[key] === 'string' && row[key].trim() ? { [key]: row[key].trim().slice(0, max) } : {};
|
|
12
|
+
const result = { ...pick('zh'), ...pick('en') };
|
|
13
|
+
return Object.keys(result).length ? result : undefined;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Whether `version` falls within a range such as "<0.3.16", ">=0.3.10", "0.3.12", "*", or several
|
|
17
|
+
* of these separated by spaces (all must hold). An unreadable range matches nothing.
|
|
18
|
+
*/
|
|
19
|
+
export function versionMatches(range, version) {
|
|
20
|
+
if (range === undefined || range === null || range === '*' || range === '')
|
|
21
|
+
return true;
|
|
22
|
+
if (typeof range !== 'string')
|
|
23
|
+
return false;
|
|
24
|
+
return range.trim().split(/\s+/).every(part => {
|
|
25
|
+
const match = /^(<=|>=|<|>|=)?v?(\d+\.\d+\.\d+)$/.exec(part);
|
|
26
|
+
if (!match)
|
|
27
|
+
return false;
|
|
28
|
+
const order = compareVersions(version, match[2]);
|
|
29
|
+
return { '<': order < 0, '<=': order <= 0, '>': order > 0, '>=': order >= 0, '=': order === 0 }[match[1] ?? '='];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The notices in a published file that apply to this version now, at most five. Anything that does
|
|
34
|
+
* not fit the format is skipped rather than shown; text is plain, links must be https.
|
|
35
|
+
*/
|
|
36
|
+
export function applicableNotices(file, version, now = Date.now()) {
|
|
37
|
+
const list = file && typeof file === 'object' && Array.isArray(file.notices) ? file.notices : [];
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const item of list.slice(0, 20)) {
|
|
40
|
+
if (!item || typeof item !== 'object')
|
|
41
|
+
continue;
|
|
42
|
+
const row = item;
|
|
43
|
+
if (typeof row.id !== 'string' || !/^[\w.-]{1,64}$/.test(row.id) || result.some(notice => notice.id === row.id))
|
|
44
|
+
continue;
|
|
45
|
+
const title = text(row.title, 80), body = text(row.body, 600);
|
|
46
|
+
if (!title || !body || !versionMatches(row.versions, version))
|
|
47
|
+
continue;
|
|
48
|
+
const time = (value) => typeof value === 'string' ? Date.parse(value) : NaN;
|
|
49
|
+
if (Number.isFinite(time(row.from)) && now < time(row.from))
|
|
50
|
+
continue;
|
|
51
|
+
if (Number.isFinite(time(row.until)) && now > time(row.until))
|
|
52
|
+
continue;
|
|
53
|
+
let link;
|
|
54
|
+
if (typeof row.link === 'string') {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(row.link);
|
|
57
|
+
if (url.protocol === 'https:' && row.link.length <= 300)
|
|
58
|
+
link = url.href;
|
|
59
|
+
}
|
|
60
|
+
catch { /* Not a link. */ }
|
|
61
|
+
}
|
|
62
|
+
result.push({ id: row.id, level: row.level === 'important' ? 'important' : 'info', title, body, ...(link ? { link } : {}) });
|
|
63
|
+
if (result.length === 5)
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/** Reads the notice file at most every three hours; a failure simply means no notices. */
|
|
69
|
+
export class NoticeBoard {
|
|
70
|
+
version;
|
|
71
|
+
url;
|
|
72
|
+
fetcher;
|
|
73
|
+
now;
|
|
74
|
+
cache;
|
|
75
|
+
pending;
|
|
76
|
+
constructor(version, url = NOTICE_URL, fetcher = fetch, now = Date.now) {
|
|
77
|
+
this.version = version;
|
|
78
|
+
this.url = url;
|
|
79
|
+
this.fetcher = fetcher;
|
|
80
|
+
this.now = now;
|
|
81
|
+
}
|
|
82
|
+
async notices() {
|
|
83
|
+
if (!this.cache || this.now() - this.cache.at > 3 * HOUR) {
|
|
84
|
+
this.pending ??= this.read().finally(() => { this.pending = undefined; });
|
|
85
|
+
await this.pending;
|
|
86
|
+
}
|
|
87
|
+
return applicableNotices(this.cache?.file, this.version, this.now());
|
|
88
|
+
}
|
|
89
|
+
async read() {
|
|
90
|
+
try {
|
|
91
|
+
// A plain GET: nothing about the reader or their conversations is sent.
|
|
92
|
+
const response = await this.fetcher(this.url, { signal: AbortSignal.timeout(8000), headers: { accept: 'application/json' } });
|
|
93
|
+
if (!response.ok)
|
|
94
|
+
throw new Error(`HTTP ${response.status}`);
|
|
95
|
+
const body = await response.text();
|
|
96
|
+
if (body.length > 64000)
|
|
97
|
+
throw new Error('Notice file too large');
|
|
98
|
+
this.cache = { at: this.now(), file: JSON.parse(body) };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Try again in half an hour, keeping what was read before.
|
|
102
|
+
this.cache = { at: this.now() - 2.5 * HOUR, file: this.cache?.file };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/settings-types.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface SettingsSnapshot {
|
|
|
13
13
|
routerMode: 'rules' | 'llm';
|
|
14
14
|
linkScope: 'off' | 'workspace' | 'auto';
|
|
15
15
|
routeNotice: 'hidden' | 'switch' | 'all';
|
|
16
|
+
notices: boolean;
|
|
16
17
|
};
|
|
17
18
|
model: {
|
|
18
19
|
provider: string;
|
|
@@ -32,7 +33,7 @@ export interface SettingsSnapshot {
|
|
|
32
33
|
/** Saved settings that apply only after DSH restarts; everything else applies when saved. */
|
|
33
34
|
restartRequired: boolean;
|
|
34
35
|
}
|
|
35
|
-
export declare const EDITABLE_SETTINGS_KEYS: readonly ["workerProvider", "workerModel", "routerMode", "historyCatalog", "catalogIntervalMs", "maxDescriptorChars", "maxResponseChars", "linkScope", "routeNotice", "contextsPath"];
|
|
36
|
+
export declare const EDITABLE_SETTINGS_KEYS: readonly ["workerProvider", "workerModel", "routerMode", "historyCatalog", "catalogIntervalMs", "maxDescriptorChars", "maxResponseChars", "linkScope", "routeNotice", "contextsPath", "notices"];
|
|
36
37
|
/** The background catalog is started once; these take effect after DSH restarts. */
|
|
37
38
|
export declare const RESTART_SETTINGS_KEYS: readonly ["historyCatalog", "catalogIntervalMs"];
|
|
38
39
|
/** Settings of the removed direct router; forms saved by older versions may still carry them. */
|
|
@@ -41,5 +42,6 @@ export declare const RETIRED_SETTINGS_KEYS: readonly ["routerTransport", "router
|
|
|
41
42
|
export declare const SETTINGS_DEFAULTS: {
|
|
42
43
|
readonly linkScope: "auto";
|
|
43
44
|
readonly routeNotice: "switch";
|
|
45
|
+
readonly notices: true;
|
|
44
46
|
};
|
|
45
47
|
export type EditableSettings = Pick<SettingsSnapshot['values'], typeof EDITABLE_SETTINGS_KEYS[number]>;
|
package/dist/settings-types.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const EDITABLE_SETTINGS_KEYS = ['workerProvider', 'workerModel', 'routerMode', 'historyCatalog',
|
|
2
|
-
'catalogIntervalMs', 'maxDescriptorChars', 'maxResponseChars', 'linkScope', 'routeNotice', 'contextsPath'];
|
|
2
|
+
'catalogIntervalMs', 'maxDescriptorChars', 'maxResponseChars', 'linkScope', 'routeNotice', 'contextsPath', 'notices'];
|
|
3
3
|
/** The background catalog is started once; these take effect after DSH restarts. */
|
|
4
4
|
export const RESTART_SETTINGS_KEYS = ['historyCatalog', 'catalogIntervalMs'];
|
|
5
5
|
/** Settings of the removed direct router; forms saved by older versions may still carry them. */
|
|
6
6
|
export const RETIRED_SETTINGS_KEYS = ['routerTransport', 'routerBaseUrl', 'routerModel', 'routerApiKeyEnv'];
|
|
7
7
|
/** Added after the first release; settings saved before them take their defaults. */
|
|
8
|
-
export const SETTINGS_DEFAULTS = { linkScope: 'auto', routeNotice: 'switch' };
|
|
8
|
+
export const SETTINGS_DEFAULTS = { linkScope: 'auto', routeNotice: 'switch', notices: true };
|
package/dist/settings.js
CHANGED
|
@@ -33,8 +33,10 @@ export function validateSettings(value, fallback = {}) {
|
|
|
33
33
|
if ((workerProvider === null) !== (workerModel === null) || workerProvider === 'theone')
|
|
34
34
|
return fail();
|
|
35
35
|
const contextsPath = row.contextsPath === null ? null : text('contextsPath', 4096);
|
|
36
|
+
if (typeof row.notices !== 'boolean')
|
|
37
|
+
return fail();
|
|
36
38
|
return { workerProvider, workerModel, routerMode: row.routerMode, historyCatalog: row.historyCatalog,
|
|
37
39
|
catalogIntervalMs: number('catalogIntervalMs', 10000, 86400000), maxDescriptorChars: number('maxDescriptorChars', 128, 1000000),
|
|
38
40
|
maxResponseChars: number('maxResponseChars', 128, 10000000),
|
|
39
|
-
linkScope: row.linkScope, routeNotice: row.routeNotice, contextsPath };
|
|
41
|
+
linkScope: row.linkScope, routeNotice: row.routeNotice, contextsPath, notices: row.notices };
|
|
40
42
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -157,5 +157,8 @@ export declare class ContextStore {
|
|
|
157
157
|
/** Remove a topic and everything TheOne kept about it. DSH keeps the conversations themselves. */
|
|
158
158
|
deleteTopic(contextId: string): void;
|
|
159
159
|
private purge;
|
|
160
|
+
/** Notices the user closed; they are not shown again on any browser. */
|
|
161
|
+
dismissNotice(id: string, now?: number): void;
|
|
162
|
+
dismissedNotices(): Set<string>;
|
|
160
163
|
close(): void;
|
|
161
164
|
}
|
package/dist/store.js
CHANGED
|
@@ -93,6 +93,7 @@ export class ContextStore {
|
|
|
93
93
|
CREATE TABLE IF NOT EXISTS learned_terms (
|
|
94
94
|
context_id TEXT NOT NULL, term TEXT NOT NULL, weight REAL NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY(context_id, term)
|
|
95
95
|
);
|
|
96
|
+
CREATE TABLE IF NOT EXISTS dismissed_notices (id TEXT PRIMARY KEY, dismissed_at INTEGER NOT NULL);
|
|
96
97
|
CREATE TABLE IF NOT EXISTS route_details (
|
|
97
98
|
message_id TEXT PRIMARY KEY, excerpt TEXT NOT NULL, receipt TEXT, corrected_to TEXT, corrected_at INTEGER
|
|
98
99
|
);
|
|
@@ -684,5 +685,12 @@ export class ContextStore {
|
|
|
684
685
|
this.db.prepare('UPDATE route_details SET corrected_to = NULL, corrected_at = NULL WHERE corrected_to = ?').run(contextId);
|
|
685
686
|
this.db.prepare('DELETE FROM contexts WHERE id = ?').run(contextId);
|
|
686
687
|
}
|
|
688
|
+
/** Notices the user closed; they are not shown again on any browser. */
|
|
689
|
+
dismissNotice(id, now = Date.now()) {
|
|
690
|
+
this.db.prepare('INSERT OR IGNORE INTO dismissed_notices VALUES (?, ?)').run(id, now);
|
|
691
|
+
}
|
|
692
|
+
dismissedNotices() {
|
|
693
|
+
return new Set(this.db.prepare('SELECT id FROM dismissed_notices').all().map(row => String(row.id)));
|
|
694
|
+
}
|
|
687
695
|
close() { this.db.close(); }
|
|
688
696
|
}
|
package/dist/update.d.ts
CHANGED
|
@@ -19,7 +19,18 @@ export interface UpdateStatus {
|
|
|
19
19
|
version: string;
|
|
20
20
|
readyAt: number;
|
|
21
21
|
};
|
|
22
|
+
/** TheOne is exempt from that rule in this DSH profile, so fresh versions install right away. */
|
|
23
|
+
exempt?: boolean;
|
|
24
|
+
/** Whether this profile's pnpm settings can be edited to grant that exemption. */
|
|
25
|
+
canExempt?: boolean;
|
|
22
26
|
}
|
|
27
|
+
/** The packages a pnpm-workspace.yaml exempts from the release-age rule (block or flow list). */
|
|
28
|
+
export declare function releaseAgeExemptions(text: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* The same pnpm-workspace.yaml with `name` added to the release-age exemptions, every other line
|
|
31
|
+
* kept as it was. Throws for a layout it does not recognise rather than guess.
|
|
32
|
+
*/
|
|
33
|
+
export declare function withReleaseAgeExemption(text: string, name?: string): string;
|
|
23
34
|
/**
|
|
24
35
|
* pnpm, which DSH installs plugins with, refuses npm versions published less than a day ago
|
|
25
36
|
* (minimumReleaseAge, a supply-chain safeguard). Updates from npm wait until then.
|
|
@@ -51,11 +62,20 @@ export declare class Updater {
|
|
|
51
62
|
private readonly spec;
|
|
52
63
|
private readonly fetcher;
|
|
53
64
|
private readonly now;
|
|
65
|
+
private readonly workspace;
|
|
54
66
|
private checked?;
|
|
55
67
|
private state?;
|
|
56
68
|
private error?;
|
|
57
69
|
private pending?;
|
|
58
|
-
|
|
70
|
+
/**
|
|
71
|
+
* @param workspace - this DSH profile's pnpm-workspace.yaml, where pnpm reads the release-age
|
|
72
|
+
* exemptions; undefined when the profile is unknown.
|
|
73
|
+
*/
|
|
74
|
+
constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number, workspace?: () => string | undefined);
|
|
75
|
+
/** Whether pnpm in this profile already lets TheOne install versions under a day old. */
|
|
76
|
+
get exempt(): boolean;
|
|
77
|
+
/** Exempt TheOne, and only TheOne, from pnpm's release-age rule in this profile. */
|
|
78
|
+
allowFresh(): void;
|
|
59
79
|
get source(): UpdateSource;
|
|
60
80
|
/** The latest known status; checks again at most every six hours (or now, when forced). */
|
|
61
81
|
status(force?: boolean): Promise<UpdateStatus>;
|
package/dist/update.js
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
1
2
|
export const REPOSITORY = 'YunongDai2005/dsh-theone';
|
|
2
3
|
export const PACKAGE_NAME = 'dsh-theone';
|
|
4
|
+
const EXCLUDE_KEY = 'minimumReleaseAgeExclude';
|
|
5
|
+
const unquote = (value) => value.trim().replace(/^['"]|['"]$/g, '');
|
|
6
|
+
/** The packages a pnpm-workspace.yaml exempts from the release-age rule (block or flow list). */
|
|
7
|
+
export function releaseAgeExemptions(text) {
|
|
8
|
+
const lines = text.split(/\r?\n/);
|
|
9
|
+
const index = lines.findIndex(line => line.startsWith(`${EXCLUDE_KEY}:`));
|
|
10
|
+
if (index < 0)
|
|
11
|
+
return [];
|
|
12
|
+
const inline = lines[index].slice(EXCLUDE_KEY.length + 1).trim();
|
|
13
|
+
if (inline.startsWith('['))
|
|
14
|
+
return inline.replace(/^\[|\]$/g, '').split(',').map(unquote).filter(Boolean);
|
|
15
|
+
const items = [];
|
|
16
|
+
for (let line = index + 1; line < lines.length && /^\s+-/.test(lines[line]); line++)
|
|
17
|
+
items.push(unquote(lines[line].replace(/^\s+-\s*/, '')));
|
|
18
|
+
return items;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The same pnpm-workspace.yaml with `name` added to the release-age exemptions, every other line
|
|
22
|
+
* kept as it was. Throws for a layout it does not recognise rather than guess.
|
|
23
|
+
*/
|
|
24
|
+
export function withReleaseAgeExemption(text, name = PACKAGE_NAME) {
|
|
25
|
+
if (releaseAgeExemptions(text).includes(name))
|
|
26
|
+
return text;
|
|
27
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
28
|
+
const lines = text.split(/\r?\n/);
|
|
29
|
+
const index = lines.findIndex(line => line.startsWith(`${EXCLUDE_KEY}:`));
|
|
30
|
+
if (index < 0)
|
|
31
|
+
return `${text}${text && !/\r?\n$/.test(text) ? newline : ''}${EXCLUDE_KEY}:${newline} - ${name}${newline}`;
|
|
32
|
+
const inline = lines[index].slice(EXCLUDE_KEY.length + 1).trim();
|
|
33
|
+
if (inline.startsWith('[') && inline.endsWith(']')) {
|
|
34
|
+
lines[index] = `${EXCLUDE_KEY}: [${[...releaseAgeExemptions(text), name].map(item => JSON.stringify(item)).join(', ')}]`;
|
|
35
|
+
return lines.join(newline);
|
|
36
|
+
}
|
|
37
|
+
if (inline)
|
|
38
|
+
throw new Error('UNRECOGNISED_WORKSPACE');
|
|
39
|
+
let end = index + 1;
|
|
40
|
+
while (end < lines.length && /^\s+-/.test(lines[end]))
|
|
41
|
+
end++;
|
|
42
|
+
const indent = end > index + 1 ? lines[index + 1].match(/^\s+/)[0] : ' ';
|
|
43
|
+
lines.splice(end, 0, `${indent}- ${name}`);
|
|
44
|
+
return lines.join(newline);
|
|
45
|
+
}
|
|
3
46
|
/**
|
|
4
47
|
* pnpm, which DSH installs plugins with, refuses npm versions published less than a day ago
|
|
5
48
|
* (minimumReleaseAge, a supply-chain safeguard). Updates from npm wait until then.
|
|
@@ -38,15 +81,56 @@ export class Updater {
|
|
|
38
81
|
spec;
|
|
39
82
|
fetcher;
|
|
40
83
|
now;
|
|
84
|
+
workspace;
|
|
41
85
|
checked;
|
|
42
86
|
state;
|
|
43
87
|
error;
|
|
44
88
|
pending;
|
|
45
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @param workspace - this DSH profile's pnpm-workspace.yaml, where pnpm reads the release-age
|
|
91
|
+
* exemptions; undefined when the profile is unknown.
|
|
92
|
+
*/
|
|
93
|
+
constructor(current, spec, fetcher = fetch, now = Date.now, workspace = () => undefined) {
|
|
46
94
|
this.current = current;
|
|
47
95
|
this.spec = spec;
|
|
48
96
|
this.fetcher = fetcher;
|
|
49
97
|
this.now = now;
|
|
98
|
+
this.workspace = workspace;
|
|
99
|
+
}
|
|
100
|
+
/** Whether pnpm in this profile already lets TheOne install versions under a day old. */
|
|
101
|
+
get exempt() {
|
|
102
|
+
const file = this.workspace();
|
|
103
|
+
if (!file)
|
|
104
|
+
return false;
|
|
105
|
+
try {
|
|
106
|
+
return releaseAgeExemptions(readFileSync(file, 'utf8')).includes(PACKAGE_NAME);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Exempt TheOne, and only TheOne, from pnpm's release-age rule in this profile. */
|
|
113
|
+
allowFresh() {
|
|
114
|
+
const file = this.workspace();
|
|
115
|
+
if (!file)
|
|
116
|
+
throw new Error('NO_PROFILE');
|
|
117
|
+
let text = '';
|
|
118
|
+
try {
|
|
119
|
+
text = readFileSync(file, 'utf8');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== 'ENOENT')
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const next = withReleaseAgeExemption(text);
|
|
126
|
+
if (next === text)
|
|
127
|
+
return;
|
|
128
|
+
writeFileSync(`${file}.theone-tmp`, next, { mode: 0o600 });
|
|
129
|
+
renameSync(`${file}.theone-tmp`, file);
|
|
130
|
+
if (this.state === 'failed') {
|
|
131
|
+
this.state = undefined;
|
|
132
|
+
this.error = undefined;
|
|
133
|
+
}
|
|
50
134
|
}
|
|
51
135
|
get source() { return installSource(this.spec()); }
|
|
52
136
|
/** The latest known status; checks again at most every six hours (or now, when forced). */
|
|
@@ -68,7 +152,7 @@ export class Updater {
|
|
|
68
152
|
const status = await this.status();
|
|
69
153
|
if (!status.available || !status.installable || !installer || this.state === 'installing' || this.state === 'reloading' || this.state === 'restart')
|
|
70
154
|
return this.snapshot();
|
|
71
|
-
const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${
|
|
155
|
+
const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${status.latest}`;
|
|
72
156
|
this.state = 'installing';
|
|
73
157
|
this.error = undefined;
|
|
74
158
|
try {
|
|
@@ -94,11 +178,15 @@ export class Updater {
|
|
|
94
178
|
return this.snapshot();
|
|
95
179
|
}
|
|
96
180
|
snapshot() {
|
|
97
|
-
const
|
|
181
|
+
const exempt = this.source === 'npm' && this.exempt;
|
|
182
|
+
// Exempt from the release-age rule, the newest version is ready as soon as it is published.
|
|
183
|
+
const fresh = this.checked?.waiting && compareVersions(this.checked.waiting.version, this.current) > 0 ? this.checked.waiting : undefined;
|
|
184
|
+
const latest = exempt && fresh ? fresh.version : this.checked?.latest;
|
|
98
185
|
const source = this.source;
|
|
99
186
|
const available = !!latest && compareVersions(latest, this.current) > 0;
|
|
100
|
-
const waiting =
|
|
187
|
+
const waiting = fresh && !exempt ? { waiting: fresh } : {};
|
|
101
188
|
return { current: this.current, ...(latest ? { latest } : {}), available, source, ...waiting,
|
|
189
|
+
...(source === 'npm' ? { exempt, canExempt: !!this.workspace() } : {}),
|
|
102
190
|
installable: available && (source === 'npm' || (source === 'github' && !!this.checked?.sha)),
|
|
103
191
|
...(this.state ? { state: this.state } : {}), ...(this.error ?? this.checked?.error ? { error: this.error ?? this.checked?.error } : {}) };
|
|
104
192
|
}
|