dsh-agentone 0.5.40 → 0.5.42
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 +59 -3
- package/lib/defaults-sync.js +12 -2
- package/lib/desktop.js +42 -0
- package/lib/index.js +50 -13
- package/lib/page.js +60 -11
- package/lib/profile-plugins.js +90 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,9 +35,17 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
35
35
|
不删除本机内容。平台暂时不可用时缓存仅展示,不用于安装。
|
|
36
36
|
- **dsh 插件管理(M3+)**:列出当前 profile 的包、版本、bundle 注册状态;
|
|
37
37
|
安装成功需物理版本、插件声明和 profile 注册同时成立。注册不证明运行时加载,
|
|
38
|
-
页面提示重启生效。Web 必须配置实际 `profileName
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
页面提示重启生效。Web 必须配置实际 `profileName`;桌面壳按宿主契约自动取
|
|
39
|
+
当前 profile——第三方壳(desktopProfiles + 受管 desktopPnpm 通道)与官方
|
|
40
|
+
DeepSeek Harness 应用(launcher 的 profileContext + 宿主 pluginManager,
|
|
41
|
+
与应用内「设置 → 插件」同一管线,不接受 pnpm 选项)分别适配;安装/卸载
|
|
42
|
+
串行排队、支持生命周期取消。自身与 DSH 基础组件不属于平台默认清单,
|
|
43
|
+
自身只在独立安装模式允许手动升级。
|
|
44
|
+
- **旧版技能记录迁移(0.5.41)**:0.5.39 起本机安装真源是
|
|
45
|
+
`$DSH_HOME/agentone/skill-state/<slug>.json`;≤0.5.38 只写平台台账,升级后
|
|
46
|
+
这些技能会「有目录、无记录」。这类条目标注「旧版安装」,插件页提供
|
|
47
|
+
「重新应用旧版安装的技能」批量入口(按平台版本原子覆盖目录并重建记录);
|
|
48
|
+
有本机记录但内容被改过的真冲突仍逐项人工确认,不自动覆盖。
|
|
41
49
|
- **CLI 探测、安装与授权(M4)**:`lark-cli` / `ccpg-cli` 安装与授权态三态探测;
|
|
42
50
|
未安装时「安装」一键自助安装(lark 走 `npm install -g @larksuite/cli@latest`,
|
|
43
51
|
ccpg 走公司 `auto-install.sh`,口径与平台 cli-deployd 一致);
|
|
@@ -123,17 +131,52 @@ dsh plugin --profile web add dsh-agentone
|
|
|
123
131
|
lib/
|
|
124
132
|
index.js 路由与业务编排(登录/状态/技能/套餐/插件/CLI)
|
|
125
133
|
page.js 状态页与授权完成页的 HTML/CSS/JS 模板(纯静态)
|
|
134
|
+
desktop.js 桌面壳探测与 profile 解析(全仓唯一一份,见下节)
|
|
126
135
|
http.js 平台请求统一通道(超时、错误码透传)
|
|
127
136
|
auth.js 登录流、令牌轮换/吊销、能力面/套餐 API
|
|
128
137
|
language.js 全局中文回复默认($DSH_HOME/AGENTS.md 受标记管理注入)
|
|
129
138
|
model.js 平台模型 → settings.yaml / .credentials.yaml
|
|
130
139
|
skill.js 技能下载/解包/卸载(zip 穿越清洗、原子落盘)
|
|
140
|
+
skill-state.js 技能本机记录($DSH_HOME/agentone/skill-state/<slug>.json)与状态判定
|
|
141
|
+
defaults-sync.js 平台默认能力协调器(清单拉取/对齐/缓存)
|
|
142
|
+
profile-plugins.js 当前 profile 的插件操作(通道选择见下节)
|
|
131
143
|
store.js 凭据持久化(0600;损坏自动移开)
|
|
132
144
|
proc.js dsh/pnpm CLI 转发与 lark-cli/ccpg-cli 探测
|
|
133
145
|
qrcode.mjs 授权 URL 的二维码 SVG(vendor qrcode-generator,MIT;lark/ccpg 通用)
|
|
134
146
|
client.js dsh 浏览器端 bundle(设置菜单 iframe + 首启引导 + 设置导航 workbench 图标换肤)
|
|
135
147
|
```
|
|
136
148
|
|
|
149
|
+
## 桌面宿主契约(两类壳,判定只有一份)
|
|
150
|
+
|
|
151
|
+
宿主服务面差异是 0.5.39/0.5.40 两次事故的根因类别(品牌正则抄了两份漏改一份;
|
|
152
|
+
只接了一类壳的契约)。**品牌判定与 profile 解析只允许 `lib/desktop.js` 一份实现**,
|
|
153
|
+
`test/desktop-srcdoc.test.mjs` 用源码扫描守卫这一点。
|
|
154
|
+
|
|
155
|
+
| | 第三方壳(dsh-plugin-desktop) | 官方 Electron 应用(DeepSeek Harness / DSH NEXT) |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| profile 来源 | `ctx.get('desktopProfiles').current`(name + dir) | `ctx.get('profileContext')`(name/dir/packageManager,launcher 发布) |
|
|
158
|
+
| 安装通道 | `ctx.get('desktopPnpm').runPlugin(argv, dir, signal)` → `{stdout, stderr, done, cancel}` | `ctx.get('pluginManager').installBundle(spec, {requestId})` / `removeBundle(name)` / `cancelInstall(requestId)` → `{application: applied\|restart-required\|overridden\|cancelled\|failed, packageResult, error}` |
|
|
159
|
+
| 不发布 | `pluginManager` | `desktopProfiles`、`desktopPnpm`(app.asar 均 0 命中,实测 2026-10) |
|
|
160
|
+
| CLI 兜底 | dsh CLI 拒绝 `--profile desktop` | 同左;profileContext 是唯一答案 |
|
|
161
|
+
|
|
162
|
+
要点:
|
|
163
|
+
|
|
164
|
+
- 官方 `pluginManager` 与应用内「设置 → 插件」同一管线,**不接受 pnpm 选项**;
|
|
165
|
+
`--allow-build` 不转发(构建审批由官方管线自理),其余附加参数必须拒绝而不是
|
|
166
|
+
静默丢弃(`profile-plugins.js` 的 `desktopManagerRun`)。
|
|
167
|
+
- `application: 'overridden'` 是「改动保留、被别的层覆盖显示」,不是失败。
|
|
168
|
+
- 非桌面宿主(`dsh web`/CLI):插件配置 `profileName` 优先,launcher 的
|
|
169
|
+
`profileContext` 次之(`desktop.js` 的 `resolveProfileTarget`)。
|
|
170
|
+
- **installed ≠ enabled**(读官方 `dsh-plugin-manager` 源码确认):
|
|
171
|
+
`dsh.profile.bundles` 是**启用集**,`node_modules` 里的依赖只是「已安装」。
|
|
172
|
+
官方「已安装列表」= 启用集 ∪ profile 依赖 ∪ 应用自带依赖
|
|
173
|
+
(`installation.dependencies`——rc.2 那批 browser-use/computer-use 由应用
|
|
174
|
+
供给、不可卸载),因此会出现「已安装但未启用」条目;`installBundle` 装新包时
|
|
175
|
+
reconcile 会**自动启用**带 `dsh.bundle` 声明的新依赖,但不会悄悄启用存量
|
|
176
|
+
未启用包。前端术语用「已启用/未启用」,别叫「已注册」——和官方 UI 对不上。
|
|
177
|
+
peer 不兼容的启用会被宿主拒绝(结构化错误 `incompatible-version`),需在
|
|
178
|
+
应用内授权该精确版本(version exemptions)或等插件发兼容版本。
|
|
179
|
+
|
|
137
180
|
## 开发
|
|
138
181
|
|
|
139
182
|
```bash
|
|
@@ -144,3 +187,16 @@ dsh plugin --profile web add /path/to/plugins/dsh/dsh-agentone
|
|
|
144
187
|
无构建步骤(纯 ESM JS);运行时 peer 依赖 `@deepseek-ai/cordis` /
|
|
145
188
|
`@deepseek-ai/schemastery` 由 dsh host 提供。测试:`npm install && node --test`
|
|
146
189
|
(YAML 合并/登出清理语义、路由冒烟、凭据容错、令牌轮换等)。
|
|
190
|
+
|
|
191
|
+
发版前真实宿主门禁(单测全绿但宿主内翻车的两类事故都由它拦):
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# 1) 把候选版本装进 desktop profile(用应用自带 pnpm,与应用内安装同源)
|
|
195
|
+
# RT="/Applications/DeepSeek Harness.app/Contents/Resources/runtime"
|
|
196
|
+
# cd ~/.dsh/profiles/desktop && \
|
|
197
|
+
# DSH_DESKTOP_NODE_EXECUTABLE="/Applications/DeepSeek Harness.app/Contents/MacOS/DeepSeek Harness" \
|
|
198
|
+
# "$RT/bin/node" "$RT/pnpm/bin/pnpm.mjs" add 'dsh-agentone@<版本>'
|
|
199
|
+
# 2) 完全退出并重开 DeepSeek Harness(插件代码 restart-required)
|
|
200
|
+
# 3) 跑门禁:逐条断言本次事故签名(旧代码未重启/profile 解析/插件清单/技能搁浅)
|
|
201
|
+
node scripts/desktop-host-smoke.mjs
|
|
202
|
+
```
|
package/lib/defaults-sync.js
CHANGED
|
@@ -176,8 +176,18 @@ export function createDefaultsSync({ config, profiles, credentials, log, interva
|
|
|
176
176
|
try {
|
|
177
177
|
const local = await localSkillState(owner.platform, item.canonical_slug, options);
|
|
178
178
|
if (!active(currentGeneration)) return;
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
// 0.5.39 起「本机记录文件」才是安装真源;更早版本(≤0.5.38)只写
|
|
180
|
+
// 平台台账,升级后这些目录必然「有目录、无记录」——和「用户改过
|
|
181
|
+
// 内容」撞在同一个 conflict 状态里,必须区分开,否则升级当天全部
|
|
182
|
+
// 技能被误报成来源不一致/未安装(实测 11/11 命中)。
|
|
183
|
+
const legacy = local.state === 'conflict' && !local.record && Boolean(item.installed_fingerprint);
|
|
184
|
+
Object.assign(entry, { state: local.state, version: local.record?.version || null, legacy });
|
|
185
|
+
if (local.state === 'conflict' && (options.missingOnly || !selectedItem)) {
|
|
186
|
+
entry.error = legacy
|
|
187
|
+
? '旧版本安装的技能缺少本机记录,请点「重新应用」重建(按平台版本覆盖技能目录)'
|
|
188
|
+
: '本机技能已有用户内容或来源不一致,请确认后手动应用';
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
181
191
|
if (local.state === 'current' && (!item.latest_version || !item.latest_fingerprint) && options.missingOnly) {
|
|
182
192
|
entry.state = 'failed'; entry.error = '暂时无法确认技能最新版本,请稍后重试'; continue;
|
|
183
193
|
}
|
package/lib/desktop.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// 桌面壳探测与 profile 解析:判定只允许一份实现。
|
|
2
|
+
//
|
|
3
|
+
// 两种桌面契约(实测当前官方应用只满足后者):
|
|
4
|
+
// 1. 第三方壳(dsh-plugin-desktop):发布 `desktopProfiles.current`
|
|
5
|
+
// (name + dir),装包走 `desktopPnpm`;
|
|
6
|
+
// 2. 官方 DeepSeek Harness Electron 应用:**不发布** desktopProfiles /
|
|
7
|
+
// desktopPnpm(app.asar 里两个名字均 0 命中),由 launcher 发布
|
|
8
|
+
// `profileContext`(name/dir/packageManager),装包走 `pluginManager`。
|
|
9
|
+
// 该壳里 dsh CLI 明确拒绝 `--profile desktop`,profileContext 是唯一答案。
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/** 桌面壳品牌:DSH Desktop(第三方壳)/ DSH NEXT、DeepSeek HARNESS
|
|
13
|
+
* (NEXT 及其公开发布名)。宿主进程是 Electron 时 execPath 指向壳可执行文件。 */
|
|
14
|
+
export function isDesktopHost() {
|
|
15
|
+
return /(?:DSH Desktop|DSH NEXT|DeepSeek HARNESS)\.(?:app|exe)/i.test(process.execPath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validProfileName(name) {
|
|
19
|
+
return typeof name === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/.test(name) && name !== 'node_modules';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 解析当前 profile。桌面宿主以壳/launcher 的答案为准(桌面壳里 dsh CLI 拒绝
|
|
24
|
+
* 操作 desktop profile,猜出来的目录一定对不上);普通 Web/CLI 宿主仍以插件
|
|
25
|
+
* 配置的 profileName 优先,其次 launcher 的 profileContext。
|
|
26
|
+
* 目录优先取 launcher 自己给的 dir——profile 落在哪里由 launcher 决定。
|
|
27
|
+
* @returns {{home: string, name: string, dir: string, desktop: boolean}|null}
|
|
28
|
+
*/
|
|
29
|
+
export function resolveProfileTarget({ ctx, config = {}, home, desktop }) {
|
|
30
|
+
const shell = ctx?.get?.('desktopProfiles');
|
|
31
|
+
const launched = ctx?.get?.('profileContext');
|
|
32
|
+
const candidates = desktop
|
|
33
|
+
? [shell?.current?.name, launched?.name, config.profileName]
|
|
34
|
+
: [config.profileName, launched?.name, shell?.current?.name];
|
|
35
|
+
const name = candidates.find(validProfileName);
|
|
36
|
+
if (!name) return null;
|
|
37
|
+
const owned = [
|
|
38
|
+
[shell?.current?.name, shell?.current?.dir],
|
|
39
|
+
[launched?.name, launched?.dir],
|
|
40
|
+
].find(([owner, dir]) => owner === name && typeof dir === 'string' && dir.trim() !== '');
|
|
41
|
+
return { home, name, dir: owned ? owned[1].trim() : join(home, 'profiles', name), desktop };
|
|
42
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { applyPlatformModel, removePlatformModel } from './model.js';
|
|
|
34
34
|
import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
|
|
35
35
|
import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
|
|
36
36
|
import { createProfilePlugins, validatePackage, PROTECTED_PACKAGES } from './profile-plugins.js';
|
|
37
|
+
import { isDesktopHost } from './desktop.js';
|
|
37
38
|
import { createDefaultsSync } from './defaults-sync.js';
|
|
38
39
|
import { canonicalSkillSlug, localSkillState } from './skill-state.js';
|
|
39
40
|
import { ensureLanguageDefault } from './language.js';
|
|
@@ -565,16 +566,6 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
|
|
|
565
566
|
});
|
|
566
567
|
}
|
|
567
568
|
|
|
568
|
-
/**
|
|
569
|
-
* 是否运行在桌面壳内:宿主进程是 Electron(execPath 指向桌面壳可执行文件)。
|
|
570
|
-
* 壳品牌:DSH Desktop / DSH NEXT / DeepSeek HARNESS(NEXT 的公开发布名)。
|
|
571
|
-
* 与页面侧标记(page.js 传 desktop:true)互为兜底——iframe 内
|
|
572
|
-
* location.search 不含宿主 query,页面自检在设置 iframe 场景会漏。
|
|
573
|
-
*/
|
|
574
|
-
function isDesktopHost() {
|
|
575
|
-
return /(?:DSH Desktop|DSH NEXT|DeepSeek HARNESS)\.(?:app|exe)/i.test(process.execPath);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
569
|
export function apply(ctx, config) {
|
|
579
570
|
const log = ctx.logger || console;
|
|
580
571
|
// 托管实例:注入的平台地址优先。容器内只有这个地址可达(本地部署是
|
|
@@ -584,9 +575,11 @@ export function apply(ctx, config) {
|
|
|
584
575
|
log.info(`[dsh-agentone] platform url from instance env: ${injectedPlatformUrl}`);
|
|
585
576
|
config.platformUrl = injectedPlatformUrl;
|
|
586
577
|
}
|
|
587
|
-
// Desktop
|
|
588
|
-
//
|
|
589
|
-
//
|
|
578
|
+
// Desktop 环境探测:第三方壳发布 desktopProfiles + desktopPnpm,官方
|
|
579
|
+
// Electron 应用只发布 profileContext + pluginManager,判定统一在 desktop.js
|
|
580
|
+
// (品牌名与 profile 解析只有一份实现)。与页面侧标记(page.js 传
|
|
581
|
+
// desktop:true)互为兜底——iframe 内 location.search 不含宿主 query,
|
|
582
|
+
// 页面自检在设置 iframe 场景会漏。
|
|
590
583
|
const desktopProfiles = ctx.get?.('desktopProfiles');
|
|
591
584
|
const isDesktopRuntime = Boolean(desktopProfiles?.current?.name) || isDesktopHost();
|
|
592
585
|
// 官方 settings 服务(可选依赖):dsh ≥ 0.1.7 用它把平台模型写进 profile
|
|
@@ -992,6 +985,50 @@ export function apply(ctx, config) {
|
|
|
992
985
|
}
|
|
993
986
|
});
|
|
994
987
|
|
|
988
|
+
// 旧版本(≤0.5.38)装的技能没有本机记录:目录和平台台账都在,但本机
|
|
989
|
+
// 状态判定只会得到 conflict。按平台台账逐个重新应用(原子覆盖目录 +
|
|
990
|
+
// 重建记录 + 回报平台),一次点击修完一台机器。只处理「无本机记录」
|
|
991
|
+
// 的旧版安装;真有记录却内容不一致(用户改过)的仍走人工确认。
|
|
992
|
+
register('/agentone/api/skill/repair', async (request, response) => {
|
|
993
|
+
if (request.method !== 'POST') {
|
|
994
|
+
response.writeHead(405, { allow: 'POST' });
|
|
995
|
+
response.end();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
const credentials = await ensureFreshToken(config, log);
|
|
1000
|
+
if (!credentials) {
|
|
1001
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const result = await defaults.manualOperation(async (owner, options) => {
|
|
1005
|
+
const listing = await listPlatformSkills(owner.platform, owner.token, { signal: options.signal });
|
|
1006
|
+
const repaired = [];
|
|
1007
|
+
const failed = [];
|
|
1008
|
+
for (const item of listing?.skills || []) {
|
|
1009
|
+
if (!item.installed_fingerprint) continue;
|
|
1010
|
+
const local = await localSkillState(owner.platform, item.canonical_slug, options);
|
|
1011
|
+
if (!(local.state === 'conflict' && !local.record)) continue;
|
|
1012
|
+
try {
|
|
1013
|
+
await installPlatformSkill(owner.platform, owner.token, item.canonical_slug, {
|
|
1014
|
+
...options,
|
|
1015
|
+
source: ['plan', 'builtin'].includes(item.source) ? item.source : 'manual',
|
|
1016
|
+
});
|
|
1017
|
+
repaired.push(item.canonical_slug);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
failed.push({ name: item.canonical_slug, error: error?.message || '重新应用失败' });
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { repaired: repaired.length, failed: failed.length, names: repaired, errors: failed };
|
|
1023
|
+
});
|
|
1024
|
+
invalidateStatusCache();
|
|
1025
|
+
void defaults.sync({ manual: true }).catch(() => {});
|
|
1026
|
+
sendJson(response, 200, { ok: true, ...result });
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
sendJson(response, 502, { error: error?.message || '重新应用旧版技能失败' });
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
995
1032
|
// 套餐技能同步:把平台上 source=plan 且未安装的技能装齐。前端在套餐
|
|
996
1033
|
// 生效联动与自动补装触发器里调,也可手动触发;同步结果失效技能缓存。
|
|
997
1034
|
register('/agentone/api/skill/sync', async (request, response) => {
|
package/lib/page.js
CHANGED
|
@@ -263,7 +263,10 @@ const maybeAutoSyncPlanSkills = (forced) => {
|
|
|
263
263
|
const skills = lastStatus?.skills?.skills;
|
|
264
264
|
if (!forced) {
|
|
265
265
|
if (!Array.isArray(skills)) return;
|
|
266
|
-
|
|
266
|
+
// 只有本机真的缺目录(missing)才值得补装。conflict 是「目录在、记录/
|
|
267
|
+
// 来源对不上」,missingOnly 的同步会跳过它——旧版技能升级后这里每 60s
|
|
268
|
+
// 空转一次,用户只会看到重复提示却什么都不变。
|
|
269
|
+
if (!skills.some((skill) => skill.source === 'plan' && !skill.installed && skill.local_state === 'missing')) return;
|
|
267
270
|
}
|
|
268
271
|
planSkillSyncAt = now;
|
|
269
272
|
planSkillSyncing = true;
|
|
@@ -399,12 +402,18 @@ const renderDefaults = (s) => {
|
|
|
399
402
|
failed: '失败', conflict: '本机冲突', report_pending: '已安装,待回报', restart_required: '已安装,重启后生效' };
|
|
400
403
|
const sources = { platform: '平台清单', compatibility: '旧平台兼容默认', cache: '本机缓存' };
|
|
401
404
|
if (!config || !s.logged_in) { body.innerHTML = '<div class="muted">登录后检查平台默认能力</div>'; return; }
|
|
405
|
+
const entries = [...(config.skills || []), ...(config.plugins || [])];
|
|
406
|
+
// legacy:0.5.39 之前装的技能只有平台台账、没有本机记录(见 defaults-sync)。
|
|
407
|
+
// 这类条目单独给一个批量入口,不然升级后每台机器都要点 N 次「确认应用」。
|
|
408
|
+
const legacyCount = entries.filter((item) => item.legacy).length;
|
|
402
409
|
const summary = '<div class="skill-sub">' + esc(sources[config.source] || '尚未检查')
|
|
403
410
|
+ (config.revision !== null ? ' · 版本 ' + esc(config.revision) : '')
|
|
404
411
|
+ (config.checked_at ? ' · ' + esc(new Date(config.checked_at).toLocaleString()) : '')
|
|
405
412
|
+ (config.running ? ' · 正在检查' : '') + '</div>'
|
|
406
|
-
+ (config.error || config.profile_error ? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '')
|
|
407
|
-
|
|
413
|
+
+ (config.error || config.profile_error ? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '')
|
|
414
|
+
+ (legacyCount ? '<div class="skill-sub"><button id="defaults-repair" class="btn small ghost"'
|
|
415
|
+
+ (config.running ? ' disabled' : '') + '>重新应用旧版安装的技能(' + legacyCount + ')</button>'
|
|
416
|
+
+ ' 技能目录会按平台当前版本覆盖</div>' : '');
|
|
408
417
|
const html = summary + entries.map((item) => {
|
|
409
418
|
const action = ['failed', 'report_pending', 'missing'].includes(item.state) ? 'retry'
|
|
410
419
|
: ['conflict', 'pending'].includes(item.state) ? 'apply' : null;
|
|
@@ -414,6 +423,7 @@ const renderDefaults = (s) => {
|
|
|
414
423
|
return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
|
|
415
424
|
+ esc(item.description || item.name) + ' <span class="tag">' + (item.kind === 'skill' ? '技能' : '插件')
|
|
416
425
|
+ '</span></div><div class="skill-sub">' + esc(item.name) + ' · ' + esc(labels[item.state] || item.state)
|
|
426
|
+
+ (item.legacy ? '(旧版安装)' : '')
|
|
417
427
|
+ (item.version ? ' · ' + esc(item.version) : '')
|
|
418
428
|
+ (item.target_version && item.target_version !== item.version ? ' → ' + esc(item.target_version) : '')
|
|
419
429
|
+ (item.error ? '<br><span style="color:var(--err)">' + esc(item.error) + '</span>' : '')
|
|
@@ -429,6 +439,37 @@ const renderDefaults = (s) => {
|
|
|
429
439
|
? confirmThen(btn, '替换当前版本?', request, '已开始应用,请查看同步状态')
|
|
430
440
|
: withLoading(btn, request, '已开始重试,请查看同步状态');
|
|
431
441
|
});
|
|
442
|
+
const repair = body.querySelector('#defaults-repair');
|
|
443
|
+
if (repair) repair.onclick = async () => {
|
|
444
|
+
if (repair.dataset.confirming !== '1') {
|
|
445
|
+
repair.dataset.confirming = '1';
|
|
446
|
+
repair.dataset.originalLabel = repair.textContent;
|
|
447
|
+
repair.textContent = '确认按平台版本覆盖?';
|
|
448
|
+
repair.dataset.confirmTimer = String(setTimeout(() => {
|
|
449
|
+
repair.textContent = repair.dataset.originalLabel || '重新应用';
|
|
450
|
+
delete repair.dataset.confirming;
|
|
451
|
+
}, 3000));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
delete repair.dataset.confirming;
|
|
455
|
+
const original = repair.innerHTML;
|
|
456
|
+
repair.disabled = true;
|
|
457
|
+
repair.innerHTML = '<span class="spin"></span>重新应用中…';
|
|
458
|
+
try {
|
|
459
|
+
const resp = await fetch('api/skill/repair', { method: 'POST' });
|
|
460
|
+
const data = await resp.json().catch(() => ({}));
|
|
461
|
+
if (!resp.ok) throw new Error(data.error || '重新应用失败');
|
|
462
|
+
notify(data.failed
|
|
463
|
+
? '已重新应用 ' + data.repaired + ' 个技能,' + data.failed + ' 个失败'
|
|
464
|
+
: '已重新应用 ' + data.repaired + ' 个旧版技能');
|
|
465
|
+
} catch (error) {
|
|
466
|
+
notify(error.message || '重新应用失败', 'err');
|
|
467
|
+
} finally {
|
|
468
|
+
repair.disabled = false;
|
|
469
|
+
repair.innerHTML = original;
|
|
470
|
+
await refresh();
|
|
471
|
+
}
|
|
472
|
+
};
|
|
432
473
|
}
|
|
433
474
|
$('defaults-sync-btn').disabled = config.running;
|
|
434
475
|
};
|
|
@@ -461,14 +502,19 @@ const renderSkills = (s) => {
|
|
|
461
502
|
// 可升级:平台对比 latest 指纹/版本后给的结论,提示「v旧 → v新」;
|
|
462
503
|
// latest 查询失败时平台降级 false,这里不额外猜测
|
|
463
504
|
const upgradable = skill.installed && skill.upgrade_available;
|
|
505
|
+
// 目录在、记录不符:不是「没装」,重新应用会按平台版本覆盖目录
|
|
506
|
+
const stranded = !skill.installed && skill.local_state === 'conflict';
|
|
464
507
|
const meta = !skill.installed
|
|
465
|
-
?
|
|
508
|
+
? (stranded
|
|
509
|
+
? '<span class="muted">本机有目录但记录不符,需重新应用</span>'
|
|
510
|
+
: '<span class="muted">未安装</span>')
|
|
466
511
|
: upgradable
|
|
467
512
|
? '已装 ' + esc(skill.installed_version || '')
|
|
468
513
|
+ ' → <b>v' + esc(skill.latest_version || '新版本') + '</b>'
|
|
469
514
|
: '已装 ' + esc(skill.installed_version || '');
|
|
470
515
|
const action = !skill.installed
|
|
471
|
-
? '<button class="btn small primary" data-install="' + esc(skill.canonical_slug) + '"
|
|
516
|
+
? '<button class="btn small primary" data-install="' + esc(skill.canonical_slug) + '">'
|
|
517
|
+
+ (stranded ? '重新应用' : '安装') + '</button>'
|
|
472
518
|
: upgradable
|
|
473
519
|
? '<button class="btn small primary" data-upgrade="' + esc(skill.canonical_slug) + '">升级</button>'
|
|
474
520
|
+ '<button class="btn small danger" data-uninstall="' + esc(skill.canonical_slug) + '">卸载</button>'
|
|
@@ -487,7 +533,7 @@ const renderSkills = (s) => {
|
|
|
487
533
|
// 次确认
|
|
488
534
|
const fingerprint = list.map((skill) =>
|
|
489
535
|
skill.canonical_slug + ':' + (skill.installed ? 1 : 0) + ':' + (skill.installed_version || '')
|
|
490
|
-
+ ':' + (skill.upgrade_available ? 1 : 0),
|
|
536
|
+
+ ':' + (skill.upgrade_available ? 1 : 0) + ':' + (skill.local_state || ''),
|
|
491
537
|
).join('|') + '#' + metaHtml;
|
|
492
538
|
if ($('skills-list').dataset.fingerprint !== fingerprint) {
|
|
493
539
|
$('skills-list').dataset.fingerprint = fingerprint;
|
|
@@ -1003,15 +1049,18 @@ const renderPlugins = async () => {
|
|
|
1003
1049
|
return;
|
|
1004
1050
|
}
|
|
1005
1051
|
const rows = data.plugins.map((plugin) => {
|
|
1006
|
-
// 状态区分三种:未安装 → 安装按钮;插件(带 bundle 声明)→
|
|
1052
|
+
// 状态区分三种:未安装 → 安装按钮;插件(带 bundle 声明)→ 启用态;
|
|
1007
1053
|
// 纯依赖(无 bundle 声明,如 @larksuite/cli 这类 CLI 工具包)→ 「依赖」
|
|
1008
|
-
// 标签——它永远不进 bundle
|
|
1054
|
+
// 标签——它永远不进 bundle 层,不能显示「未启用/重启生效」误导用户。
|
|
1055
|
+
// 术语与官方「设置 → 插件」对齐:installed(在 node_modules)和
|
|
1056
|
+
// enabled(在 dsh.profile.bundles)是两个维度,官方列表把两类都显示为
|
|
1057
|
+
// 「已安装」,这里的「未启用」= 已安装但没挂进 profile。
|
|
1009
1058
|
const statePill = !plugin.version
|
|
1010
1059
|
? '<span class="pill warn" style="font-size:11px">未安装</span>'
|
|
1011
1060
|
: plugin.registered
|
|
1012
|
-
? '<span class="pill ok" style="font-size:11px"
|
|
1061
|
+
? '<span class="pill ok" style="font-size:11px">已启用</span>'
|
|
1013
1062
|
: plugin.bundled
|
|
1014
|
-
? '<span class="pill warn" style="font-size:11px"
|
|
1063
|
+
? '<span class="pill warn" style="font-size:11px">未启用</span>'
|
|
1015
1064
|
: '<span class="tag">依赖</span>';
|
|
1016
1065
|
const builtinTag = plugin.builtin
|
|
1017
1066
|
? '<span class="tag">受管</span>'
|
|
@@ -1041,7 +1090,7 @@ const renderPlugins = async () => {
|
|
|
1041
1090
|
return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
|
|
1042
1091
|
+ esc(plugin.name) + builtinTag + statePill
|
|
1043
1092
|
+ '</div><div class="skill-sub">' + desc + esc(plugin.source) + ' · ' + version
|
|
1044
|
-
+ (plugin.registered ? ' ·
|
|
1093
|
+
+ (plugin.registered ? ' · 已启用,重启后生效' : plugin.bundled ? ' · 已安装,未启用当前 profile(「升级」或重装可启用)' : '')
|
|
1045
1094
|
+ '</div></div>' + actionHtml + '</div>';
|
|
1046
1095
|
});
|
|
1047
1096
|
// 指纹保护(与 renderSkills 同思路):内容没变不碰 innerHTML——升级后
|
package/lib/profile-plugins.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// 当前 profile 的插件操作;读取真实包和注册状态,写操作共享实例队列。
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { isDesktopHost, resolveProfileTarget } from './desktop.js';
|
|
4
6
|
import { dshPluginCli, pnpmViewLatest } from './proc.js';
|
|
5
7
|
import { resolveDshHome } from './store.js';
|
|
6
8
|
|
|
@@ -20,22 +22,97 @@ async function readJson(path) {
|
|
|
20
22
|
try { return JSON.parse(await readFile(path, 'utf8')); }
|
|
21
23
|
catch (error) { if (error.code === 'ENOENT') return null; throw new Error('插件配置文件无法读取,请检查本机 profile'); }
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
/** npm spec 白名单:`name` 或 `name@version`(`TARGET_RE` 同时用于官方
|
|
27
|
+
* 桌面端 pluginManager 通道,那个通道只接受包名/版本,不接受 pnpm 选项)。 */
|
|
28
|
+
export const TARGET_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[^\s@]+)?$/i;
|
|
29
|
+
|
|
30
|
+
/** 官方桌面端 pluginManager 的返回:application 为终态枚举,packageResult
|
|
31
|
+
* 是 pnpm 段的输出。`overridden` 是「改动保留了,只是被别的层覆盖显示」,
|
|
32
|
+
* 不是失败(市场侧踩过这个坑)。 */
|
|
33
|
+
const MANAGER_APPLIED = new Set(['applied', 'restart-required', 'overridden']);
|
|
34
|
+
|
|
35
|
+
/** 官方宿主的结构化错误 → 中文可操作文案。宿主的 error 是 JSON 对象
|
|
36
|
+
* (实测 dsh-im@1.0.5 在 dsh 0.2.0-rc.2 上返回 incompatible-version),
|
|
37
|
+
* 直接 stringify 会把英文原文甩给用户(AGENTS.md 报错规范)。 */
|
|
38
|
+
function failureDetail(error) {
|
|
39
|
+
if (typeof error === 'string') return error;
|
|
40
|
+
if (error instanceof Error) return error.message;
|
|
41
|
+
if (error && typeof error === 'object' && !Array.isArray(error)) {
|
|
42
|
+
if (error.code === 'incompatible-version' && Array.isArray(error.incompatible) && error.incompatible.length) {
|
|
43
|
+
return error.incompatible.map((item) => {
|
|
44
|
+
const peers = Object.entries(item.peers || {})
|
|
45
|
+
.map(([name, range]) => `${name}@${range}`).join('、');
|
|
46
|
+
return `${item.name || '插件'}@${item.version || '?'} 与当前宿主 dsh ${item.runtimeVersion || '?'} 不兼容`
|
|
47
|
+
+ (peers ? `(peer 依赖要求 ${peers})` : '')
|
|
48
|
+
+ ':请在「设置 → 插件」为该版本授权,或等插件发布兼容新版本';
|
|
49
|
+
}).join(';');
|
|
50
|
+
}
|
|
51
|
+
if (typeof error.message === 'string' && error.message) return error.message;
|
|
52
|
+
if (typeof error.code === 'string' && error.code) return `插件操作未完成(${error.code}),请在「设置 → 插件」里查看详情`;
|
|
53
|
+
}
|
|
54
|
+
if (error === undefined) return '';
|
|
55
|
+
try { return JSON.stringify(error); } catch { return String(error); }
|
|
25
56
|
}
|
|
26
|
-
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 官方 Electron 桌面端通道:这个 profile 完全由宿主 in-process 的
|
|
60
|
+
* pluginManager 管(安装/卸载只会走它自己的「设置 → 插件」管线)。只接受
|
|
61
|
+
* `add <spec>` / `remove <name>`——官方管线自己处理构建脚本审批,不接受
|
|
62
|
+
* pnpm 选项,传进去会让整次操作被拒(`--allow-build` 因此不转发)。
|
|
63
|
+
* 返回契约与 dshPluginCli 一致:成功 {stdout, stderr},失败抛 Error。
|
|
64
|
+
*/
|
|
65
|
+
export function desktopManagerRun(manager) {
|
|
66
|
+
let queue = Promise.resolve();
|
|
67
|
+
const settle = (result) => {
|
|
68
|
+
const stdout = String(result?.packageResult?.output || '');
|
|
69
|
+
if (MANAGER_APPLIED.has(result?.application)) return { stdout, stderr: '' };
|
|
70
|
+
throw new Error(failureDetail(result?.error) || '插件操作未完成,请在「设置 → 插件」里重试');
|
|
71
|
+
};
|
|
72
|
+
const run = async (args, options = {}) => {
|
|
73
|
+
options.signal?.throwIfAborted();
|
|
74
|
+
const [command, target, ...extra] = args;
|
|
75
|
+
// 官方管线自己处理构建脚本审批(与应用内「设置 → 插件」同一管线),
|
|
76
|
+
// 不接受 pnpm 选项:除了 dsh-agentone 自己生成的 --allow-build,其余
|
|
77
|
+
// 参数一律拒绝,避免「静默丢参数改变用户意图」。
|
|
78
|
+
const rejected = extra.filter((argument) => !/^--allow-build=[^@\s]+$/.test(argument));
|
|
79
|
+
if (rejected.length) throw new Error('官方桌面端的插件操作由应用自己执行,不接受附加参数');
|
|
80
|
+
if (typeof target !== 'string' || !TARGET_RE.test(target)) throw new Error('插件名不合法,请填写完整的 npm 包名');
|
|
81
|
+
if (command === 'add') {
|
|
82
|
+
const requestId = randomUUID();
|
|
83
|
+
const cancel = () => manager.cancelInstall?.(requestId);
|
|
84
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
85
|
+
try {
|
|
86
|
+
const result = await manager.installBundle(target, { requestId });
|
|
87
|
+
options.signal?.throwIfAborted();
|
|
88
|
+
return settle(result);
|
|
89
|
+
} finally {
|
|
90
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (command === 'remove') return settle(await manager.removeBundle(target));
|
|
94
|
+
throw new Error(`官方桌面端不支持这个插件操作(${command}),请在「设置 → 插件」里完成`);
|
|
95
|
+
};
|
|
96
|
+
return (args, options = {}) => {
|
|
97
|
+
const next = queue.then(() => run(args, options));
|
|
98
|
+
queue = next.catch(() => undefined);
|
|
99
|
+
return next;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createProfilePlugins({ ctx, config = {}, desktopRun, runCli = dshPluginCli, latest = pnpmViewLatest,
|
|
104
|
+
isDesktop = () => Boolean(ctx.get?.('desktopProfiles')) || isDesktopHost(),
|
|
105
|
+
managerRun = desktopManagerRun }) {
|
|
27
106
|
let queue = Promise.resolve();
|
|
28
107
|
let closed = false;
|
|
29
108
|
let policy = [];
|
|
30
109
|
function target() {
|
|
31
110
|
if (closed) throw new Error('插件已停止,请重新打开后再试');
|
|
32
|
-
const desktop = ctx.get?.('desktopProfiles');
|
|
33
|
-
const isDesktop = Boolean(desktop) || /DSH Desktop\.(?:app|exe)/i.test(process.execPath);
|
|
34
|
-
const name = desktop?.current?.name || (!isDesktop ? config.profileName : null);
|
|
35
|
-
if (!validProfile(name)) throw new Error('无法确定当前 DSH profile,请在 AgentOne 插件配置中填写 profileName 后重启');
|
|
36
111
|
const homeService = ctx.get?.('dshHomePath');
|
|
37
112
|
const home = resolve(config.dshHome || (typeof homeService === 'function' ? homeService() : homeService) || resolveDshHome());
|
|
38
|
-
|
|
113
|
+
const resolved = resolveProfileTarget({ ctx, config, home, desktop: isDesktop() });
|
|
114
|
+
if (!resolved) throw new Error('无法确定当前 DSH profile,请在 AgentOne 插件配置中填写 profileName 后重启');
|
|
115
|
+
return resolved;
|
|
39
116
|
}
|
|
40
117
|
function enqueue(operation) {
|
|
41
118
|
let expected;
|
|
@@ -91,9 +168,12 @@ export function createProfilePlugins({ ctx, config = {}, desktopRun, runCli = ds
|
|
|
91
168
|
await options.beforeCommit?.();
|
|
92
169
|
check(current, options);
|
|
93
170
|
if (current.desktop) {
|
|
171
|
+
// 第三方壳:受管 desktopPnpm;官方 Electron 应用:pluginManager。
|
|
94
172
|
const service = ctx.get?.('desktopPnpm');
|
|
95
|
-
if (
|
|
96
|
-
|
|
173
|
+
if (service && desktopRun) return desktopRun(service, current.dir)(args, options);
|
|
174
|
+
const manager = ctx.get?.('pluginManager');
|
|
175
|
+
if (manager) return managerRun(manager)(args, options);
|
|
176
|
+
throw new Error('桌面插件安装通道尚未就绪,请稍后重试');
|
|
97
177
|
}
|
|
98
178
|
return runCli(args, current.name, { env: { DSH_HOME: current.home }, signal: options.signal });
|
|
99
179
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI
|
|
3
|
+
"version": "0.5.42",
|
|
4
|
+
"description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;宿主插件错误中文化与启用态术语对齐官方插件管理",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"smoke": "node scripts/env-matrix-smoke.mjs"
|