dsh-agentone 0.5.43 → 1.0.0
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 +3 -1
- package/lib/defaults-sync.js +11 -1
- package/lib/index.js +1 -1
- package/lib/page.js +131 -39
- package/lib/proc.js +204 -33
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,7 +48,9 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
48
48
|
有本机记录但内容被改过的真冲突仍逐项人工确认,不自动覆盖。
|
|
49
49
|
- **CLI 探测、安装与授权(M4)**:`lark-cli` / `ccpg-cli` 安装与授权态三态探测;
|
|
50
50
|
未安装时「安装」一键自助安装(lark 走 `npm install -g @larksuite/cli@latest`,
|
|
51
|
-
|
|
51
|
+
无 npm 的机器退化 pnpm——含 DeepSeek Harness 桌面壳自带 Node/pnpm 工具链,
|
|
52
|
+
全新 macOS 不装 Node 也能装;ccpg 走公司 `auto-install.sh`,口径与平台
|
|
53
|
+
cli-deployd 一致);
|
|
52
54
|
已装未授权时「去授权」发起官方 Device Flow(授权链接 + ASCII 二维码 +
|
|
53
55
|
验证码 + 自动轮询确认,设备码仅存插件进程内存);
|
|
54
56
|
lark-cli 首装未 `config init` 时探测为「未配置」,「初始化应用」一键驱动
|
package/lib/defaults-sync.js
CHANGED
|
@@ -259,7 +259,17 @@ export function createDefaultsSync({ config, profiles, credentials, log, interva
|
|
|
259
259
|
promise.finally(() => manualOperations.delete(promise)).catch(() => {});
|
|
260
260
|
return promise;
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
// 快照是 /api/status(5s 轮询)与 defaults/status 的下发边界:description
|
|
263
|
+
// (技能包全文,几百字/条)只在展示层被引用,而新版页面已彻底不渲染——
|
|
264
|
+
// 在出口剥掉,轮询不再背着几十 KB 死重;内部 state 保持完整
|
|
265
|
+
const snapshot = () => {
|
|
266
|
+
const clone = structuredClone(state);
|
|
267
|
+
for (const list of [clone.skills, clone.plugins]) {
|
|
268
|
+
for (const entry of list || []) delete entry.description;
|
|
269
|
+
}
|
|
270
|
+
return clone;
|
|
271
|
+
};
|
|
272
|
+
return { sync, manualOperation, snapshot,
|
|
263
273
|
reset() { stop(); profiles.setPolicy([]); state = { source: null, revision: null,
|
|
264
274
|
checked_at: null, user_id: null, error: null, running: false, skills: [], plugins: [] }; },
|
|
265
275
|
dispose() { disposed = true; stop(); return Promise.allSettled([inFlight, ...manualOperations.keys()]); },
|
package/lib/index.js
CHANGED
|
@@ -1057,7 +1057,7 @@ export function apply(ctx, config) {
|
|
|
1057
1057
|
invalidateStatusCache();
|
|
1058
1058
|
sendJson(response, 200, { ok: true, installed: result?.installed ?? 0, failed: result?.failed ?? 0 });
|
|
1059
1059
|
} catch (error) {
|
|
1060
|
-
sendJson(response, 502, { error: error?.message || '
|
|
1060
|
+
sendJson(response, 502, { error: error?.message || '同步技能失败' });
|
|
1061
1061
|
}
|
|
1062
1062
|
});
|
|
1063
1063
|
|
package/lib/page.js
CHANGED
|
@@ -395,52 +395,87 @@ const renderPlan = async () => {
|
|
|
395
395
|
).then(() => renderPlan());
|
|
396
396
|
});
|
|
397
397
|
};
|
|
398
|
+
// 平台清单同步态与插件列表的同名合并:renderDefaults 记录待处理插件并排除
|
|
399
|
+
// 已列名条目,renderPlugins 把同步状态/原因/重试合并进同名行——同一个包只
|
|
400
|
+
// 出现一行(真实用户机 dsh-im:清单同步失败 + 本机未启用曾显示成两条)
|
|
401
|
+
let defaultsPendingPlugins = [];
|
|
402
|
+
let listedPluginNames = new Set();
|
|
403
|
+
let mergedPendingSig = '';
|
|
404
|
+
const DEFAULTS_STATE_LABELS = { current: '已符合', missing: '待安装', pending: '待应用', running: '同步中',
|
|
405
|
+
failed: '失败', conflict: '本机冲突', report_pending: '已安装,待回报', restart_required: '已安装,重启后生效' };
|
|
406
|
+
/** 同步动作按钮的统一绑定(待处理块与合并进插件行的按钮同一契约)。 */
|
|
407
|
+
const bindDefaultsActions = (el) => {
|
|
408
|
+
el.querySelectorAll('[data-default-action]').forEach((btn) => {
|
|
409
|
+
const request = () => fetch('api/defaults/' + btn.dataset.defaultAction, { method: 'POST',
|
|
410
|
+
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: btn.dataset.kind,
|
|
411
|
+
name: btn.dataset.name, confirm: btn.dataset.defaultAction === 'apply' }) });
|
|
412
|
+
btn.onclick = () => btn.dataset.defaultAction === 'apply'
|
|
413
|
+
? confirmThen(btn, '替换当前版本?', request, '已开始应用,请查看同步状态')
|
|
414
|
+
: withLoading(btn, request, '已开始重试,请查看同步状态');
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
const defaultsPendingSig = () => defaultsPendingPlugins.map((item) => [item.name, item.state,
|
|
418
|
+
item.target_version || '', item.error || ''].join('~')).join('|');
|
|
419
|
+
|
|
398
420
|
const renderDefaults = (s) => {
|
|
399
|
-
const body = $('defaults-body');
|
|
400
421
|
const config = s.defaults;
|
|
401
|
-
const
|
|
402
|
-
|
|
422
|
+
const syncBtn = $('defaults-sync-btn');
|
|
423
|
+
const labels = DEFAULTS_STATE_LABELS;
|
|
403
424
|
const sources = { platform: '平台清单', compatibility: '旧平台兼容默认', cache: '本机缓存' };
|
|
404
|
-
|
|
425
|
+
const clearBlock = (el) => { el.innerHTML = ''; delete el.dataset.fingerprint; };
|
|
426
|
+
if (!config || !s.logged_in) {
|
|
427
|
+
syncBtn.hidden = true;
|
|
428
|
+
for (const id of ['defaults-meta', 'defaults-plugins', 'defaults-skills']) clearBlock($(id));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
syncBtn.hidden = false;
|
|
432
|
+
syncBtn.disabled = Boolean(config.running);
|
|
405
433
|
const entries = [...(config.skills || []), ...(config.plugins || [])];
|
|
406
434
|
// legacy:0.5.39 之前装的技能只有平台台账、没有本机记录(见 defaults-sync)。
|
|
407
435
|
// 这类条目单独给一个批量入口,不然升级后每台机器都要点 N 次「确认应用」。
|
|
408
436
|
const legacyCount = entries.filter((item) => item.legacy).length;
|
|
409
|
-
const
|
|
410
|
-
|
|
437
|
+
const pendingCount = entries.filter((item) => item.state !== 'current').length;
|
|
438
|
+
// 待处理插件交给 renderPlugins 合并进同名行(见顶部说明)
|
|
439
|
+
defaultsPendingPlugins = (config.plugins || []).filter((item) => item.state !== 'current');
|
|
440
|
+
// 清单状态收成插件卡头一行 meta(来源/版本/时间/符合统计/错误):健康项不
|
|
441
|
+
// 铺开——技能与插件分别在各自 Tab 的列表里以「平台默认/受管」标签呈现,
|
|
442
|
+
// 这里只按类型冒泡待处理项,长描述彻底退出列表。
|
|
443
|
+
const metaHtml = '<div class="skill-sub">' + esc(sources[config.source] || '尚未检查')
|
|
444
|
+
+ (config.revision !== null ? ' · 清单版本 ' + esc(config.revision) : '')
|
|
411
445
|
+ (config.checked_at ? ' · ' + esc(new Date(config.checked_at).toLocaleString()) : '')
|
|
446
|
+
+ ' · <span style="color:var(--ok)">' + (entries.length - pendingCount) + ' 项已符合</span>'
|
|
447
|
+
+ (pendingCount ? ' · <span style="color:var(--warn)">' + pendingCount + ' 项待处理(见下)</span>' : '')
|
|
448
|
+
+ (!entries.length ? ' · 当前清单为空' : '')
|
|
412
449
|
+ (config.running ? ' · 正在检查' : '') + '</div>'
|
|
413
|
-
+ (config.error || config.profile_error
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
450
|
+
+ (config.error || config.profile_error
|
|
451
|
+
? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '');
|
|
452
|
+
const meta = $('defaults-meta');
|
|
453
|
+
if (meta.dataset.fingerprint !== metaHtml) { meta.dataset.fingerprint = metaHtml; meta.innerHTML = metaHtml; }
|
|
454
|
+
const statePill = (state) => state === 'failed' || state === 'conflict'
|
|
455
|
+
? '<span class="pill" style="font-size:11px;background:var(--err-weak);color:var(--err)">'
|
|
456
|
+
+ esc(labels[state] || state) + '</span>'
|
|
457
|
+
: '<span class="pill warn" style="font-size:11px">' + esc(labels[state] || state) + '</span>';
|
|
458
|
+
// 单行紧凑条目:名称 · 类型徽标 · 状态徽标 · 版本(→ 目标版本)+ 行内操作
|
|
459
|
+
const rowHtml = (item) => {
|
|
418
460
|
const action = ['failed', 'report_pending', 'missing'].includes(item.state) ? 'retry'
|
|
419
461
|
: ['conflict', 'pending'].includes(item.state) ? 'apply' : null;
|
|
420
462
|
const button = action ? '<button class="btn small ghost" data-default-action="' + action
|
|
421
463
|
+ '" data-kind="' + item.kind + '" data-name="' + esc(item.name) + '"'
|
|
422
464
|
+ (config.running ? ' disabled' : '') + '>' + (action === 'retry' ? '重试' : '确认应用') + '</button>' : '';
|
|
423
465
|
return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
|
|
424
|
-
+ esc(item.
|
|
425
|
-
+
|
|
426
|
-
+ (item.legacy ? '
|
|
427
|
-
+
|
|
466
|
+
+ esc(item.name) + ' <span class="tag">' + (item.kind === 'skill' ? '技能' : '插件') + '</span>'
|
|
467
|
+
+ statePill(item.state)
|
|
468
|
+
+ (item.legacy ? ' <span class="tag">旧版安装</span>' : '')
|
|
469
|
+
+ '</div><div class="skill-sub">'
|
|
470
|
+
+ (item.version ? esc(item.version) : '')
|
|
428
471
|
+ (item.target_version && item.target_version !== item.version ? ' → ' + esc(item.target_version) : '')
|
|
429
472
|
+ (item.error ? '<br><span style="color:var(--err)">' + esc(item.error) + '</span>' : '')
|
|
430
473
|
+ '</div></div>' + button + '</div>';
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: btn.dataset.kind,
|
|
437
|
-
name: btn.dataset.name, confirm: btn.dataset.defaultAction === 'apply' }) });
|
|
438
|
-
btn.onclick = () => btn.dataset.defaultAction === 'apply'
|
|
439
|
-
? confirmThen(btn, '替换当前版本?', request, '已开始应用,请查看同步状态')
|
|
440
|
-
: withLoading(btn, request, '已开始重试,请查看同步状态');
|
|
441
|
-
});
|
|
442
|
-
const repair = body.querySelector('#defaults-repair');
|
|
443
|
-
if (repair) repair.onclick = async () => {
|
|
474
|
+
};
|
|
475
|
+
const bindRepair = (el) => {
|
|
476
|
+
const repair = el.querySelector('#defaults-repair');
|
|
477
|
+
if (!repair) return;
|
|
478
|
+
repair.onclick = async () => {
|
|
444
479
|
if (repair.dataset.confirming !== '1') {
|
|
445
480
|
repair.dataset.confirming = '1';
|
|
446
481
|
repair.dataset.originalLabel = repair.textContent;
|
|
@@ -470,8 +505,31 @@ const renderDefaults = (s) => {
|
|
|
470
505
|
await refresh();
|
|
471
506
|
}
|
|
472
507
|
};
|
|
473
|
-
}
|
|
474
|
-
|
|
508
|
+
};
|
|
509
|
+
// 按类型各归其位:待处理插件进插件 Tab、待处理技能进技能 Tab,只列非已符合
|
|
510
|
+
// 项;5s 轮询高频调用,块内容没变时靠指纹跳过重绘(不打断进行中的点击)
|
|
511
|
+
const renderBlock = (id, list) => {
|
|
512
|
+
const el = $(id);
|
|
513
|
+
// 插件待处理项:已进插件列表的同名条目不在这里重复出现(合并进那一行)
|
|
514
|
+
const pending = (list || []).filter((item) => item.state !== 'current'
|
|
515
|
+
&& !(id === 'defaults-plugins' && listedPluginNames.has(item.name)));
|
|
516
|
+
const legacy = id === 'defaults-skills' && legacyCount
|
|
517
|
+
? '<div class="skill-sub"><button id="defaults-repair" class="btn small ghost"'
|
|
518
|
+
+ (config.running ? ' disabled' : '') + '>重新应用旧版安装的技能(' + legacyCount + ')</button>'
|
|
519
|
+
+ ' 技能目录会按平台当前版本覆盖</div>'
|
|
520
|
+
: '';
|
|
521
|
+
const html = (pending.length || legacy)
|
|
522
|
+
? '<div class="skill-sub" style="margin-top:10px">平台同步待处理</div>'
|
|
523
|
+
+ pending.map(rowHtml).join('') + legacy
|
|
524
|
+
: '';
|
|
525
|
+
if (el.dataset.fingerprint !== html) { el.dataset.fingerprint = html; el.innerHTML = html; bindDefaultsActions(el); bindRepair(el); }
|
|
526
|
+
};
|
|
527
|
+
renderBlock('defaults-plugins', config.plugins);
|
|
528
|
+
renderBlock('defaults-skills', config.skills);
|
|
529
|
+
// 待处理集合变化(新失败/恢复)→ 让已加载的插件列表行重新合并;
|
|
530
|
+
// renderPlugins 侧会先对齐 mergedPendingSig,不会互相触发死循环
|
|
531
|
+
const pendingSig = defaultsPendingSig();
|
|
532
|
+
if (renderPlugins._loaded && pendingSig !== mergedPendingSig) void renderPlugins();
|
|
475
533
|
};
|
|
476
534
|
const renderSkills = (s) => {
|
|
477
535
|
const skillhubEnabled = s.capabilities?.channels?.skillhub?.enabled !== false;
|
|
@@ -1044,8 +1102,14 @@ const renderPlugins = async () => {
|
|
|
1044
1102
|
}
|
|
1045
1103
|
// 渲染成功即标记已加载:避免切 tab 重复拉取(outdated 查询有网络开销)
|
|
1046
1104
|
renderPlugins._loaded = true;
|
|
1105
|
+
// 同名合并登记(见 renderDefaults 顶部说明):已列名的插件,其清单同步
|
|
1106
|
+
// 状态合并进本行;待处理块不再单列一条
|
|
1107
|
+
listedPluginNames = new Set(data.plugins.map((plugin) => plugin.name));
|
|
1108
|
+
const pendingByN = new Map(defaultsPendingPlugins.map((item) => [item.name, item]));
|
|
1047
1109
|
if (!data.plugins?.length) {
|
|
1048
1110
|
list.innerHTML = '<div class="skill-sub">暂无已安装插件;内置插件缺失时会在 dsh 启动时自动恢复。</div>';
|
|
1111
|
+
mergedPendingSig = defaultsPendingSig();
|
|
1112
|
+
if (lastStatus) renderDefaults(lastStatus);
|
|
1049
1113
|
return;
|
|
1050
1114
|
}
|
|
1051
1115
|
const rows = data.plugins.map((plugin) => {
|
|
@@ -1065,6 +1129,15 @@ const renderPlugins = async () => {
|
|
|
1065
1129
|
const builtinTag = plugin.builtin
|
|
1066
1130
|
? '<span class="tag">受管</span>'
|
|
1067
1131
|
: '';
|
|
1132
|
+
// 清单同步态合并:同名待处理插件在本行直接给出状态/原因/动作
|
|
1133
|
+
const pending = pendingByN.get(plugin.name);
|
|
1134
|
+
const syncPill = pending
|
|
1135
|
+
? (['failed', 'conflict'].includes(pending.state)
|
|
1136
|
+
? '<span class="pill" style="font-size:11px;background:var(--err-weak);color:var(--err)">同步'
|
|
1137
|
+
+ esc(DEFAULTS_STATE_LABELS[pending.state] || pending.state) + '</span>'
|
|
1138
|
+
: '<span class="pill warn" style="font-size:11px">同步'
|
|
1139
|
+
+ esc(DEFAULTS_STATE_LABELS[pending.state] || pending.state) + '</span>')
|
|
1140
|
+
: '';
|
|
1068
1141
|
const version = plugin.latest
|
|
1069
1142
|
? esc(plugin.version) + ' → <b>' + esc(plugin.latest) + '</b>'
|
|
1070
1143
|
: esc(plugin.version || '—');
|
|
@@ -1084,17 +1157,25 @@ const renderPlugins = async () => {
|
|
|
1084
1157
|
actions.push('<button class="btn small danger" data-remove="' + esc(plugin.name) + '">卸载</button>');
|
|
1085
1158
|
}
|
|
1086
1159
|
}
|
|
1160
|
+
if (pending && ['failed', 'report_pending', 'missing'].includes(pending.state)) {
|
|
1161
|
+
actions.push('<button class="btn small ghost" data-default-action="retry" data-kind="plugin" data-name="' + esc(plugin.name) + '">重试</button>');
|
|
1162
|
+
} else if (pending && ['conflict', 'pending'].includes(pending.state)) {
|
|
1163
|
+
actions.push('<button class="btn small ghost" data-default-action="apply" data-kind="plugin" data-name="' + esc(plugin.name) + '">确认应用</button>');
|
|
1164
|
+
}
|
|
1087
1165
|
const actionHtml = actions.length
|
|
1088
1166
|
? '<div style="display:flex;gap:6px">' + actions.join('') + '</div>'
|
|
1089
1167
|
: '';
|
|
1090
1168
|
return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
|
|
1091
|
-
+ esc(plugin.name) + builtinTag + statePill
|
|
1169
|
+
+ esc(plugin.name) + builtinTag + statePill + syncPill
|
|
1092
1170
|
+ '</div><div class="skill-sub">' + desc + esc(plugin.source) + ' · ' + version
|
|
1093
1171
|
+ (plugin.registered ? ' · 已启用,重启后生效' : plugin.bundled ? ' · 已安装,未启用当前 profile(「升级」或重装可启用)' : '')
|
|
1172
|
+
+ (pending ? ' · 平台同步 ' + esc(pending.version || '') + (pending.target_version && pending.target_version !== pending.version ? ' → ' + esc(pending.target_version) : '') : '')
|
|
1173
|
+
+ (pending?.error ? '<br><span style="color:var(--err)">' + esc(pending.error) + '</span>' : '')
|
|
1094
1174
|
+ '</div></div>' + actionHtml + '</div>';
|
|
1095
1175
|
});
|
|
1096
1176
|
// 指纹保护(与 renderSkills 同思路):内容没变不碰 innerHTML——升级后
|
|
1097
|
-
// 的定向刷新、切 tab
|
|
1177
|
+
// 的定向刷新、切 tab 重入都不会重建按钮节点(打断卸载二次确认)或闪动。
|
|
1178
|
+
// 清单同步态参与指纹:失败/恢复会让行内容变化
|
|
1098
1179
|
const fingerprint = data.plugins.map((plugin) => [
|
|
1099
1180
|
plugin.name,
|
|
1100
1181
|
plugin.version || '',
|
|
@@ -1102,6 +1183,10 @@ const renderPlugins = async () => {
|
|
|
1102
1183
|
plugin.registered ? 1 : 0,
|
|
1103
1184
|
plugin.bundled ? 1 : 0,
|
|
1104
1185
|
plugin.self_managed ? 1 : 0,
|
|
1186
|
+
pendingByN.get(plugin.name)
|
|
1187
|
+
? [pendingByN.get(plugin.name).state, pendingByN.get(plugin.name).target_version || '',
|
|
1188
|
+
pendingByN.get(plugin.name).error || ''].join('~')
|
|
1189
|
+
: '',
|
|
1105
1190
|
].join(':')).join('|');
|
|
1106
1191
|
if (list.dataset.fingerprint !== fingerprint) {
|
|
1107
1192
|
list.dataset.fingerprint = fingerprint;
|
|
@@ -1125,6 +1210,12 @@ const renderPlugins = async () => {
|
|
|
1125
1210
|
body: JSON.stringify({ name: btn.dataset.remove }),
|
|
1126
1211
|
}).then(() => renderPlugins()), '插件已卸载');
|
|
1127
1212
|
});
|
|
1213
|
+
// 合并进插件行的同步动作按钮(与待处理块同一契约)
|
|
1214
|
+
bindDefaultsActions(list);
|
|
1215
|
+
// 对齐「已合并的待处理集合」:renderDefaults 的变化触发到此为止(不会回环);
|
|
1216
|
+
// 列名登记可能让待处理块的排除集变化,定向重绘一次
|
|
1217
|
+
mergedPendingSig = defaultsPendingSig();
|
|
1218
|
+
if (lastStatus) renderDefaults(lastStatus);
|
|
1128
1219
|
};
|
|
1129
1220
|
// ==about-upgrade-begin==
|
|
1130
1221
|
// 关于页版本行:aboutSelf(GET api/plugin/self)有值时展示升级入口或
|
|
@@ -1403,7 +1494,7 @@ $('skillhub-sync-btn').onclick = () => withLoading(
|
|
|
1403
1494
|
() => fetch('api/skillhub/sync', { method: 'POST' }),
|
|
1404
1495
|
'SkillHub 配置已同步',
|
|
1405
1496
|
);
|
|
1406
|
-
//
|
|
1497
|
+
// 手动同步技能:文案按结果给(装了几个),失败由 withLoading 展示
|
|
1407
1498
|
$('skill-sync-btn').onclick = () => withLoading(
|
|
1408
1499
|
$('skill-sync-btn'),
|
|
1409
1500
|
async () => {
|
|
@@ -1514,9 +1605,10 @@ export function statusPageHtml() {
|
|
|
1514
1605
|
<div class="card" id="skills-card">
|
|
1515
1606
|
<h3>平台技能 <span class="muted">套餐自动同步</span></h3>
|
|
1516
1607
|
<div id="skills-list"></div>
|
|
1608
|
+
<div id="defaults-skills"></div>
|
|
1517
1609
|
<div class="hint" id="skills-meta"></div>
|
|
1518
1610
|
<div class="row" style="margin-top:8px">
|
|
1519
|
-
<button id="skill-sync-btn" class="btn ghost small" data-loading="同步中…"
|
|
1611
|
+
<button id="skill-sync-btn" class="btn ghost small" data-loading="同步中…">同步技能</button>
|
|
1520
1612
|
</div>
|
|
1521
1613
|
<div class="search" style="margin-top:12px">
|
|
1522
1614
|
<input id="search-input" placeholder="搜索技能市场,如:数据 / 报告 / lark" />
|
|
@@ -1532,11 +1624,11 @@ export function statusPageHtml() {
|
|
|
1532
1624
|
|
|
1533
1625
|
<div class="tabpane" id="pane-plugins" hidden>
|
|
1534
1626
|
<div class="card">
|
|
1535
|
-
<h3
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
<
|
|
1627
|
+
<h3>dsh 插件 <span class="muted">当前 profile 已安装的插件</span>
|
|
1628
|
+
<button id="defaults-sync-btn" class="btn ghost small" style="margin-left:auto" hidden>检查并更新</button>
|
|
1629
|
+
</h3>
|
|
1630
|
+
<div id="defaults-meta" aria-live="polite"></div>
|
|
1631
|
+
<div id="defaults-plugins"></div>
|
|
1540
1632
|
<div id="plugins-list"></div>
|
|
1541
1633
|
<div class="hint" id="plugins-hint">受管技能和插件始终更新到最新发布版本;仅管理员停用后可卸载受管插件。</div>
|
|
1542
1634
|
</div>
|
package/lib/proc.js
CHANGED
|
@@ -93,7 +93,19 @@ function extraBinDirs() {
|
|
|
93
93
|
// 无 nvm:跳过,走后面的通用目录
|
|
94
94
|
}
|
|
95
95
|
dirs.push(join(home, '.local/bin'), '/opt/homebrew/bin', '/usr/local/bin');
|
|
96
|
+
// pnpm 独立安装与 pnpm -g 全局 bin 的默认落点(darwin PNPM_HOME=
|
|
97
|
+
// ~/Library/pnpm,兼容 bin 子目录布局;Linux 默认 ~/.local/share/pnpm):
|
|
98
|
+
// GUI 贫瘠 PATH 下 pnpm 装完 lark-cli 要能立即探测到
|
|
99
|
+
dirs.push(
|
|
100
|
+
join(home, 'Library', 'pnpm'),
|
|
101
|
+
join(home, 'Library', 'pnpm', 'bin'),
|
|
102
|
+
join(home, '.local', 'share', 'pnpm'),
|
|
103
|
+
);
|
|
96
104
|
}
|
|
105
|
+
// 桌面壳自带真实 Node 二进制的 bin 目录(DeepSeek Harness runtime):
|
|
106
|
+
// 无 nvm/brew/node 的全新机器上 node 由宿主兜底(排在真工具链之后)
|
|
107
|
+
const bundled = desktopBundledRuntime();
|
|
108
|
+
if (bundled?.node) dirs.push(dirname(bundled.node));
|
|
97
109
|
// DSH Desktop shim 目录固定排在兑底列表末尾:这些 shim 是 Electron 当
|
|
98
110
|
// node 用的包装(见 desktopShimDirs),真工具链存在时应让位。
|
|
99
111
|
dirs.push(...desktopShimDirs());
|
|
@@ -106,37 +118,51 @@ function extraBinDirs() {
|
|
|
106
118
|
* node 环境但 lark-cli 安装仍报「未找到 npm」。macOS 位
|
|
107
119
|
* ~/Library/Application Support/DSH Desktop/cli/<profile hash>/bin;
|
|
108
120
|
* Windows 实测在 Roaming 下(真实用户机报错的命令路径前缀就是
|
|
109
|
-
* C:\Users\<user>\AppData\Roaming\DSH…),Local
|
|
110
|
-
*
|
|
111
|
-
*
|
|
121
|
+
* C:\Users\<user>\AppData\Roaming\DSH…),Local 也一并扫。两代品牌的
|
|
122
|
+
* userData 目录都扫(老 DSH Desktop 与新 DeepSeek Harness 的
|
|
123
|
+
* @deepseek-ai/dsh-desktop)。两种布局都兼容:shims/ 直挂、cli/<hash>/bin。
|
|
124
|
+
* 独立成函数并单独缓存:extraBinDirs 引用之外,execResolved 还要用它把
|
|
125
|
+
* 这些 shim 目录排到子进程 PATH 末尾。 */
|
|
112
126
|
let desktopShimDirsCache = null;
|
|
113
|
-
export function desktopShimDirs() {
|
|
114
|
-
|
|
127
|
+
export function desktopShimDirs(options) {
|
|
128
|
+
// 传 options 的是纯探测调用(测试注入假环境),不进进程缓存
|
|
129
|
+
const injected = options !== undefined && options !== null;
|
|
130
|
+
if (!injected && desktopShimDirsCache !== null) return desktopShimDirsCache;
|
|
131
|
+
const {
|
|
132
|
+
home = homedir(),
|
|
133
|
+
platform = process.platform,
|
|
134
|
+
env = process.env,
|
|
135
|
+
exists = existsSync,
|
|
136
|
+
readdir = readdirSync,
|
|
137
|
+
} = options || {};
|
|
115
138
|
const dirs = [];
|
|
116
|
-
const
|
|
117
|
-
const
|
|
139
|
+
const roaming = env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
140
|
+
const local = env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
141
|
+
const desktopRoots = platform === 'win32'
|
|
118
142
|
? [
|
|
119
|
-
join(
|
|
120
|
-
join(
|
|
143
|
+
join(roaming, 'DSH Desktop'),
|
|
144
|
+
join(local, 'DSH Desktop'),
|
|
145
|
+
join(roaming, '@deepseek-ai', 'dsh-desktop'),
|
|
146
|
+
join(local, '@deepseek-ai', 'dsh-desktop'),
|
|
121
147
|
]
|
|
122
|
-
: [
|
|
148
|
+
: [
|
|
149
|
+
join(home, 'Library', 'Application Support', 'DSH Desktop'),
|
|
150
|
+
join(home, 'Library', 'Application Support', '@deepseek-ai', 'dsh-desktop'),
|
|
151
|
+
];
|
|
152
|
+
const hasNodeShim = (dir) => exists(join(dir, 'node')) || exists(join(dir, 'node.exe')) || exists(join(dir, 'node.cmd'));
|
|
123
153
|
for (const root of desktopRoots) {
|
|
124
154
|
try {
|
|
125
155
|
const shimDir = join(root, 'shims');
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
for (const entry of readdirSync(join(root, 'cli'))) {
|
|
156
|
+
if (hasNodeShim(shimDir)) dirs.push(shimDir);
|
|
157
|
+
for (const entry of readdir(join(root, 'cli'))) {
|
|
130
158
|
const shimBin = join(root, 'cli', entry, 'bin');
|
|
131
|
-
if (
|
|
132
|
-
dirs.push(shimBin);
|
|
133
|
-
}
|
|
159
|
+
if (hasNodeShim(shimBin)) dirs.push(shimBin);
|
|
134
160
|
}
|
|
135
161
|
} catch {
|
|
136
162
|
// 目录不存在:跳过
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
|
-
desktopShimDirsCache = dirs;
|
|
165
|
+
if (!injected) desktopShimDirsCache = dirs;
|
|
140
166
|
return dirs;
|
|
141
167
|
}
|
|
142
168
|
|
|
@@ -171,6 +197,110 @@ export function desktopShimEnvDefaults({
|
|
|
171
197
|
return defaults;
|
|
172
198
|
}
|
|
173
199
|
|
|
200
|
+
/** DeepSeek Harness / DSH Desktop 桌面壳自带的 Node + pnpm 工具链。官方应用
|
|
201
|
+
* 在 <安装目录>/Contents/Resources(win32: <安装目录>/resources)下发布:
|
|
202
|
+
* runtime/primary-runtime/dependencies/node/bin/node 真实 Node 二进制
|
|
203
|
+
* runtime/pnpm/bin/pnpm.mjs pnpm 11
|
|
204
|
+
* 宿主把它交给 dsh 核心(profileContext.packageManager)做装包,但插件宿主
|
|
205
|
+
* 进程的 PATH 里没有——全新 macOS(无 nvm/brew/node,只装了桌面应用)上点
|
|
206
|
+
* 「安装」lark-cli 只能得到「请先安装 Node.js」(真实用户机实测)。这里从
|
|
207
|
+
* execPath/resourcesPath/argv 反推同一套工具链:纯文件系统判定,不依赖宿主
|
|
208
|
+
* 注入任何变量。传 options 的是纯探测调用(测试注入假环境),不进缓存。 */
|
|
209
|
+
let desktopBundledRuntimeCache;
|
|
210
|
+
export function desktopBundledRuntime(options) {
|
|
211
|
+
const injected = options !== undefined && options !== null;
|
|
212
|
+
if (!injected && desktopBundledRuntimeCache !== undefined) return desktopBundledRuntimeCache;
|
|
213
|
+
const {
|
|
214
|
+
execPath = process.execPath,
|
|
215
|
+
argv = process.argv,
|
|
216
|
+
platform = process.platform,
|
|
217
|
+
resourcesPath = process.resourcesPath,
|
|
218
|
+
exists = existsSync,
|
|
219
|
+
} = options || {};
|
|
220
|
+
const nodeName = platform === 'win32' ? 'node.exe' : 'node';
|
|
221
|
+
const roots = [];
|
|
222
|
+
const push = (value) => {
|
|
223
|
+
if (typeof value === 'string' && value && !roots.includes(value)) roots.push(value);
|
|
224
|
+
};
|
|
225
|
+
push(resourcesPath);
|
|
226
|
+
if (typeof execPath === 'string' && execPath) {
|
|
227
|
+
// 形态一:Electron 壳可执行文件(darwin <App>/Contents/MacOS/<exe>)
|
|
228
|
+
push(join(dirname(execPath), '..', 'Resources'));
|
|
229
|
+
push(join(dirname(execPath), 'resources'));
|
|
230
|
+
// 形态二:真实 node(<Res>/runtime/primary-runtime/dependencies/node/bin)
|
|
231
|
+
let dir = dirname(execPath);
|
|
232
|
+
for (let i = 0; i < 6 && dir && dir !== dirname(dir); i += 1) {
|
|
233
|
+
push(join(dir, 'Resources'));
|
|
234
|
+
push(join(dir, 'resources'));
|
|
235
|
+
dir = dirname(dir);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 形态三:宿主 argv 里直接带 runtime 布局的路径(桌面壳把 pnpm 入口与
|
|
239
|
+
// node 启动目录作为启动参数传给宿主进程,布局演进时按路径特征反推)
|
|
240
|
+
for (const item of Array.isArray(argv) ? argv : []) {
|
|
241
|
+
const value = String(item || '').replace(/\\/g, '/');
|
|
242
|
+
const pnpmMatch = value.match(/^(.+?)\/runtime\/pnpm\/bin\/pnpm\.(?:mjs|cjs)$/);
|
|
243
|
+
if (pnpmMatch) push(pnpmMatch[1]);
|
|
244
|
+
const runtimeMatch = value.match(/^(.+?)\/runtime\/(?:bin|primary-runtime)$/);
|
|
245
|
+
if (runtimeMatch) push(runtimeMatch[1]);
|
|
246
|
+
}
|
|
247
|
+
const finish = (runtime) => {
|
|
248
|
+
if (!injected) desktopBundledRuntimeCache = runtime;
|
|
249
|
+
return runtime;
|
|
250
|
+
};
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
for (const root of roots) {
|
|
253
|
+
if (!root || seen.has(root)) continue;
|
|
254
|
+
seen.add(root);
|
|
255
|
+
const pnpm = [
|
|
256
|
+
join(root, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'),
|
|
257
|
+
join(root, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'),
|
|
258
|
+
join(root, 'app', 'node_modules', 'pnpm', 'bin', 'pnpm.mjs'),
|
|
259
|
+
].find((entry) => exists(entry));
|
|
260
|
+
if (!pnpm) continue;
|
|
261
|
+
const node = [
|
|
262
|
+
join(root, 'runtime', 'primary-runtime', 'dependencies', 'node', 'bin', nodeName),
|
|
263
|
+
join(root, 'runtime', 'bin', nodeName),
|
|
264
|
+
].find((entry) => exists(entry)) || null;
|
|
265
|
+
return finish({ resources: root, node, pnpm });
|
|
266
|
+
}
|
|
267
|
+
return finish(null);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 兜底命令映射:PATH 与常规兑底目录都没有时,把 pnpm/node 指到桌面壳自带
|
|
271
|
+
* 工具链(pnpm 是 .mjs 脚本入口,由 scriptInvocation 用自带 node 执行)。
|
|
272
|
+
* 不映射 dsh——桌面壳内的 dsh CLI 明确拒绝操作 desktop profile(见
|
|
273
|
+
* desktop.js),插件管理走 pluginManager 服务,不缺这个兜底。 */
|
|
274
|
+
export function bundledCommandFor(tool, runtime = desktopBundledRuntime()) {
|
|
275
|
+
if (!runtime) return null;
|
|
276
|
+
if (tool === 'pnpm') return runtime.pnpm;
|
|
277
|
+
if (tool === 'node') return runtime.node;
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** 脚本入口的执行形态(纯函数,供测试):resolveCommandOrNull 会把 pnpm 指到
|
|
282
|
+
* 桌面壳自带的 pnpm.mjs——它不是可执行文件,必须经解释器运行。优先自带
|
|
283
|
+
* 真实 node;没有时退化 Electron-as-node(插件宿主本身就是这个形态)。
|
|
284
|
+
* 返回 null 表示 resolved 可直接执行。 */
|
|
285
|
+
export function scriptInvocation(resolved, args, {
|
|
286
|
+
runtime = desktopBundledRuntime(),
|
|
287
|
+
electron = process.versions?.electron,
|
|
288
|
+
execPath = process.execPath,
|
|
289
|
+
} = {}) {
|
|
290
|
+
if (!/\.(?:mjs|cjs)$/i.test(String(resolved || ''))) return null;
|
|
291
|
+
if (runtime?.node) {
|
|
292
|
+
// Windows 的 node 启动器可能是 .cmd shim(runtime/bin/node.cmd 要求
|
|
293
|
+
// DSH_DESKTOP_NODE_EXECUTABLE 指向真实可执行文件,与
|
|
294
|
+
// desktopShimEnvDefaults 同一约定);真实 node.exe 则无需任何变量
|
|
295
|
+
const env = /\.cmd$/i.test(runtime.node) ? { DSH_DESKTOP_NODE_EXECUTABLE: execPath } : {};
|
|
296
|
+
return { file: runtime.node, args: [resolved, ...args], env };
|
|
297
|
+
}
|
|
298
|
+
if (electron) {
|
|
299
|
+
return { file: execPath, args: [resolved, ...args], env: { ELECTRON_RUN_AS_NODE: '1' } };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
174
304
|
/** 组装子进程 PATH(纯函数,供测试):非 shim 兑底目录排最前(GUI 贫瘠
|
|
175
305
|
* PATH 也能解析到真工具),原有 PATH 居中,DSH Desktop shim 目录垫底——
|
|
176
306
|
* shim 是 Electron 包装,真 node/pnpm 存在时优先用真的;贫瘠机器上原
|
|
@@ -188,6 +318,7 @@ export function childPathWithExtras(originalPath, extras, shimDirs) {
|
|
|
188
318
|
export function resetExtraBinDirsCache() {
|
|
189
319
|
extraBinDirsCache = null;
|
|
190
320
|
desktopShimDirsCache = null;
|
|
321
|
+
desktopBundledRuntimeCache = undefined;
|
|
191
322
|
preferredCommandCache.clear();
|
|
192
323
|
}
|
|
193
324
|
|
|
@@ -229,18 +360,31 @@ export function pathEnvKey(env, platform) {
|
|
|
229
360
|
|
|
230
361
|
/** 命令解析:PATH 里找不到时从兑底目录解析绝对路径(找不到返回 null)。
|
|
231
362
|
* 所有 execFile 调用统一过这里;返回 null 时调用方走「未找到命令」的
|
|
232
|
-
* 中文指引分支。绝对路径原样返回(ccpg
|
|
363
|
+
* 中文指引分支。绝对路径原样返回(ccpg 安装器等)。优先级:PATH 与常规
|
|
364
|
+
* 兑底目录 → 桌面壳自带工具链(pnpm/node)→ 桌面 shim 目录垫底——shim
|
|
365
|
+
* 是 Electron 包装且可能随应用改名而指向已卸载的旧路径(真实用户机实测
|
|
366
|
+
* DSH Desktop → DeepSeek Harness 后残留指向已删除的 /Applications/DSH
|
|
367
|
+
* Desktop.app),真工具链存在时永远让位。 */
|
|
233
368
|
export function resolveCommandOrNull(tool) {
|
|
234
369
|
if (isAbsolute(tool)) return existsSync(tool) ? tool : null;
|
|
235
|
-
const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
|
|
236
370
|
const suffixes = candidateSuffixes(process.platform);
|
|
237
|
-
|
|
238
|
-
for (const
|
|
239
|
-
|
|
240
|
-
|
|
371
|
+
const lookup = (dirs) => {
|
|
372
|
+
for (const dir of dirs) {
|
|
373
|
+
for (const suffix of suffixes) {
|
|
374
|
+
const candidate = join(dir, tool + suffix);
|
|
375
|
+
if (existsSync(candidate)) return candidate;
|
|
376
|
+
}
|
|
241
377
|
}
|
|
242
|
-
|
|
243
|
-
|
|
378
|
+
return null;
|
|
379
|
+
};
|
|
380
|
+
const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
|
|
381
|
+
const shimDirs = desktopShimDirs();
|
|
382
|
+
const shimSet = new Set(shimDirs);
|
|
383
|
+
const found = lookup([...pathDirs, ...extraBinDirs().filter((dir) => !shimSet.has(dir))]);
|
|
384
|
+
if (found) return found;
|
|
385
|
+
const bundled = bundledCommandFor(tool);
|
|
386
|
+
if (bundled && existsSync(bundled)) return bundled;
|
|
387
|
+
return lookup(shimDirs);
|
|
244
388
|
}
|
|
245
389
|
|
|
246
390
|
/** 与 resolveCommandOrNull 同口径,但返回全部按序去重候选。python 这类
|
|
@@ -310,9 +454,13 @@ function composeChildEnv(extraEnv) {
|
|
|
310
454
|
}
|
|
311
455
|
|
|
312
456
|
function execResolvedPath(resolved, args, { timeoutMs, cwd, env, signal } = {}) {
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
457
|
+
// 脚本入口(桌面壳自带 pnpm.mjs)换成解释器执行形态
|
|
458
|
+
const script = scriptInvocation(resolved, args);
|
|
459
|
+
const file = script ? script.file : resolved;
|
|
460
|
+
const argv = script ? script.args : args;
|
|
461
|
+
const childEnv = composeChildEnv({ ...(script?.env || {}), ...(env || {}) });
|
|
462
|
+
const shim = cmdShimInvocation(process.platform, file, argv);
|
|
463
|
+
return execFileAsync(shim ? shim.file : file, shim ? shim.args : argv, {
|
|
316
464
|
timeout: timeoutMs,
|
|
317
465
|
signal,
|
|
318
466
|
killSignal: 'SIGKILL',
|
|
@@ -447,9 +595,15 @@ export async function runCliTool(tool, args, { timeoutMs = 300000, cwd, env, sig
|
|
|
447
595
|
/** 工具缺失时的中文指引:不同工具有不同的安装出路。 */
|
|
448
596
|
function missingCommandGuidance(tool) {
|
|
449
597
|
if (tool === 'npm' || tool === 'npx') {
|
|
598
|
+
// 触达这里的机器连桌面壳自带工具链都没有(scriptInvocation/兜底映射
|
|
599
|
+
// 均未命中)——重启通常能恢复探测,再给手动安装出路。社区桌面端
|
|
600
|
+
// (DSH Desktop/NEXT)与官方壳分开点名,避免指错重启对象
|
|
601
|
+
const brand = /DSH (?:Desktop|NEXT)\.(?:app|exe)/i.test(process.execPath)
|
|
602
|
+
? 'DSH Desktop'
|
|
603
|
+
: 'DeepSeek Harness';
|
|
450
604
|
return process.platform === 'win32'
|
|
451
|
-
?
|
|
452
|
-
:
|
|
605
|
+
? `未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,请先从 https://nodejs.org 下载安装 Node.js(LTS 版,安装时保持默认选项),安装完成后重启 ${brand} 再点「安装」`
|
|
606
|
+
: `未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,且未探测到 ${brand} 自带的 Node 运行时。请重启 ${brand} 后重试;仍失败时安装 Node.js(推荐 nvm:nvm install --lts)后再点「安装」`;
|
|
453
607
|
}
|
|
454
608
|
if (tool === 'pnpm') {
|
|
455
609
|
return '未找到 pnpm 命令,无法查询插件最新版本。可安装 pnpm(npm install -g pnpm)后重启 DSH Desktop;不影响插件正常使用,仅影响升级提示。';
|
|
@@ -983,10 +1137,27 @@ function rememberGlobalBinDir(dir) {
|
|
|
983
1137
|
extraBinDirsCache = [value, ...existing.filter((item) => item.toLowerCase() !== value.toLowerCase())];
|
|
984
1138
|
}
|
|
985
1139
|
|
|
1140
|
+
/** pnpm 全局 home(PNPM_HOME)的平台默认值。老实现只区分 win32 与「其余
|
|
1141
|
+
* 平台」——macOS/Linux 上退化成 Windows 目录(~/AppData/Local/pnpm 在
|
|
1142
|
+
* macOS 是无效路径,装出来的 lark-cli 谁也找不到)。pnpm 官方默认:
|
|
1143
|
+
* win32 %LOCALAPPDATA%\pnpm、darwin ~/Library/pnpm、
|
|
1144
|
+
* linux ~/.local/share/pnpm;显式 PNPM_HOME 永远优先。 */
|
|
1145
|
+
export function pnpmHomeDefault({ platform = process.platform, env = process.env, home = homedir() } = {}) {
|
|
1146
|
+
const configured = String(env.PNPM_HOME || '').trim();
|
|
1147
|
+
if (configured) return configured;
|
|
1148
|
+
if (platform === 'win32') {
|
|
1149
|
+
return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'pnpm');
|
|
1150
|
+
}
|
|
1151
|
+
if (platform === 'darwin') return join(home, 'Library', 'pnpm');
|
|
1152
|
+
return join(home, '.local', 'share', 'pnpm');
|
|
1153
|
+
}
|
|
1154
|
+
|
|
986
1155
|
async function installLarkCliWithPnpm(timeoutMs) {
|
|
987
|
-
const pnpmHome =
|
|
988
|
-
|| join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'pnpm');
|
|
1156
|
+
const pnpmHome = pnpmHomeDefault();
|
|
989
1157
|
const pnpmEnv = { PNPM_HOME: pnpmHome };
|
|
1158
|
+
// pnpm -g 前置校验要求全局 bin 目录在 PATH 中:先记住再装,避免第一次
|
|
1159
|
+
// 必失败(错误输出折行解析的重试兜底保留,覆盖自定义安装位)
|
|
1160
|
+
rememberGlobalBinDir(pnpmHome);
|
|
990
1161
|
const pnpmInstall = () => runCliTool(
|
|
991
1162
|
'pnpm',
|
|
992
1163
|
['add', '-g', '@larksuite/cli@latest'],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;平台清单融入技能/插件双 Tab,无 Node 机器用桌面壳自带工具链装 lark-cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"smoke": "node scripts/env-matrix-smoke.mjs"
|