dsh-nerd-mode 0.4.3 → 0.4.5
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/NOTICE.md +1 -1
- package/README.md +305 -218
- package/README.zh.md +305 -220
- package/client.js +457 -62
- package/package.json +1 -1
- package/skills/diagram/SKILL.md +0 -2
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
package/client.js
CHANGED
|
@@ -59,7 +59,110 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
&& left.unseenQuestionType === right.unseenQuestionType;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const MARKET_API = '/dsh-market/api/v1';
|
|
63
|
+
const PACKAGE = 'dsh-nerd-mode';
|
|
64
|
+
const UPDATE_SCHEMA = 'dsh-market/update-api/v1';
|
|
65
|
+
const REQUEST_TIMEOUT = 10000;
|
|
66
|
+
const POLL_INTERVAL = 1000;
|
|
67
|
+
|
|
68
|
+
function useT(locale) {
|
|
69
|
+
React.useSyncExternalStore(
|
|
70
|
+
(listener) => locale.subscribe(listener),
|
|
71
|
+
() => locale.getSnapshot(),
|
|
72
|
+
() => locale.getSnapshot(),
|
|
73
|
+
);
|
|
74
|
+
return (text) => locale.resolveText(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Every Market call carries the session cookies and gives up after 10 s.
|
|
78
|
+
async function marketRequest(url, options = {}, life = null) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
if (life !== null) life.controller = controller;
|
|
81
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
credentials: 'same-origin',
|
|
85
|
+
...options,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
const body = await response.json().catch(() => null);
|
|
89
|
+
return { ok: response.ok, status: response.status, body };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (controller.signal.aborted) throw new Error('DSH Market did not answer within 10 seconds.');
|
|
92
|
+
throw error;
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (life !== null && life.controller === controller) life.controller = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function marketFailure(result, fallback) {
|
|
100
|
+
const body = result?.body ?? {};
|
|
101
|
+
return body.error
|
|
102
|
+
?? body.failure?.message
|
|
103
|
+
?? body.failure?.code
|
|
104
|
+
?? `${fallback} (HTTP ${result?.status ?? 0}).`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The schema is optional on the operation endpoints, but a wrong one is a different API.
|
|
108
|
+
const wrongSchema = (body) => body?.schema !== undefined && body.schema !== UPDATE_SCHEMA;
|
|
109
|
+
|
|
110
|
+
async function readCapabilities(life) {
|
|
111
|
+
const result = await marketRequest(`${MARKET_API}/capabilities`, {}, life);
|
|
112
|
+
const body = result.body ?? {};
|
|
113
|
+
if (!result.ok) throw new Error(marketFailure(result, 'DSH Market did not answer'));
|
|
114
|
+
if (body.schema !== UPDATE_SCHEMA || body.features?.update !== true) {
|
|
115
|
+
throw new Error('DSH Market does not support plugin updates.');
|
|
116
|
+
}
|
|
117
|
+
const endpoints = body.endpoints ?? {};
|
|
118
|
+
if (typeof endpoints.updates !== 'string' || typeof endpoints.operations !== 'string') {
|
|
119
|
+
throw new Error('DSH Market returned no update endpoints.');
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
endpoints: { updates: endpoints.updates, operations: endpoints.operations },
|
|
123
|
+
profile: typeof body.profile === 'string' ? body.profile : null,
|
|
124
|
+
restart: body.features.restart === true && typeof endpoints.restart === 'string' ? endpoints.restart : null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readRelease(market, force, life) {
|
|
129
|
+
const url = `${market.endpoints.updates}?name=${encodeURIComponent(PACKAGE)}${force ? '&force=1' : ''}`;
|
|
130
|
+
const result = await marketRequest(url, {}, life);
|
|
131
|
+
const body = result.body ?? {};
|
|
132
|
+
if (!result.ok) throw new Error(marketFailure(result, 'DSH Market did not check for an update'));
|
|
133
|
+
if (body.schema !== UPDATE_SCHEMA || typeof body.package !== 'object' || body.package === null) {
|
|
134
|
+
throw new Error('DSH Market returned no release for this plugin.');
|
|
135
|
+
}
|
|
136
|
+
return body.package;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
140
|
+
|
|
141
|
+
// Only a plain `major.minor.patch` pair can prove the local copy is newer.
|
|
142
|
+
function compareVersions(left, right) {
|
|
143
|
+
const first = VERSION.exec(String(left ?? ''));
|
|
144
|
+
const second = VERSION.exec(String(right ?? ''));
|
|
145
|
+
if (first === null || second === null) return 0;
|
|
146
|
+
for (let at = 1; at <= 3; at += 1) {
|
|
147
|
+
const difference = Number(first[at]) - Number(second[at]);
|
|
148
|
+
if (difference !== 0) return difference;
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const manualCommand = (profile) => `dsh plugin --profile ${profile ?? '<profile>'} add ${PACKAGE}`;
|
|
154
|
+
|
|
155
|
+
function NerdModeSettingsEntry({ configForms, close, locale }) {
|
|
156
|
+
return React.createElement(
|
|
157
|
+
'div',
|
|
158
|
+
{ 'data-nerd-mode-settings': '', style: settingsColumnStyle },
|
|
159
|
+
React.createElement(NerdModeSettingsBody, { configForms, close, locale }),
|
|
160
|
+
React.createElement(AboutGroup, { locale }),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function NerdModeSettingsBody({ configForms, close, locale }) {
|
|
165
|
+
const t = useT(locale);
|
|
63
166
|
const description = configForms.describe();
|
|
64
167
|
const described = React.useSyncExternalStore(
|
|
65
168
|
(listener) => description.subscribe(listener),
|
|
@@ -69,30 +172,32 @@ window.__ModuleLoader__.load({
|
|
|
69
172
|
const settingsExist = described.view?.namespaces.some(({ ns }) => ns === SETTINGS_NAMESPACE) === true;
|
|
70
173
|
if (!settingsExist) {
|
|
71
174
|
const loading = (described.status === 'idle' || described.status === 'loading') && !described.error;
|
|
72
|
-
return React.createElement(
|
|
175
|
+
return React.createElement(React.Fragment, null,
|
|
73
176
|
React.createElement('p', {
|
|
74
177
|
role: 'status',
|
|
75
178
|
'aria-busy': loading,
|
|
76
179
|
style: feedbackStyle,
|
|
77
180
|
}, loading
|
|
78
|
-
? 'Loading Nerd Mode settings…'
|
|
181
|
+
? t({ en: 'Loading Nerd Mode settings…', zh: '正在加载 Nerd Mode 设置…' })
|
|
79
182
|
: described.status === 'ready'
|
|
80
|
-
? 'Nerd Mode settings are unavailable in this profile.'
|
|
81
|
-
: 'Nerd Mode settings could not be loaded. Refresh the Settings window and try again.'),
|
|
183
|
+
? t({ en: 'Nerd Mode settings are unavailable in this profile.', zh: '此 profile 中没有 Nerd Mode 设置。' })
|
|
184
|
+
: t({ en: 'Nerd Mode settings could not be loaded. Refresh the Settings window and try again.', zh: '无法加载 Nerd Mode 设置。请刷新设置窗口后重试。' })),
|
|
82
185
|
loading ? null : React.createElement('button', {
|
|
83
186
|
type: 'button',
|
|
84
187
|
onClick: close,
|
|
85
188
|
style: secondaryButtonStyle,
|
|
86
|
-
}, 'Close'),
|
|
189
|
+
}, t({ en: 'Close', zh: '关闭' })),
|
|
87
190
|
);
|
|
88
191
|
}
|
|
89
192
|
return React.createElement(NerdModeSettings, {
|
|
90
193
|
close,
|
|
194
|
+
locale,
|
|
91
195
|
scope: configForms.get(SETTINGS_NAMESPACE),
|
|
92
196
|
});
|
|
93
197
|
}
|
|
94
198
|
|
|
95
|
-
function NerdModeSettings({ scope, close }) {
|
|
199
|
+
function NerdModeSettings({ scope, close, locale }) {
|
|
200
|
+
const t = useT(locale);
|
|
96
201
|
const snapshot = React.useSyncExternalStore(
|
|
97
202
|
(listener) => scope.subscribe(listener),
|
|
98
203
|
() => scope.getSnapshot(),
|
|
@@ -111,19 +216,19 @@ window.__ModuleLoader__.load({
|
|
|
111
216
|
role: 'status',
|
|
112
217
|
'aria-busy': true,
|
|
113
218
|
style: feedbackStyle,
|
|
114
|
-
}, 'Loading Nerd Mode settings…');
|
|
219
|
+
}, t({ en: 'Loading Nerd Mode settings…', zh: '正在加载 Nerd Mode 设置…' }));
|
|
115
220
|
}
|
|
116
221
|
if (snapshot?.status !== 'ready') {
|
|
117
|
-
return React.createElement(
|
|
222
|
+
return React.createElement(React.Fragment, null,
|
|
118
223
|
React.createElement('p', { role: 'status', style: feedbackStyle },
|
|
119
224
|
snapshot?.status === 'unavailable'
|
|
120
|
-
? 'Nerd Mode settings are unavailable in this profile.'
|
|
121
|
-
: 'Nerd Mode settings could not be loaded. Refresh the Settings window and try again.'),
|
|
225
|
+
? t({ en: 'Nerd Mode settings are unavailable in this profile.', zh: '此 profile 中没有 Nerd Mode 设置。' })
|
|
226
|
+
: t({ en: 'Nerd Mode settings could not be loaded. Refresh the Settings window and try again.', zh: '无法加载 Nerd Mode 设置。请刷新设置窗口后重试。' })),
|
|
122
227
|
React.createElement('button', {
|
|
123
228
|
type: 'button',
|
|
124
229
|
onClick: close,
|
|
125
230
|
style: secondaryButtonStyle,
|
|
126
|
-
}, 'Close'),
|
|
231
|
+
}, t({ en: 'Close', zh: '关闭' })),
|
|
127
232
|
);
|
|
128
233
|
}
|
|
129
234
|
const current = settingsValues(snapshot);
|
|
@@ -153,38 +258,39 @@ window.__ModuleLoader__.load({
|
|
|
153
258
|
setFeedback(null);
|
|
154
259
|
try {
|
|
155
260
|
const accepted = await scope.mutate(operations, latest.revision);
|
|
156
|
-
if (accepted === false)
|
|
261
|
+
if (accepted === false) {
|
|
262
|
+
throw new Error(t({ en: 'The profile rejected these settings.', zh: 'profile 拒绝了这些设置。' }));
|
|
263
|
+
}
|
|
157
264
|
setDraft(settingsValues(scope.getSnapshot()));
|
|
158
|
-
setFeedback(
|
|
265
|
+
setFeedback({
|
|
266
|
+
tone: 'status',
|
|
267
|
+
text: {
|
|
268
|
+
en: 'Saved to this profile. Changes apply to the next study-plus session.',
|
|
269
|
+
zh: '已保存到此 profile。更改将在下次 study-plus 会话中生效。',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
159
272
|
} catch (error) {
|
|
160
|
-
setFeedback(
|
|
273
|
+
setFeedback({
|
|
274
|
+
tone: 'alert',
|
|
275
|
+
text: {
|
|
276
|
+
en: `Could not save settings: ${error?.message ?? 'the profile rejected the changes'}`,
|
|
277
|
+
zh: `无法保存设置:${error?.message ?? 'profile 拒绝了这些更改'}`,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
161
280
|
} finally {
|
|
162
281
|
setSaving(false);
|
|
163
282
|
}
|
|
164
283
|
};
|
|
165
284
|
|
|
166
285
|
return React.createElement(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
'data-nerd-mode-settings': '',
|
|
170
|
-
style: {
|
|
171
|
-
boxSizing: 'border-box',
|
|
172
|
-
color: 'var(--dsw-alias-label-primary)',
|
|
173
|
-
display: 'flex',
|
|
174
|
-
flexDirection: 'column',
|
|
175
|
-
fontFamily: 'var(--dsw-font-family, inherit)',
|
|
176
|
-
gap: 16,
|
|
177
|
-
maxWidth: 720,
|
|
178
|
-
paddingBottom: 24,
|
|
179
|
-
width: '100%',
|
|
180
|
-
},
|
|
181
|
-
},
|
|
286
|
+
React.Fragment,
|
|
287
|
+
null,
|
|
182
288
|
React.createElement(
|
|
183
289
|
'header',
|
|
184
290
|
{ style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
185
291
|
React.createElement('h2', {
|
|
186
292
|
style: { fontSize: 16, fontWeight: 600, lineHeight: '24px', margin: 0 },
|
|
187
|
-
}, 'Nerd Mode'),
|
|
293
|
+
}, t({ en: 'Nerd Mode', zh: 'Nerd Mode' })),
|
|
188
294
|
React.createElement('p', {
|
|
189
295
|
style: {
|
|
190
296
|
color: 'var(--dsw-alias-label-tertiary)',
|
|
@@ -192,7 +298,7 @@ window.__ModuleLoader__.load({
|
|
|
192
298
|
lineHeight: '22px',
|
|
193
299
|
margin: 0,
|
|
194
300
|
},
|
|
195
|
-
}, 'Profile defaults for new study-plus sessions.'),
|
|
301
|
+
}, t({ en: 'Profile defaults for new study-plus sessions.', zh: '新 study-plus 会话的 profile 默认值。' })),
|
|
196
302
|
),
|
|
197
303
|
React.createElement(
|
|
198
304
|
'section',
|
|
@@ -203,8 +309,8 @@ window.__ModuleLoader__.load({
|
|
|
203
309
|
React.createElement(
|
|
204
310
|
'div',
|
|
205
311
|
{ style: { minWidth: 0 } },
|
|
206
|
-
React.createElement('h3', { style: groupTitleStyle }, 'Recall Session / 课前回忆'),
|
|
207
|
-
React.createElement('p', { style: groupDescriptionStyle }, 'One ungraded recall question before teaching.'),
|
|
312
|
+
React.createElement('h3', { style: groupTitleStyle }, t({ en: 'Recall Session / 课前回忆', zh: '课前回忆' })),
|
|
313
|
+
React.createElement('p', { style: groupDescriptionStyle }, t({ en: 'One ungraded recall question before teaching.', zh: '讲授前的一次不计分课前回忆。' })),
|
|
208
314
|
),
|
|
209
315
|
React.createElement(
|
|
210
316
|
'button',
|
|
@@ -212,14 +318,16 @@ window.__ModuleLoader__.load({
|
|
|
212
318
|
type: 'button',
|
|
213
319
|
role: 'switch',
|
|
214
320
|
'aria-checked': draft.recallSessionEnabled,
|
|
215
|
-
'aria-label': 'Recall Session',
|
|
321
|
+
'aria-label': t({ en: 'Recall Session', zh: '课前回忆' }),
|
|
216
322
|
'data-setting': 'recallSessionEnabled',
|
|
217
323
|
'data-state': draft.recallSessionEnabled ? 'on' : 'off',
|
|
218
324
|
disabled: !writable || saving,
|
|
219
325
|
onClick: () => updateDraft('recallSessionEnabled', !draft.recallSessionEnabled),
|
|
220
326
|
style: switchStyle(draft.recallSessionEnabled, !writable || saving),
|
|
221
327
|
},
|
|
222
|
-
React.createElement('span', null, draft.recallSessionEnabled
|
|
328
|
+
React.createElement('span', null, draft.recallSessionEnabled
|
|
329
|
+
? t({ en: 'On', zh: '开' })
|
|
330
|
+
: t({ en: 'Off', zh: '关' })),
|
|
223
331
|
React.createElement('span', {
|
|
224
332
|
'aria-hidden': 'true',
|
|
225
333
|
style: {
|
|
@@ -241,8 +349,8 @@ window.__ModuleLoader__.load({
|
|
|
241
349
|
React.createElement(
|
|
242
350
|
'div',
|
|
243
351
|
null,
|
|
244
|
-
React.createElement('h3', { style: groupTitleStyle }, 'Post-lecture questions'),
|
|
245
|
-
React.createElement('p', { style: groupDescriptionStyle }, 'Choose how many unseen questions follow each lecture.'),
|
|
352
|
+
React.createElement('h3', { style: groupTitleStyle }, t({ en: 'Post-lecture questions', zh: '课后提问' })),
|
|
353
|
+
React.createElement('p', { style: groupDescriptionStyle }, t({ en: 'Choose how many unseen questions follow each lecture.', zh: '选择每节课后出几道题。' })),
|
|
246
354
|
),
|
|
247
355
|
React.createElement(
|
|
248
356
|
'div',
|
|
@@ -250,7 +358,7 @@ window.__ModuleLoader__.load({
|
|
|
250
358
|
React.createElement(
|
|
251
359
|
'label',
|
|
252
360
|
{ htmlFor: 'nerd-mode-question-count', style: fieldLabelStyle },
|
|
253
|
-
React.createElement('span', null, 'Question count'),
|
|
361
|
+
React.createElement('span', null, t({ en: 'Question count', zh: '题数' })),
|
|
254
362
|
React.createElement('input', {
|
|
255
363
|
id: 'nerd-mode-question-count',
|
|
256
364
|
type: 'number',
|
|
@@ -264,12 +372,12 @@ window.__ModuleLoader__.load({
|
|
|
264
372
|
onChange: (event) => updateDraft('unseenQuestionCount', event.target.value),
|
|
265
373
|
style: fieldControlStyle,
|
|
266
374
|
}),
|
|
267
|
-
React.createElement('span', { style: fieldHintStyle }, 'Use 0 to skip unseen questions.'),
|
|
375
|
+
React.createElement('span', { style: fieldHintStyle }, t({ en: 'Use 0 to skip unseen questions.', zh: '设为 0 可跳过课后提问。' })),
|
|
268
376
|
),
|
|
269
377
|
React.createElement(
|
|
270
378
|
'label',
|
|
271
379
|
{ htmlFor: 'nerd-mode-question-type', style: fieldLabelStyle },
|
|
272
|
-
React.createElement('span', null, 'Question type'),
|
|
380
|
+
React.createElement('span', null, t({ en: 'Question type', zh: '题型' })),
|
|
273
381
|
React.createElement(
|
|
274
382
|
'select',
|
|
275
383
|
{
|
|
@@ -280,11 +388,11 @@ window.__ModuleLoader__.load({
|
|
|
280
388
|
onChange: (event) => updateDraft('unseenQuestionType', event.target.value),
|
|
281
389
|
style: fieldControlStyle,
|
|
282
390
|
},
|
|
283
|
-
React.createElement('option', { value: 'short-answer' }, 'Short answer'),
|
|
284
|
-
React.createElement('option', { value: 'multiple-choice' }, 'Multiple choice'),
|
|
285
|
-
React.createElement('option', { value: 'mixed' }, 'Mixed'),
|
|
391
|
+
React.createElement('option', { value: 'short-answer' }, t({ en: 'Short answer', zh: '简答' })),
|
|
392
|
+
React.createElement('option', { value: 'multiple-choice' }, t({ en: 'Multiple choice', zh: '四选一' })),
|
|
393
|
+
React.createElement('option', { value: 'mixed' }, t({ en: 'Mixed', zh: '混合' })),
|
|
286
394
|
),
|
|
287
|
-
React.createElement('span', { style: fieldHintStyle }, 'Mixed chooses a type for each question.'),
|
|
395
|
+
React.createElement('span', { style: fieldHintStyle }, t({ en: 'Mixed chooses a type for each question.', zh: '混合会为每道题随机选题型。' })),
|
|
288
396
|
),
|
|
289
397
|
),
|
|
290
398
|
),
|
|
@@ -306,7 +414,7 @@ window.__ModuleLoader__.load({
|
|
|
306
414
|
disabled: !writable || saving,
|
|
307
415
|
onClick: restoreDefaults,
|
|
308
416
|
style: secondaryButtonStyle,
|
|
309
|
-
}, 'Restore defaults'),
|
|
417
|
+
}, t({ en: 'Restore defaults', zh: '恢复默认' })),
|
|
310
418
|
React.createElement(
|
|
311
419
|
'div',
|
|
312
420
|
{ style: { display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' } },
|
|
@@ -315,27 +423,301 @@ window.__ModuleLoader__.load({
|
|
|
315
423
|
disabled: !dirty || saving,
|
|
316
424
|
onClick: cancel,
|
|
317
425
|
style: secondaryButtonStyle,
|
|
318
|
-
}, 'Cancel'),
|
|
426
|
+
}, t({ en: 'Cancel', zh: '取消' })),
|
|
319
427
|
React.createElement('button', {
|
|
320
428
|
type: 'button',
|
|
321
429
|
disabled: !dirty || !validSettings(draft) || !writable || saving,
|
|
322
430
|
onClick: save,
|
|
323
431
|
style: primaryButtonStyle,
|
|
324
|
-
}, saving ? 'Saving…' : 'Save'),
|
|
432
|
+
}, saving ? t({ en: 'Saving…', zh: '保存中…' }) : t({ en: 'Save', zh: '保存' })),
|
|
325
433
|
),
|
|
326
434
|
),
|
|
327
435
|
snapshot?.writable === false
|
|
328
|
-
? React.createElement('p', { role: 'status', style: feedbackStyle }, 'These profile settings are read-only.')
|
|
436
|
+
? React.createElement('p', { role: 'status', style: feedbackStyle }, t({ en: 'These profile settings are read-only.', zh: '这些 profile 设置为只读。' }))
|
|
329
437
|
: null,
|
|
330
438
|
feedback === null
|
|
331
439
|
? null
|
|
332
440
|
: React.createElement('p', {
|
|
333
|
-
role: feedback.
|
|
441
|
+
role: feedback.tone,
|
|
334
442
|
style: feedbackStyle,
|
|
335
|
-
}, feedback),
|
|
443
|
+
}, t(feedback.text)),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function AboutGroup({ locale }) {
|
|
448
|
+
const t = useT(locale);
|
|
449
|
+
const [life] = React.useState(() => ({ cancelled: false, timer: null, controller: null }));
|
|
450
|
+
const [view, setView] = React.useState({ phase: 'checking' });
|
|
451
|
+
|
|
452
|
+
// An answer that arrives after the panel closed writes nothing.
|
|
453
|
+
const show = (next) => {
|
|
454
|
+
if (!life.cancelled) setView(next);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
async function check(force) {
|
|
458
|
+
setView({ phase: 'checking' });
|
|
459
|
+
let market;
|
|
460
|
+
try {
|
|
461
|
+
market = await readCapabilities(life);
|
|
462
|
+
} catch {
|
|
463
|
+
show({ phase: 'unavailable' });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const release = await readRelease(market, force, life);
|
|
468
|
+
show({ phase: 'ready', market, release, confirming: false, linkedNote: false });
|
|
469
|
+
} catch (error) {
|
|
470
|
+
show({
|
|
471
|
+
phase: 'failed',
|
|
472
|
+
market,
|
|
473
|
+
release: null,
|
|
474
|
+
message: error?.message ?? 'The update check failed.',
|
|
475
|
+
retry: 'check',
|
|
476
|
+
force: false,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function follow(market, operationId, release) {
|
|
482
|
+
return new Promise((resolve, reject) => {
|
|
483
|
+
const tick = async () => {
|
|
484
|
+
if (life.cancelled) {
|
|
485
|
+
resolve(null);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
let result;
|
|
489
|
+
try {
|
|
490
|
+
result = await marketRequest(`${market.endpoints.operations}?operationId=${encodeURIComponent(operationId)}`, {}, life);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
reject(error);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (life.cancelled) {
|
|
496
|
+
resolve(null);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!result.ok || wrongSchema(result.body)) {
|
|
500
|
+
reject(new Error(marketFailure(result, 'The update could not be followed')));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const operation = result.body?.operation ?? {};
|
|
504
|
+
if (operation.state === 'succeeded') {
|
|
505
|
+
resolve(operation);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (operation.state === 'failed' || operation.state === 'cancelled' || operation.state === 'rolled-back') {
|
|
509
|
+
const error = new Error(operation.failure?.message ?? operation.failure?.code ?? `The update ${operation.state}.`);
|
|
510
|
+
error.code = operation.failure?.code ?? null;
|
|
511
|
+
reject(error);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
show({ phase: 'updating', market, release, progress: operation.state ?? 'running' });
|
|
515
|
+
life.timer = setTimeout(tick, POLL_INTERVAL);
|
|
516
|
+
};
|
|
517
|
+
tick();
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function install(market, release, force) {
|
|
522
|
+
setView({ phase: 'updating', market, release, progress: 'queued' });
|
|
523
|
+
try {
|
|
524
|
+
const result = await marketRequest(market.endpoints.updates, {
|
|
525
|
+
method: 'POST',
|
|
526
|
+
headers: { 'content-type': 'application/json' },
|
|
527
|
+
body: JSON.stringify({ packageName: PACKAGE, ...(force ? { force: true } : {}) }),
|
|
528
|
+
}, life);
|
|
529
|
+
if (!result.ok || wrongSchema(result.body)) {
|
|
530
|
+
const error = new Error(marketFailure(result, 'The update did not start'));
|
|
531
|
+
error.code = result.body?.failure?.code ?? null;
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
const operationId = result.body?.operation?.operationId;
|
|
535
|
+
if (typeof operationId !== 'string') throw new Error('DSH Market returned no operation to follow.');
|
|
536
|
+
const operation = await follow(market, operationId, release);
|
|
537
|
+
if (operation === null) return;
|
|
538
|
+
show({
|
|
539
|
+
phase: 'installed',
|
|
540
|
+
market,
|
|
541
|
+
release,
|
|
542
|
+
version: operation.installedVersion ?? release.latestVersion,
|
|
543
|
+
restartNeeded: operation.outcome?.restartRequired === true,
|
|
544
|
+
restarting: false,
|
|
545
|
+
restartSent: false,
|
|
546
|
+
});
|
|
547
|
+
} catch (error) {
|
|
548
|
+
const code = error?.code ?? null;
|
|
549
|
+
show({
|
|
550
|
+
phase: 'failed',
|
|
551
|
+
market,
|
|
552
|
+
release,
|
|
553
|
+
message: error?.message ?? 'The update failed.',
|
|
554
|
+
retry: 'install',
|
|
555
|
+
force: code === 'RELEASE_TOO_FRESH' || code === 'VERSION_UNCHANGED',
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function restart(current) {
|
|
561
|
+
setView({ ...current, restarting: true });
|
|
562
|
+
try {
|
|
563
|
+
const result = await marketRequest(current.market.restart, {
|
|
564
|
+
method: 'POST',
|
|
565
|
+
headers: { 'content-type': 'application/json' },
|
|
566
|
+
body: '{}',
|
|
567
|
+
}, life);
|
|
568
|
+
if (!result.ok) throw new Error(marketFailure(result, 'DSH could not restart'));
|
|
569
|
+
show({ ...current, restarting: false, restartSent: true });
|
|
570
|
+
} catch (error) {
|
|
571
|
+
show({
|
|
572
|
+
phase: 'failed',
|
|
573
|
+
market: current.market,
|
|
574
|
+
release: current.release,
|
|
575
|
+
message: error?.message ?? 'DSH could not restart.',
|
|
576
|
+
retry: 'check',
|
|
577
|
+
force: false,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
React.useEffect(() => {
|
|
583
|
+
check(false);
|
|
584
|
+
return () => {
|
|
585
|
+
life.cancelled = true;
|
|
586
|
+
if (life.timer !== null) clearTimeout(life.timer);
|
|
587
|
+
if (life.controller !== null) life.controller.abort();
|
|
588
|
+
};
|
|
589
|
+
}, []);
|
|
590
|
+
|
|
591
|
+
const busy = view.phase === 'checking' || view.phase === 'updating' || view.restarting === true;
|
|
592
|
+
const button = (text, onClick, disabled = false, primary = false) => React.createElement('button', {
|
|
593
|
+
type: 'button',
|
|
594
|
+
disabled,
|
|
595
|
+
onClick,
|
|
596
|
+
style: primary ? primaryButtonStyle : secondaryButtonStyle,
|
|
597
|
+
}, text);
|
|
598
|
+
const code = (text) => React.createElement('code', { style: commandStyle }, text);
|
|
599
|
+
const checkAgain = button(t({ en: 'Check again', zh: '重新检查' }), () => check(true));
|
|
600
|
+
const versions = (release) => t({
|
|
601
|
+
en: `Installed v${release.installedVersion} · latest v${release.latestVersion}`,
|
|
602
|
+
zh: `已安装 v${release.installedVersion} · 最新 v${release.latestVersion}`,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
let versionLine = null;
|
|
606
|
+
const feedback = [];
|
|
607
|
+
let actions = null;
|
|
608
|
+
|
|
609
|
+
if (view.phase === 'checking') {
|
|
610
|
+
versionLine = t({ en: 'Checking…', zh: '正在检查…' });
|
|
611
|
+
} else if (view.phase === 'unavailable') {
|
|
612
|
+
feedback.push(t({
|
|
613
|
+
en: 'DSH Market is not available on this host, so Nerd Mode cannot check for updates. Run this command in a terminal:',
|
|
614
|
+
zh: '此主机上没有 DSH Market,因此 Nerd Mode 无法检查更新。请在终端运行:',
|
|
615
|
+
}));
|
|
616
|
+
feedback.push(code(manualCommand(null)));
|
|
617
|
+
} else if (view.phase === 'failed') {
|
|
618
|
+
if (view.release !== null) versionLine = versions(view.release);
|
|
619
|
+
feedback.push(t({ en: 'The update did not finish: ', zh: '更新未完成:' }));
|
|
620
|
+
feedback.push(view.message);
|
|
621
|
+
feedback.push(t({ en: ' Install it by hand with:', zh: ' 可手动安装:' }));
|
|
622
|
+
feedback.push(code(manualCommand(view.market?.profile ?? null)));
|
|
623
|
+
actions = [button(t({ en: 'Try again', zh: '重试' }), () => {
|
|
624
|
+
if (view.retry === 'install') install(view.market, view.release, view.force === true);
|
|
625
|
+
else check(true);
|
|
626
|
+
})];
|
|
627
|
+
} else if (view.phase === 'installed') {
|
|
628
|
+
versionLine = t({ en: `Installed v${view.version}`, zh: `已安装 v${view.version}` });
|
|
629
|
+
feedback.push(view.restartSent
|
|
630
|
+
? t({ en: `DSH is restarting to load v${view.version}.`, zh: `DSH 正在重启以加载 v${view.version}。` })
|
|
631
|
+
: t({ en: `Restart DSH to use v${view.version}.`, zh: `重启 DSH 以使用 v${view.version}。` }));
|
|
632
|
+
actions = view.restartNeeded && view.market.restart !== null && view.restartSent !== true
|
|
633
|
+
? [button(
|
|
634
|
+
t({ en: view.restarting ? 'Restarting…' : 'Restart now', zh: view.restarting ? '正在重启…' : '立即重启' }),
|
|
635
|
+
() => restart(view),
|
|
636
|
+
view.restarting === true,
|
|
637
|
+
true,
|
|
638
|
+
)]
|
|
639
|
+
: [checkAgain];
|
|
640
|
+
} else if (view.release.source === 'linked') {
|
|
641
|
+
versionLine = t({ en: `Local v${view.release.installedVersion}`, zh: `本地 v${view.release.installedVersion}` });
|
|
642
|
+
actions = [button(t({ en: 'About local installs', zh: '关于本地安装' }), () => {
|
|
643
|
+
setView({ ...view, linkedNote: view.linkedNote !== true });
|
|
644
|
+
})];
|
|
645
|
+
if (view.linkedNote === true) {
|
|
646
|
+
feedback.push(t({ en: 'This copy is linked to a folder. Update it with git, not npm.', zh: '此副本链接到本地文件夹。请用 git 更新,不要用 npm。' }));
|
|
647
|
+
}
|
|
648
|
+
} else if (view.phase === 'updating') {
|
|
649
|
+
versionLine = versions(view.release);
|
|
650
|
+
feedback.push(t({ en: `Update in progress (${view.progress}).`, zh: `更新进行中(${view.progress})。` }));
|
|
651
|
+
actions = [button(t({ en: 'Updating…', zh: '正在更新…' }), () => {}, true)];
|
|
652
|
+
} else if (view.release.updateAvailable === true) {
|
|
653
|
+
versionLine = versions(view.release);
|
|
654
|
+
if (view.confirming === true) {
|
|
655
|
+
feedback.push(t({
|
|
656
|
+
en: `Install v${view.release.latestVersion} and restart DSH afterwards?`,
|
|
657
|
+
zh: `安装 v${view.release.latestVersion} 并在之后重启 DSH?`,
|
|
658
|
+
}));
|
|
659
|
+
actions = [
|
|
660
|
+
button(t({ en: 'Cancel', zh: '取消' }), () => setView({ ...view, confirming: false })),
|
|
661
|
+
button(t({ en: 'Update', zh: '更新' }), () => install(view.market, view.release, false), false, true),
|
|
662
|
+
];
|
|
663
|
+
} else {
|
|
664
|
+
actions = [button(
|
|
665
|
+
t({ en: `Update to v${view.release.latestVersion}`, zh: `更新到 v${view.release.latestVersion}` }),
|
|
666
|
+
() => setView({ ...view, confirming: true }),
|
|
667
|
+
false,
|
|
668
|
+
true,
|
|
669
|
+
)];
|
|
670
|
+
}
|
|
671
|
+
} else if (compareVersions(view.release.installedVersion, view.release.latestVersion) > 0) {
|
|
672
|
+
versionLine = t({
|
|
673
|
+
en: `Local v${view.release.installedVersion} · npm v${view.release.latestVersion}`,
|
|
674
|
+
zh: `本地 v${view.release.installedVersion} · npm v${view.release.latestVersion}`,
|
|
675
|
+
});
|
|
676
|
+
feedback.push(t({ en: 'This copy is newer than npm. No update.', zh: '此副本比 npm 上的版本更新,无需更新。' }));
|
|
677
|
+
actions = [checkAgain];
|
|
678
|
+
} else {
|
|
679
|
+
versionLine = versions(view.release);
|
|
680
|
+
actions = [checkAgain];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return React.createElement(
|
|
684
|
+
'div',
|
|
685
|
+
{ 'data-settings-group': 'about', style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
686
|
+
React.createElement(
|
|
687
|
+
'div',
|
|
688
|
+
{ style: groupStyle },
|
|
689
|
+
React.createElement(
|
|
690
|
+
'div',
|
|
691
|
+
{ style: { minWidth: 0 } },
|
|
692
|
+
React.createElement('h3', { style: groupTitleStyle }, t({ en: 'About', zh: '关于' })),
|
|
693
|
+
versionLine === null ? null : React.createElement('p', { style: groupDescriptionStyle }, versionLine),
|
|
694
|
+
),
|
|
695
|
+
actions === null ? null : React.createElement(
|
|
696
|
+
'div',
|
|
697
|
+
{ style: { alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' } },
|
|
698
|
+
...actions,
|
|
699
|
+
),
|
|
700
|
+
),
|
|
701
|
+
React.createElement('p', {
|
|
702
|
+
role: view.phase === 'failed' ? 'alert' : 'status',
|
|
703
|
+
'aria-busy': busy,
|
|
704
|
+
style: feedbackStyle,
|
|
705
|
+
}, ...feedback),
|
|
336
706
|
);
|
|
337
707
|
}
|
|
338
708
|
|
|
709
|
+
const settingsColumnStyle = {
|
|
710
|
+
boxSizing: 'border-box',
|
|
711
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
712
|
+
display: 'flex',
|
|
713
|
+
flexDirection: 'column',
|
|
714
|
+
fontFamily: 'var(--dsw-font-family, inherit)',
|
|
715
|
+
gap: 16,
|
|
716
|
+
maxWidth: 720,
|
|
717
|
+
paddingBottom: 24,
|
|
718
|
+
width: '100%',
|
|
719
|
+
};
|
|
720
|
+
|
|
339
721
|
const groupStyle = {
|
|
340
722
|
alignItems: 'center',
|
|
341
723
|
background: 'var(--dsw-alias-bg-layer-1)',
|
|
@@ -408,6 +790,16 @@ window.__ModuleLoader__.load({
|
|
|
408
790
|
lineHeight: '20px',
|
|
409
791
|
margin: 0,
|
|
410
792
|
};
|
|
793
|
+
const commandStyle = {
|
|
794
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
795
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
796
|
+
display: 'inline-block',
|
|
797
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
798
|
+
fontSize: 12,
|
|
799
|
+
lineHeight: '18px',
|
|
800
|
+
padding: '2px 6px',
|
|
801
|
+
wordBreak: 'break-all',
|
|
802
|
+
};
|
|
411
803
|
|
|
412
804
|
function switchStyle(active, disabled) {
|
|
413
805
|
const accent = 'var(--dsw-alias-state-business-primary)';
|
|
@@ -1349,17 +1741,20 @@ window.__ModuleLoader__.load({
|
|
|
1349
1741
|
}),
|
|
1350
1742
|
}, NerdModeToggle),
|
|
1351
1743
|
);
|
|
1352
|
-
ctx.
|
|
1353
|
-
ctx.slots.
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
close,
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1744
|
+
ctx.inject(['locale'], (localeCtx) => {
|
|
1745
|
+
ctx.slots.inject(SETTINGS_SLOT, () =>
|
|
1746
|
+
ctx.slots.register({
|
|
1747
|
+
name: SETTINGS_SLOT,
|
|
1748
|
+
id: 'dsh-nerd-mode',
|
|
1749
|
+
order: 30,
|
|
1750
|
+
label: LABEL,
|
|
1751
|
+
}, ({ close }) => React.createElement(NerdModeSettingsEntry, {
|
|
1752
|
+
close,
|
|
1753
|
+
configForms: ctx.configForms,
|
|
1754
|
+
locale: localeCtx.locale,
|
|
1755
|
+
})),
|
|
1756
|
+
);
|
|
1757
|
+
});
|
|
1363
1758
|
ctx.slots.inject(VIEW_SLOT, () =>
|
|
1364
1759
|
ctx.slots.register({
|
|
1365
1760
|
name: VIEW_SLOT,
|