dsh-agentone 0.5.15 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/lib/cliauth.mjs +115 -0
- package/lib/client.js +51 -0
- package/lib/index.js +266 -29
- package/lib/model.js +13 -3
- package/lib/page.js +179 -16
- package/lib/skillhub.js +130 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,12 +14,24 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
14
14
|
走 TokenHub 按用户计量。平台 403 的 `plan_required` 错误码原样透传,
|
|
15
15
|
前端据此引导申请套餐(不依赖中文文案匹配)。
|
|
16
16
|
- **SkillHub 技能(M3)**:套餐技能启动/登录时自动装齐;技能市场搜索、
|
|
17
|
-
|
|
17
|
+
手动安装/卸载;已装技能平台侧对比 latest 版本/指纹,有更新时展示
|
|
18
|
+
「v旧 → v新」并支持一键升级(覆盖式重装,原子替换旧目录);技能包
|
|
19
|
+
zip 走平台代理下载(穿越清洗 + 原子落盘)。
|
|
20
|
+
- **SkillHub 连接配置(M3+)**:登录后自动从平台拉取 registry 与用户级
|
|
21
|
+
个人 token,写入 `$DSH_HOME/agentone/skillhub.env`(0600,含
|
|
22
|
+
`SKILLHUB_BASE_URL`/`SKILLHUB_TOKEN`/`CLAWHUB_REGISTRY`/`CLAWHUB_TOKEN`
|
|
23
|
+
四个与平台托管容器同名的变量),本机有 clawhub CLI 时同步写入登录态;
|
|
24
|
+
登出时全部清理。会话内使用:`source $DSH_HOME/agentone/skillhub.env`。
|
|
18
25
|
- **套餐申请(M3+)**:状态页「套餐」tab 展示当前套餐/审批中/可申请列表,
|
|
19
26
|
一键提交申请;审批通过后前端轮询检测到 active 自动同步模型。
|
|
20
27
|
- **dsh 插件管理(M3+)**:列出镜像内置与用户自装插件(版本/升级/激活态),
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
内置插件缺失时启动自动重装(自身也在常驻保证清单内,防误删导致平台集成
|
|
29
|
+
失效);desktop 受管 pnpm 通道操作串行排队,宿主并发锁占用时自动退避
|
|
30
|
+
重试(不把「another desktop pnpm operation is already running」抛给用户);
|
|
31
|
+
自身随平台镜像升级(列表中不提供按钮)。
|
|
32
|
+
- **CLI 探测与授权(M4)**:`lark-cli` / `ccpg-cli` 安装与授权态三态探测;
|
|
33
|
+
已装未授权时「去授权」发起官方 Device Flow(授权链接 + ASCII 二维码 +
|
|
34
|
+
验证码 + 自动轮询确认,设备码仅存插件进程内存)。
|
|
23
35
|
- **首启引导**:dsh 首次启动弹出飞书登录引导(order -50,早于官方
|
|
24
36
|
「添加 API Key」),登录一次即跳过官方密钥引导;暗色主题适配。
|
|
25
37
|
|
|
@@ -89,7 +101,7 @@ lib/
|
|
|
89
101
|
skill.js 技能下载/解包/卸载(zip 穿越清洗、原子落盘)
|
|
90
102
|
store.js 凭据持久化(0600;损坏自动移开)
|
|
91
103
|
proc.js dsh/pnpm CLI 转发与 lark-cli/ccpg-cli 探测
|
|
92
|
-
client.js dsh 浏览器端 bundle(设置菜单 iframe +
|
|
104
|
+
client.js dsh 浏览器端 bundle(设置菜单 iframe + 首启引导 + 设置导航 workbench 图标换肤)
|
|
93
105
|
```
|
|
94
106
|
|
|
95
107
|
## 开发
|
package/lib/cliauth.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// CLI Device Flow 授权(与平台 cli-deployd auth_begin/auth_complete 同源语义)。
|
|
2
|
+
//
|
|
3
|
+
// 独立 dsh 的 CLI 授权由插件进程直接驱动本机 CLI(lark-cli / ccpg-cli 官方
|
|
4
|
+
// 设备流命令,两 CLI 参数面一致):
|
|
5
|
+
// beginCliAuth 发起授权(--no-wait --json),解析出 verification_url /
|
|
6
|
+
// user_code / 设备码 / expires_in;ccpg-cli 老版本不支持
|
|
7
|
+
// --json 时去掉重试(文本解析兜底)。
|
|
8
|
+
// completeCliAuth 用 begin 拿到的设备码轮询并完成授权(阻塞至用户在
|
|
9
|
+
// 授权页同意或超时)。
|
|
10
|
+
// cliAuthQrcode lark-cli 的 auth qrcode --ascii 生成终端二维码(状态页
|
|
11
|
+
// 等宽字体直接渲染)。
|
|
12
|
+
//
|
|
13
|
+
// 安全:所有子进程经 runCliTool(execFile 参数数组,无 shell 参与)。设备码
|
|
14
|
+
// 是 begin 步骤本机 CLI 自己签发的随机 token,非用户自由输入;仍先过
|
|
15
|
+
// sanitizeDeviceCode 白名单(字母数字、连字符与点号,无空白与其它符号),校验
|
|
16
|
+
// 失败直接抛中文错误、不触碰子进程。校验后的 token 以独立 argv 元素传给
|
|
17
|
+
// execFile——无 shell 二次解析,token 字符集也决定了它不可能被解释为选项。
|
|
18
|
+
import { runCliTool } from './proc.js';
|
|
19
|
+
|
|
20
|
+
/** 授权 begin/complete 超时对齐平台 cli-deployd(begin 30s / complete 240s)。 */
|
|
21
|
+
export const CLI_AUTH_BEGIN_TIMEOUT_MS = 30_000;
|
|
22
|
+
export const CLI_AUTH_COMPLETE_TIMEOUT_MS = 240_000;
|
|
23
|
+
|
|
24
|
+
/** 设备码白名单:官方设备码为字母数字、连字符与点号(实测 lark-cli 的
|
|
25
|
+
* device_code 含 base64url 段 + '.' 分隔的签名后缀),长度 4–256。 */
|
|
26
|
+
const DEVICE_CODE_RE = /^[A-Za-z0-9_.-]{4,256}$/;
|
|
27
|
+
|
|
28
|
+
/** 校验设备码:非法值返回 null(调用方直接拒绝,不进任何子进程参数)。 */
|
|
29
|
+
export function sanitizeDeviceCode(raw) {
|
|
30
|
+
const value = String(raw || '').trim();
|
|
31
|
+
return DEVICE_CODE_RE.test(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cliToolEnv(tool) {
|
|
35
|
+
return tool === 'ccpg-cli' ? { CCPG_FULL_CLI: '1' } : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function combineOutput(stdout, stderr) {
|
|
39
|
+
return `${stdout || ''}\n${stderr || ''}`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 发起 Device Flow:auth login --no-wait,返回原始输出(含 JSON/URL)。 */
|
|
43
|
+
export async function beginCliAuth(tool, { timeoutMs = CLI_AUTH_BEGIN_TIMEOUT_MS } = {}) {
|
|
44
|
+
const options = { timeoutMs, env: cliToolEnv(tool) };
|
|
45
|
+
let result;
|
|
46
|
+
try {
|
|
47
|
+
result = await runCliTool(tool, ['auth', 'login', '--no-wait', '--json'], options);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (tool !== 'ccpg-cli') throw error;
|
|
50
|
+
result = await runCliTool(tool, ['auth', 'login', '--no-wait'], options);
|
|
51
|
+
}
|
|
52
|
+
return combineOutput(result.stdout, result.stderr);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 完成 Device Flow:用 begin 返回的设备码轮询并完成授权(阻塞至用户同意
|
|
57
|
+
* 或超时)。设备码先过白名单(见文件头);失败抛中文错误。
|
|
58
|
+
*/
|
|
59
|
+
export async function completeCliAuth(tool, rawDeviceCode, { timeoutMs = CLI_AUTH_COMPLETE_TIMEOUT_MS } = {}) {
|
|
60
|
+
const token = sanitizeDeviceCode(rawDeviceCode);
|
|
61
|
+
if (token === null) {
|
|
62
|
+
throw new Error('设备授权码格式不正确');
|
|
63
|
+
}
|
|
64
|
+
const args = ['auth', 'login', '--device-code', token];
|
|
65
|
+
const { stdout, stderr } = await runCliTool(tool, args, {
|
|
66
|
+
timeoutMs,
|
|
67
|
+
env: cliToolEnv(tool),
|
|
68
|
+
});
|
|
69
|
+
return combineOutput(stdout, stderr);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** lark-cli 生成授权 URL 的 ASCII 二维码(状态页等宽字体直接渲染)。 */
|
|
73
|
+
export async function cliAuthQrcode(url, { timeoutMs = 15_000 } = {}) {
|
|
74
|
+
const { stdout } = await runCliTool('lark-cli', ['auth', 'qrcode', String(url || ''), '--ascii'], {
|
|
75
|
+
timeoutMs,
|
|
76
|
+
});
|
|
77
|
+
return String(stdout || '').trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 从 auth begin 输出解析授权信息(verification_url / user_code / 设备码)。
|
|
82
|
+
* 与平台 _normalize_cli_auth_data 同口径:JSON 优先(lark-cli --json),
|
|
83
|
+
* 键名兼容 snake/camel;ccpg-cli 纯文本输出退化为正则抽 URL/code。
|
|
84
|
+
*/
|
|
85
|
+
export function parseCliAuthBeginOutput(raw) {
|
|
86
|
+
const text = String(raw || '');
|
|
87
|
+
const jsonMatcher = /\{[^{}]*\}/g;
|
|
88
|
+
for (const chunk of text.match(jsonMatcher) || []) {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(chunk);
|
|
91
|
+
if (!data || typeof data !== 'object') continue;
|
|
92
|
+
const verificationUrl = data.verification_url || data.verificationUrl || data.verification_uri || data.url;
|
|
93
|
+
const userCode = data.user_code || data.userCode || data.code || '';
|
|
94
|
+
const deviceCode = data.device_code || data.deviceCode || '';
|
|
95
|
+
const expiresIn = Number(data.expires_in ?? data.expiresIn) || 600;
|
|
96
|
+
if (verificationUrl && (deviceCode || userCode)) {
|
|
97
|
+
return {
|
|
98
|
+
verificationUrl: String(verificationUrl),
|
|
99
|
+
userCode: String(userCode),
|
|
100
|
+
deviceCode: String(deviceCode),
|
|
101
|
+
expiresIn,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// 非 JSON 块继续
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ccpg-cli 文本形态:URL 与 code 各取首个出现
|
|
109
|
+
const urlMatch = /https?:\/\/[^\s"'<>]+/.exec(text);
|
|
110
|
+
if (!urlMatch) return null;
|
|
111
|
+
const withoutUrl = text.slice(0, urlMatch.index) + ' ' + text.slice(urlMatch.index + urlMatch[0].length);
|
|
112
|
+
const codeMatch = /\b([A-Z0-9]{4,12})\b/.exec(withoutUrl);
|
|
113
|
+
if (!codeMatch) return null;
|
|
114
|
+
return { verificationUrl: urlMatch[0], userCode: codeMatch[1], deviceCode: codeMatch[1], expiresIn: 600 };
|
|
115
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -189,7 +189,58 @@ window.__ModuleLoader__.load({
|
|
|
189
189
|
);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// ── 设置导航图标(workbench_fill)─────────────────────────────────────
|
|
193
|
+
// dsh 设置弹窗的导航图标由 shell(ui-settings-general)按 section id 硬编码,
|
|
194
|
+
// 注册项没有 icon 选项(仅 models/agent-presets/plugins 有专用图标,其余一律
|
|
195
|
+
// fallback 齿轮)。改用「标记 + CSS」换肤:给「AgentOne」导航格打 data 属性,
|
|
196
|
+
// 注入样式隐藏官方 svg、用 mask 画 workbench 图标;颜色取 currentColor(导航格
|
|
197
|
+
// color 即 --dsw-alias-label-primary),随 dsh 明暗主题自动适配。只加属性不
|
|
198
|
+
// 改节点结构,React 重渲染/卸载都不会被破坏。
|
|
199
|
+
const NAV_ICON_LABEL = 'AgentOne'; // 与 settings.section 注册的 label 保持一致
|
|
200
|
+
const NAV_ICON_PATH = 'M160 800.256C160 835.424 188.608 864 223.712 864h92.48V448H160v352.256zM380.16 864h420.096A63.808 63.808 0 0 0 864 800.256V448H380.16v416zM800.256 160H223.68A63.808 63.808 0 0 0 160 223.744V384h704V223.744A63.776 63.776 0 0 0 800.256 160';
|
|
201
|
+
|
|
202
|
+
function tagNavIconCells() {
|
|
203
|
+
for (const cell of document.querySelectorAll('button')) {
|
|
204
|
+
if (cell.textContent.trim() !== NAV_ICON_LABEL) continue;
|
|
205
|
+
if (cell.querySelector('svg')) cell.setAttribute('data-agentone-nav-cell', '');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function injectNavIconStyle() {
|
|
210
|
+
if (document.getElementById('agentone-nav-icon-style')) return;
|
|
211
|
+
const icon = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='${NAV_ICON_PATH}'/%3E%3C/svg%3E`;
|
|
212
|
+
const style = document.createElement('style');
|
|
213
|
+
style.id = 'agentone-nav-icon-style';
|
|
214
|
+
style.textContent = [
|
|
215
|
+
'button[data-agentone-nav-cell] > svg { display: none; }',
|
|
216
|
+
'button[data-agentone-nav-cell]::before {',
|
|
217
|
+
" content: ''; flex: none; width: 16px; height: 16px;",
|
|
218
|
+
' background-color: currentColor;',
|
|
219
|
+
` -webkit-mask: url("${icon}") center / contain no-repeat;`,
|
|
220
|
+
` mask: url("${icon}") center / contain no-repeat;`,
|
|
221
|
+
'}',
|
|
222
|
+
].join('\n');
|
|
223
|
+
document.head.appendChild(style);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 设置面板每次打开都重新挂载导航节点:监听 DOM 变化补打标记
|
|
227
|
+
// (rAF 节流,且只在有新增节点时才扫描)
|
|
228
|
+
function setupNavIcon() {
|
|
229
|
+
injectNavIconStyle();
|
|
230
|
+
let scheduled = false;
|
|
231
|
+
new MutationObserver((records) => {
|
|
232
|
+
if (scheduled || !records.some((record) => record.addedNodes.length)) return;
|
|
233
|
+
scheduled = true;
|
|
234
|
+
requestAnimationFrame(() => {
|
|
235
|
+
scheduled = false;
|
|
236
|
+
tagNavIconCells();
|
|
237
|
+
});
|
|
238
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
239
|
+
tagNavIconCells();
|
|
240
|
+
}
|
|
241
|
+
|
|
192
242
|
function apply(ctx) {
|
|
243
|
+
setupNavIcon();
|
|
193
244
|
ctx.slots.inject('settings.section', () =>
|
|
194
245
|
ctx.slots.register(
|
|
195
246
|
{
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from './auth.js';
|
|
31
31
|
import { applyPlatformModel, removePlatformModel } from './model.js';
|
|
32
32
|
import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
|
|
33
|
+
import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
|
|
34
|
+
import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
|
|
33
35
|
import { platformFetch } from './http.js';
|
|
34
36
|
import { donePageHtml, statusPageHtml } from './page.js';
|
|
35
37
|
import { compareSemver, dshPluginCli, firstErrorLines, probeCliDetail, pnpmViewLatest, upgradeCliTool } from './proc.js';
|
|
@@ -105,6 +107,33 @@ async function syncPlatformModel(config, log) {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/** 应用平台 SkillHub 配置并落盘(成功清 skillhub_error,失败记录原因)。
|
|
111
|
+
* 与 syncPlatformModel 同容错模式:失败不阻断登录/启动,状态页展示原因。 */
|
|
112
|
+
async function syncPlatformSkillhub(config, log, credentials) {
|
|
113
|
+
const accessToken = credentials?.access_token;
|
|
114
|
+
if (!accessToken) return null;
|
|
115
|
+
try {
|
|
116
|
+
const result = await applyPlatformSkillhub(
|
|
117
|
+
config,
|
|
118
|
+
credentials.platform_url || config.platformUrl,
|
|
119
|
+
accessToken,
|
|
120
|
+
);
|
|
121
|
+
await updateCredentials(config, {
|
|
122
|
+
skillhub_registry: result.registryUrl,
|
|
123
|
+
skillhub_token: result.token,
|
|
124
|
+
skillhub_synced_at: new Date().toISOString(),
|
|
125
|
+
skillhub_error: null,
|
|
126
|
+
});
|
|
127
|
+
log.info(`[dsh-agentone] platform skillhub config applied: ${result.registryUrl}`);
|
|
128
|
+
return result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = error?.message || String(error);
|
|
131
|
+
await updateCredentials(config, { skillhub_error: message }).catch(() => {});
|
|
132
|
+
log.warn(`[dsh-agentone] platform skillhub config sync failed: ${message}`);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
/** 进行中的令牌轮换:临期窗口内多个请求共享同一次 refresh(轮换是
|
|
109
138
|
* 一次性的,并发各刷一次会 401 把用户登出)。 */
|
|
110
139
|
let tokenRefreshInFlight = null;
|
|
@@ -173,6 +202,12 @@ async function runPluginCli(args) {
|
|
|
173
202
|
return dshPluginCli(args, PLUGIN_PROFILE);
|
|
174
203
|
}
|
|
175
204
|
|
|
205
|
+
/** 宿主 DesktopPnpmService 的并发锁占用报错(同一时刻只允许一个 pnpm
|
|
206
|
+
* 操作——设计如此,不是故障)。识别后应排队等待而非把错误抛给用户。 */
|
|
207
|
+
const DESKTOP_PNPM_BUSY_RE = /another desktop pnpm operation is already running/i;
|
|
208
|
+
|
|
209
|
+
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
210
|
+
|
|
176
211
|
/**
|
|
177
212
|
* 包装宿主 desktopPnpm.runPlugin,对齐 dshPluginCli 的返回契约(成功
|
|
178
213
|
* {stdout, stderr},失败 throw Error)。宿主契约(dsh-plugin-desktop 的
|
|
@@ -183,36 +218,69 @@ async function runPluginCli(args) {
|
|
|
183
218
|
* 2. 返回 {stdout, stderr, done, cancel} 句柄而非 Promise,done 结算
|
|
184
219
|
* {exitCode, signal} 且不因非零退出 reject——直接 await 句柄会立即
|
|
185
220
|
* 返回,装完前的落盘校验必然读到旧版本,误报「升级未生效」。
|
|
221
|
+
*
|
|
222
|
+
* 并发占用(启动时常驻检测 vs 用户在插件市场装包同时发生)是高频场景:
|
|
223
|
+
* 宿主 busy 时同步抛错。这里做两层防御——
|
|
224
|
+
* 1. 插件自身操作串行队列:自己绝不和自己抢锁;
|
|
225
|
+
* 2. 对宿主 busy 退避重试(2s 起最多 5 次约 2 分钟):等宿主侧的其它
|
|
226
|
+
* 操作(插件市场安装等)结束后继续,而不是报错打断用户。
|
|
186
227
|
*/
|
|
187
228
|
export function desktopRunPlugin(service, invokingDir) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
let queue = Promise.resolve();
|
|
230
|
+
const run = async (args) => {
|
|
231
|
+
// 宿主 lock 是同步抛错(runPlugin 调用即抛),先探再跑 + 失败退避都做
|
|
232
|
+
let attempt = 0;
|
|
233
|
+
for (;;) {
|
|
234
|
+
let handle;
|
|
235
|
+
try {
|
|
236
|
+
handle = service.runPlugin([...args], invokingDir);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const message = error?.message || String(error);
|
|
239
|
+
if (DESKTOP_PNPM_BUSY_RE.test(message) && attempt < 5) {
|
|
240
|
+
attempt += 1;
|
|
241
|
+
await sleep(2000 * attempt);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
let stdout = '';
|
|
247
|
+
let stderr = '';
|
|
248
|
+
const append = (acc, chunk) => (acc + String(chunk)).slice(-64 * 1024);
|
|
249
|
+
handle.stdout?.on?.('data', (chunk) => { stdout = append(stdout, chunk); });
|
|
250
|
+
handle.stderr?.on?.('data', (chunk) => { stderr = append(stderr, chunk); });
|
|
251
|
+
// 与 web 通道 runCliTool 同宽的超时(300s):宿主自身不设整体超时
|
|
252
|
+
let timedOut = false;
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
timedOut = true;
|
|
255
|
+
handle.cancel();
|
|
256
|
+
}, 300_000);
|
|
257
|
+
try {
|
|
258
|
+
const outcome = await handle.done;
|
|
259
|
+
if (timedOut) throw new Error('命令执行超时,请稍后重试');
|
|
260
|
+
if (outcome.exitCode !== 0 || outcome.signal != null) {
|
|
261
|
+
const message = firstErrorLines(`${stderr}\n${stdout}`);
|
|
262
|
+
// 旧版宿主在子进程段抛 busy:同样排队重试
|
|
263
|
+
if (DESKTOP_PNPM_BUSY_RE.test(message) && attempt < 5) {
|
|
264
|
+
attempt += 1;
|
|
265
|
+
await sleep(2000 * attempt);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// ERR_PNPM_IGNORED_BUILDS 等非零退出在这里抛出,由调用侧
|
|
269
|
+
// install/upgrade 的「以落盘版本为准」兜底接管
|
|
270
|
+
throw new Error(message || `命令执行失败(退出码 ${outcome.exitCode ?? '未知'})`);
|
|
271
|
+
}
|
|
272
|
+
return { stdout, stderr };
|
|
273
|
+
} finally {
|
|
274
|
+
clearTimeout(timer);
|
|
210
275
|
}
|
|
211
|
-
return { stdout, stderr };
|
|
212
|
-
} finally {
|
|
213
|
-
clearTimeout(timer);
|
|
214
276
|
}
|
|
215
277
|
};
|
|
278
|
+
return (args) => {
|
|
279
|
+
const next = queue.then(() => run(args));
|
|
280
|
+
// 队列尾吞错,避免一次失败卡死后续排队操作
|
|
281
|
+
queue = next.catch(() => undefined);
|
|
282
|
+
return next;
|
|
283
|
+
};
|
|
216
284
|
}
|
|
217
285
|
|
|
218
286
|
/**
|
|
@@ -289,7 +357,17 @@ const INBOX_BUNDLES = new Set([
|
|
|
289
357
|
* 每次 dsh 启动时检测:缺失则静默重装(常驻保证),已装则不碰(尊重用户
|
|
290
358
|
* 的版本选择)。
|
|
291
359
|
*/
|
|
292
|
-
const BUILTIN_PLUGINS = [
|
|
360
|
+
const BUILTIN_PLUGINS = [
|
|
361
|
+
'dsh-better-sidebar',
|
|
362
|
+
'dsh-harness-one',
|
|
363
|
+
'dshmarket',
|
|
364
|
+
'dsh-im',
|
|
365
|
+
// 自身也纳入常驻保证:独立/桌面版用户删掉依赖或 profile 损坏导致本插件
|
|
366
|
+
// 缺失时,最后一个运行周期内无法自救(插件代码不在了)——但只要还装着,
|
|
367
|
+
// 卸载残留/版本损坏后的重装走这里。镜像内 self_managed 时列表页隐藏
|
|
368
|
+
// 卸载按钮,不会出现「自动重装刚被用户卸载的自己」的拉扯。
|
|
369
|
+
PLUGIN_PACKAGE_NAME,
|
|
370
|
+
];
|
|
293
371
|
|
|
294
372
|
/**
|
|
295
373
|
* 内置插件的用途说明(以各自官方 README 为准):用户视角讲清楚装了能
|
|
@@ -304,6 +382,8 @@ const BUILTIN_PLUGIN_DESCRIPTIONS = {
|
|
|
304
382
|
'插件市场:在 dsh 设置里逛社区插件,像应用商店一样点一下就装好',
|
|
305
383
|
'dsh-im':
|
|
306
384
|
'IM 桥核心:让 dsh 接入飞书等聊天工具——群内 @机器人 干活、收通知与审批卡片',
|
|
385
|
+
[PLUGIN_PACKAGE_NAME]:
|
|
386
|
+
'AgentOne 平台集成:飞书登录、平台模型、SkillHub 技能与 CLI 授权(本插件)',
|
|
307
387
|
};
|
|
308
388
|
|
|
309
389
|
/** 汇总 profile 插件状态:dependencies(去掉 in-box)+ 已装版本 + 激活态。 */
|
|
@@ -368,6 +448,7 @@ const BUILTIN_PLUGIN_ALLOW_BUILD = {
|
|
|
368
448
|
'dsh-harness-one': ['--allow-build=node-pty'],
|
|
369
449
|
dshmarket: ['--allow-build=node-pty'],
|
|
370
450
|
'dsh-im': [],
|
|
451
|
+
[PLUGIN_PACKAGE_NAME]: [],
|
|
371
452
|
};
|
|
372
453
|
|
|
373
454
|
/** 安装:白名单校验后转发(desktop 受管 pnpm / 官方 CLI;参数数组,无 shell)。
|
|
@@ -589,6 +670,10 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
|
|
|
589
670
|
});
|
|
590
671
|
log.info('[dsh-agentone] platform login stored (desktop callback server)');
|
|
591
672
|
await syncPlatformModel(config, log).catch(() => {});
|
|
673
|
+
await syncPlatformSkillhub(config, log, {
|
|
674
|
+
...(await loadCredentials(config)),
|
|
675
|
+
access_token: result.access_token,
|
|
676
|
+
}).catch(() => {});
|
|
592
677
|
await syncPlanSkills(config, log, {
|
|
593
678
|
...(await loadCredentials(config)),
|
|
594
679
|
access_token: result.access_token,
|
|
@@ -696,6 +781,13 @@ export function apply(ctx, config) {
|
|
|
696
781
|
error_code: credentials.model_error_code ?? null,
|
|
697
782
|
}
|
|
698
783
|
: null,
|
|
784
|
+
skillhub: credentials
|
|
785
|
+
? {
|
|
786
|
+
registry: credentials.skillhub_registry ?? null,
|
|
787
|
+
synced_at: credentials.skillhub_synced_at ?? null,
|
|
788
|
+
error: credentials.skillhub_error ?? null,
|
|
789
|
+
}
|
|
790
|
+
: null,
|
|
699
791
|
});
|
|
700
792
|
} catch (error) {
|
|
701
793
|
log.warn(`[dsh-agentone] status failed: ${error?.message || error}`);
|
|
@@ -768,8 +860,12 @@ export function apply(ctx, config) {
|
|
|
768
860
|
expires_at: expiresIn ? new Date(Date.now() + expiresIn * 1000).toISOString() : null,
|
|
769
861
|
});
|
|
770
862
|
log.info('[dsh-agentone] platform login stored');
|
|
771
|
-
// 登录即配置模型 + 同步套餐技能:失败不阻断登录,状态页展示原因
|
|
863
|
+
// 登录即配置模型 + SkillHub + 同步套餐技能:失败不阻断登录,状态页展示原因
|
|
772
864
|
await syncPlatformModel(config, log).catch(() => {});
|
|
865
|
+
await syncPlatformSkillhub(config, log, {
|
|
866
|
+
...(await loadCredentials(config)),
|
|
867
|
+
access_token: result.access_token,
|
|
868
|
+
}).catch(() => {});
|
|
773
869
|
await syncPlanSkills(config, log, {
|
|
774
870
|
...(await loadCredentials(config)),
|
|
775
871
|
access_token: result.access_token,
|
|
@@ -799,6 +895,25 @@ export function apply(ctx, config) {
|
|
|
799
895
|
}
|
|
800
896
|
});
|
|
801
897
|
|
|
898
|
+
register('/agentone/api/skillhub/sync', async (request, response) => {
|
|
899
|
+
if (request.method !== 'POST') {
|
|
900
|
+
response.writeHead(405, { allow: 'POST' });
|
|
901
|
+
response.end();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
const credentials = await ensureFreshToken(config, log);
|
|
906
|
+
if (!credentials) {
|
|
907
|
+
sendJson(response, 401, { error: '请先完成飞书登录' });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const result = await syncPlatformSkillhub(config, log, credentials);
|
|
911
|
+
sendJson(response, 200, { ok: true, registry: result.registryUrl });
|
|
912
|
+
} catch (error) {
|
|
913
|
+
sendJson(response, 502, { error: error?.message || '同步 SkillHub 配置失败' });
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
802
917
|
register('/agentone/api/skill/install', async (request, response) => {
|
|
803
918
|
if (request.method !== 'POST') {
|
|
804
919
|
response.writeHead(405, { allow: 'POST' });
|
|
@@ -1031,6 +1146,123 @@ export function apply(ctx, config) {
|
|
|
1031
1146
|
}
|
|
1032
1147
|
});
|
|
1033
1148
|
|
|
1149
|
+
// 进行中的 Device Flow:deviceCode 只存内存(进程级),complete 用后即删。
|
|
1150
|
+
// 与 pendingStates 同生命周期语义——插件重启后用户重新发起即可。
|
|
1151
|
+
const pendingCliAuth = new Map();
|
|
1152
|
+
|
|
1153
|
+
register('/agentone/api/cli/auth/begin', async (request, response) => {
|
|
1154
|
+
if (request.method !== 'POST') {
|
|
1155
|
+
response.writeHead(405, { allow: 'POST' });
|
|
1156
|
+
response.end();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
const body = await readJsonBody(request);
|
|
1161
|
+
const tool = String(body.tool || '');
|
|
1162
|
+
if (!CLI_TOOLS.has(tool)) {
|
|
1163
|
+
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1167
|
+
if (detail?.state === 'missing') {
|
|
1168
|
+
sendJson(response, 400, { error: 'CLI 未安装,请先安装或联系管理员' });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const raw = await beginCliAuth(tool);
|
|
1172
|
+
const parsed = parseCliAuthBeginOutput(raw);
|
|
1173
|
+
if (!parsed?.verificationUrl || !parsed?.deviceCode) {
|
|
1174
|
+
sendJson(response, 502, { error: '发起授权失败,请稍后重试' });
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
pendingCliAuth.set(tool, parsed.deviceCode);
|
|
1178
|
+
sendJson(response, 200, {
|
|
1179
|
+
ok: true,
|
|
1180
|
+
tool,
|
|
1181
|
+
verification_url: parsed.verificationUrl,
|
|
1182
|
+
user_code: parsed.userCode,
|
|
1183
|
+
expires_in: parsed.expiresIn,
|
|
1184
|
+
});
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
sendJson(response, 502, { error: error?.message || '发起授权失败,请稍后重试' });
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
register('/agentone/api/cli/auth/complete', async (request, response) => {
|
|
1191
|
+
if (request.method !== 'POST') {
|
|
1192
|
+
response.writeHead(405, { allow: 'POST' });
|
|
1193
|
+
response.end();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
const body = await readJsonBody(request);
|
|
1198
|
+
const tool = String(body.tool || '');
|
|
1199
|
+
if (!CLI_TOOLS.has(tool)) {
|
|
1200
|
+
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
// 只信 begin 存在本进程内存里的设备码,不接收前端回传——前端回环
|
|
1204
|
+
// 会让任意页面伪造 complete 调用。
|
|
1205
|
+
const deviceCode = pendingCliAuth.get(tool);
|
|
1206
|
+
if (!deviceCode) {
|
|
1207
|
+
sendJson(response, 400, { error: '授权会话不存在或已过期,请重新发起授权' });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
// 单次 complete 限时 25s:complete 是真长轮询(CLI 阻塞到用户同意
|
|
1212
|
+
// 或设备码过期),若不限时前端每轮询一次就叠一个子进程,几十个
|
|
1213
|
+
// lark-cli 同时轮同一码。这里 25s 一掐,前端串行重试(见 page.js)。
|
|
1214
|
+
await completeCliAuth(tool, deviceCode, { timeoutMs: 25_000 });
|
|
1215
|
+
pendingCliAuth.delete(tool);
|
|
1216
|
+
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1217
|
+
sendJson(response, 200, { ok: true, tool: detail });
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
// 超时=我们主动掐断轮询(用户还没在授权页同意):保留会话,202
|
|
1220
|
+
// 让前端继续等;只有终态(设备码失效/被拒)才删会话,否则用户
|
|
1221
|
+
// 慢一点授权就会因会话被删而 400「重新发起」。
|
|
1222
|
+
const message = String(error?.message || '');
|
|
1223
|
+
const terminal = /expired|invalid|denied|拒绝|失效|过期/i.test(message);
|
|
1224
|
+
if (terminal) pendingCliAuth.delete(tool);
|
|
1225
|
+
sendJson(response, terminal ? 502 : 202, {
|
|
1226
|
+
pending: !terminal,
|
|
1227
|
+
error: message,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
sendJson(response, 502, { error: error?.message || '授权确认失败,请稍后重试' });
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
register('/agentone/api/cli/auth/qrcode', async (request, response) => {
|
|
1236
|
+
if (request.method !== 'GET') {
|
|
1237
|
+
response.writeHead(405, { allow: 'GET' });
|
|
1238
|
+
response.end();
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
// 只为 begin 返回过的 URL 生成二维码:URL 来自本机 CLI 输出,但也不
|
|
1242
|
+
// 直接接受前端任意 URL(避免把本接口当任意二维码渲染器)。
|
|
1243
|
+
const queryUrl = new URL(request.url, 'http://localhost').searchParams.get('url') || '';
|
|
1244
|
+
const pending = [...pendingCliAuth.keys()];
|
|
1245
|
+
let matched = false;
|
|
1246
|
+
for (const tool of pending) {
|
|
1247
|
+
if (tool === 'lark-cli' && queryUrl) {
|
|
1248
|
+
matched = true;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (!matched) {
|
|
1253
|
+
sendJson(response, 400, { error: '没有进行中的授权会话' });
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
const ascii = await cliAuthQrcode(queryUrl);
|
|
1258
|
+
if (!ascii) throw new Error('二维码生成失败');
|
|
1259
|
+
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1260
|
+
response.end(ascii);
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
sendJson(response, 502, { error: error?.message || '二维码生成失败' });
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1034
1266
|
register('/agentone/api/plan', async (request, response) => {
|
|
1035
1267
|
if (request.method !== 'GET') {
|
|
1036
1268
|
response.writeHead(405, { allow: 'GET' });
|
|
@@ -1098,6 +1330,9 @@ export function apply(ctx, config) {
|
|
|
1098
1330
|
await removePlatformModel(config).catch((error) => {
|
|
1099
1331
|
log.warn(`[dsh-agentone] remove platform model failed: ${error?.message || error}`);
|
|
1100
1332
|
});
|
|
1333
|
+
await removePlatformSkillhub(config, log).catch((error) => {
|
|
1334
|
+
log.warn(`[dsh-agentone] remove platform skillhub failed: ${error?.message || error}`);
|
|
1335
|
+
});
|
|
1101
1336
|
sendJson(response, 200, { ok: true });
|
|
1102
1337
|
} catch (error) {
|
|
1103
1338
|
// 本地清理任何一步失败都明确报错(中文),不再静默挂起连接
|
|
@@ -1106,8 +1341,8 @@ export function apply(ctx, config) {
|
|
|
1106
1341
|
}
|
|
1107
1342
|
});
|
|
1108
1343
|
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1344
|
+
// 启动时若已有登录态:先保鲜令牌(临期轮换),再后台刷新模型配置、
|
|
1345
|
+
// SkillHub 配置与套餐技能。内置插件常驻检测与登录无关,独立先行。
|
|
1111
1346
|
ensureBuiltinPlugins(config, log).catch((error) => {
|
|
1112
1347
|
log.warn(`[dsh-agentone] builtin plugin check failed: ${error?.message || error}`);
|
|
1113
1348
|
});
|
|
@@ -1115,11 +1350,13 @@ export function apply(ctx, config) {
|
|
|
1115
1350
|
const credentials = await ensureFreshToken(config, log);
|
|
1116
1351
|
if (!credentials) return;
|
|
1117
1352
|
await syncPlatformModel(config, log).catch(() => {});
|
|
1353
|
+
await syncPlatformSkillhub(config, log, credentials).catch(() => {});
|
|
1118
1354
|
await syncPlanSkills(config, log, credentials).catch(() => {});
|
|
1119
1355
|
})();
|
|
1120
1356
|
|
|
1121
1357
|
return () => {
|
|
1122
1358
|
pendingStates.clear();
|
|
1359
|
+
pendingCliAuth.clear();
|
|
1123
1360
|
log.info('[dsh-agentone] routes unmounted');
|
|
1124
1361
|
};
|
|
1125
1362
|
}, 'dsh-agentone: http routes');
|
package/lib/model.js
CHANGED
|
@@ -21,8 +21,9 @@ const REASONING_EFFORTS = { off: null, low: 'low', medium: 'medium', high: 'high
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* 把平台 /models 条目转成 llm-pi-ai models 配置。
|
|
24
|
-
* input
|
|
25
|
-
*
|
|
24
|
+
* input(视觉)、reasoning(思考)与 contextWindow(上下文窗口)是平台
|
|
25
|
+
* 能力表下发的声明,插件只翻译:未声明的旧平台版本回退到全档位思考 +
|
|
26
|
+
* 纯文本 + 不写上下文(llm-pi-ai 回退自身 catalog 默认)。
|
|
26
27
|
*/
|
|
27
28
|
function platformModelEntry(item) {
|
|
28
29
|
const id = typeof item === 'string' ? item : item?.id;
|
|
@@ -30,6 +31,9 @@ function platformModelEntry(item) {
|
|
|
30
31
|
const entry = { id, name: friendlyModelName(id) };
|
|
31
32
|
const input = Array.isArray(item?.input) ? item.input : ['text'];
|
|
32
33
|
if (input.includes('image')) entry.input = ['text', 'image'];
|
|
34
|
+
if (typeof item?.contextWindow === 'number' && item.contextWindow > 0) {
|
|
35
|
+
entry.contextWindow = item.contextWindow;
|
|
36
|
+
}
|
|
33
37
|
if (item?.reasoning === false) entry.reasoningEfforts = false;
|
|
34
38
|
else entry.reasoningEfforts = { ...REASONING_EFFORTS };
|
|
35
39
|
return entry;
|
|
@@ -137,7 +141,13 @@ export async function fetchPlatformModels(platformUrl, accessToken) {
|
|
|
137
141
|
const data = await response.json().catch(() => ({}));
|
|
138
142
|
const models = Array.isArray(data.data)
|
|
139
143
|
? data.data
|
|
140
|
-
.map((item) =>
|
|
144
|
+
.map((item) =>
|
|
145
|
+
typeof item === 'string'
|
|
146
|
+
? item
|
|
147
|
+
: item?.id
|
|
148
|
+
? { id: item.id, input: item.input, reasoning: item.reasoning, contextWindow: item.contextWindow }
|
|
149
|
+
: null,
|
|
150
|
+
)
|
|
141
151
|
.filter(Boolean)
|
|
142
152
|
: [];
|
|
143
153
|
if (models.length === 0) throw new Error('平台返回的可用模型列表为空');
|
package/lib/page.js
CHANGED
|
@@ -69,6 +69,11 @@ button:disabled { opacity: .55; cursor: default; }
|
|
|
69
69
|
}
|
|
70
70
|
.banner.ok { background: var(--ok-weak); color: var(--ok); display: block; }
|
|
71
71
|
.banner.err { background: var(--err-weak); color: var(--err); display: block; }
|
|
72
|
+
/* CLI 授权弹窗遮罩:全屏半透明,点遮罩关闭 */
|
|
73
|
+
.modal-backdrop {
|
|
74
|
+
position: fixed; inset: 0; z-index: 50; display: block;
|
|
75
|
+
background: rgba(0, 0, 0, 0.45); padding: 0 12px; overflow-y: auto;
|
|
76
|
+
}
|
|
72
77
|
/* toast 常驻可视区顶部:页面滚动到底部时操作反馈仍可见 */
|
|
73
78
|
.toast-fixed { position: sticky; top: 8px; z-index: 10; }
|
|
74
79
|
/* 首屏骨架占位:慢网下 status 未回来前不至于一片空白 */
|
|
@@ -225,14 +230,14 @@ $('cli-recheck-btn').onclick = () => {
|
|
|
225
230
|
document.querySelectorAll('.tab').forEach((tab) => {
|
|
226
231
|
tab.onclick = () => activateTab(tab.dataset.tab);
|
|
227
232
|
});
|
|
228
|
-
const skillAction = (slug, action, btn) => withLoading(
|
|
233
|
+
const skillAction = (slug, action, btn, successText) => withLoading(
|
|
229
234
|
btn,
|
|
230
235
|
() => fetch('api/skill/' + action, {
|
|
231
236
|
method: 'POST',
|
|
232
237
|
headers: { 'Content-Type': 'application/json' },
|
|
233
238
|
body: JSON.stringify({ slug }),
|
|
234
239
|
}),
|
|
235
|
-
action === 'install' ? '技能已安装' : '技能已卸载',
|
|
240
|
+
successText || (action === 'install' ? '技能已安装' : '技能已卸载'),
|
|
236
241
|
);
|
|
237
242
|
const PLAN_CAPABILITY_LABELS = {
|
|
238
243
|
image_understanding_enabled: '图片理解',
|
|
@@ -360,12 +365,21 @@ const renderSkills = (s) => {
|
|
|
360
365
|
}
|
|
361
366
|
const rows = list.map((skill) => {
|
|
362
367
|
const tag = skill.source === 'plan' ? '<span class="tag">套餐</span>' : '';
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
? '<
|
|
368
|
-
:
|
|
368
|
+
// 可升级:平台对比 latest 指纹/版本后给的结论,提示「v旧 → v新」;
|
|
369
|
+
// latest 查询失败时平台降级 false,这里不额外猜测
|
|
370
|
+
const upgradable = skill.installed && skill.upgrade_available;
|
|
371
|
+
const meta = !skill.installed
|
|
372
|
+
? '<span class="muted">未安装</span>'
|
|
373
|
+
: upgradable
|
|
374
|
+
? '已装 ' + esc(skill.installed_version || '')
|
|
375
|
+
+ ' → <b>v' + esc(skill.latest_version || '新版本') + '</b>'
|
|
376
|
+
: '已装 ' + esc(skill.installed_version || '');
|
|
377
|
+
const action = !skill.installed
|
|
378
|
+
? '<button class="btn small primary" data-install="' + esc(skill.canonical_slug) + '">安装</button>'
|
|
379
|
+
: upgradable
|
|
380
|
+
? '<button class="btn small primary" data-upgrade="' + esc(skill.canonical_slug) + '">升级</button>'
|
|
381
|
+
+ '<button class="btn small danger" data-uninstall="' + esc(skill.canonical_slug) + '">卸载</button>'
|
|
382
|
+
: '<button class="btn small danger" data-uninstall="' + esc(skill.canonical_slug) + '">卸载</button>';
|
|
369
383
|
return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
|
|
370
384
|
+ esc(displayName(skill.canonical_slug)) + tag + '</div><div class="skill-sub">'
|
|
371
385
|
+ esc(skill.canonical_slug) + ' · ' + meta + '</div></div>' + action + '</div>';
|
|
@@ -375,10 +389,12 @@ const renderSkills = (s) => {
|
|
|
375
389
|
if (s.skills_synced_at) metaLine.push('自动同步:' + new Date(s.skills_synced_at).toLocaleString());
|
|
376
390
|
if (s.skills_error) metaLine.push('<span style="color:var(--err)">' + esc(s.skills_error) + '</span>');
|
|
377
391
|
const metaHtml = metaLine.join(' · ');
|
|
378
|
-
// 5s 轮询会反复调用本函数:指纹(slug
|
|
379
|
-
// innerHTML
|
|
392
|
+
// 5s 轮询会反复调用本函数:指纹(slug:装否:版本:可升级 + meta 行)没变
|
|
393
|
+
// 时跳过 innerHTML 重写,避免替换按钮节点吞掉正在进行的点击、打断卸载二
|
|
394
|
+
// 次确认
|
|
380
395
|
const fingerprint = list.map((skill) =>
|
|
381
|
-
skill.canonical_slug + ':' + (skill.installed ? 1 : 0) + ':' + (skill.installed_version || '')
|
|
396
|
+
skill.canonical_slug + ':' + (skill.installed ? 1 : 0) + ':' + (skill.installed_version || '')
|
|
397
|
+
+ ':' + (skill.upgrade_available ? 1 : 0),
|
|
382
398
|
).join('|') + '#' + metaHtml;
|
|
383
399
|
if ($('skills-list').dataset.fingerprint !== fingerprint) {
|
|
384
400
|
$('skills-list').dataset.fingerprint = fingerprint;
|
|
@@ -388,6 +404,11 @@ const renderSkills = (s) => {
|
|
|
388
404
|
btn.dataset.loading = '安装中…';
|
|
389
405
|
btn.onclick = () => skillAction(btn.dataset.install, 'install', btn);
|
|
390
406
|
});
|
|
407
|
+
// 升级即覆盖式重装(staging 原子替换旧目录),成功提示与安装同口径
|
|
408
|
+
$('skills-list').querySelectorAll('[data-upgrade]').forEach((btn) => {
|
|
409
|
+
btn.dataset.loading = '升级中…';
|
|
410
|
+
btn.onclick = () => skillAction(btn.dataset.upgrade, 'install', btn, '技能已升级');
|
|
411
|
+
});
|
|
391
412
|
$('skills-list').querySelectorAll('[data-uninstall]').forEach((btn) => {
|
|
392
413
|
btn.dataset.loading = '卸载中…';
|
|
393
414
|
btn.onclick = () => confirmThen(btn, '确认卸载?', () =>
|
|
@@ -459,19 +480,139 @@ const renderCliData = (s, tools) => {
|
|
|
459
480
|
? ' → <b>v' + esc(info.latest) + '</b>'
|
|
460
481
|
: '');
|
|
461
482
|
const upgradable = info.latest && info.latest !== info.version;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
483
|
+
// 已装未授权 → 「去授权」:发起官方 Device Flow(扫码/链接 + 轮询确认)
|
|
484
|
+
const actions = [
|
|
485
|
+
state === 'unauthorized'
|
|
486
|
+
? '<button class="btn small primary" data-cli-auth="' + esc(name) + '">去授权</button>'
|
|
487
|
+
: '',
|
|
488
|
+
upgradable
|
|
489
|
+
? '<button class="btn small ghost" data-cli-upgrade="' + esc(name) + '">升级</button>'
|
|
490
|
+
: '',
|
|
491
|
+
].join('');
|
|
465
492
|
return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
|
|
466
493
|
+ esc(name) + status + '</div><div class="skill-sub">'
|
|
467
|
-
+ versionLine + '</div></div><div>' + actions + '</div></div>';
|
|
494
|
+
+ versionLine + '</div></div><div style="display:flex;gap:6px">' + actions + '</div></div>';
|
|
468
495
|
});
|
|
469
496
|
body.innerHTML = rows.join('')
|
|
470
497
|
+ '<div class="hint">在对话中直接使用(如 /lark 帮助),由 dsh 自动调用。</div>'
|
|
471
|
-
+ '<div class="hint"
|
|
498
|
+
+ '<div class="hint">未安装时请联系管理员;已安装未授权时点「去授权」用飞书扫码完成即可。</div>';
|
|
472
499
|
body.querySelectorAll('[data-cli-upgrade]').forEach((btn) => {
|
|
473
500
|
btn.onclick = () => cliUpgrade(btn.dataset.cliUpgrade, btn);
|
|
474
501
|
});
|
|
502
|
+
body.querySelectorAll('[data-cli-auth]').forEach((btn) => {
|
|
503
|
+
btn.onclick = () => cliAuthDialog(btn.dataset.cliAuth);
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* CLI 授权弹窗:begin 拿授权链接与 user_code → 展示链接 + ASCII 二维码 →
|
|
509
|
+
* 轮询 complete(用户在授权页同意后 CLI 落盘 token)→ 成功刷新面板。
|
|
510
|
+
* 弹窗关闭即停轮询;设备码只存插件后端内存,前端不回传。
|
|
511
|
+
*/
|
|
512
|
+
const cliAuthDialog = async (tool) => {
|
|
513
|
+
const toolLabel = CLI_TOOL_META[tool]?.label || tool;
|
|
514
|
+
const overlay = document.createElement('div');
|
|
515
|
+
overlay.className = 'modal-backdrop';
|
|
516
|
+
overlay.innerHTML = '<div class="card" style="max-width:420px;margin:8% auto;text-align:left">'
|
|
517
|
+
+ '<h3 style="margin:0 0 6px">授权 ' + esc(tool) + '</h3>'
|
|
518
|
+
+ '<div id="cli-auth-status" class="skill-sub">正在发起授权…</div>'
|
|
519
|
+
+ '<pre id="cli-auth-qr" style="display:none;background:var(--card,#fff);color:#111;'
|
|
520
|
+
+ 'border-radius:10px;padding:8px;font:11px/1.15 ui-monospace,monospace;overflow:auto;margin:10px 0"></pre>'
|
|
521
|
+
+ '<div id="cli-auth-link"></div>'
|
|
522
|
+
+ '<div class="row" style="margin-top:12px;justify-content:flex-end">'
|
|
523
|
+
+ '<button id="cli-auth-open" class="btn small primary" hidden>打开授权页</button>'
|
|
524
|
+
+ '<button id="cli-auth-copy" class="btn small ghost" hidden>复制验证码</button>'
|
|
525
|
+
+ '<button id="cli-auth-close" class="btn small ghost">关闭</button></div></div>';
|
|
526
|
+
document.body.appendChild(overlay);
|
|
527
|
+
let pollTimer = null;
|
|
528
|
+
let expiryTimer = null;
|
|
529
|
+
let closed = false;
|
|
530
|
+
const statusEl = overlay.querySelector('#cli-auth-status');
|
|
531
|
+
const close = () => {
|
|
532
|
+
closed = true;
|
|
533
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
534
|
+
if (expiryTimer) clearTimeout(expiryTimer);
|
|
535
|
+
overlay.remove();
|
|
536
|
+
};
|
|
537
|
+
overlay.querySelector('#cli-auth-close').onclick = close;
|
|
538
|
+
overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); });
|
|
539
|
+
try {
|
|
540
|
+
const resp = await fetch('api/cli/auth/begin', {
|
|
541
|
+
method: 'POST',
|
|
542
|
+
headers: { 'Content-Type': 'application/json' },
|
|
543
|
+
body: JSON.stringify({ tool }),
|
|
544
|
+
});
|
|
545
|
+
const data = await resp.json().catch(() => ({}));
|
|
546
|
+
if (!resp.ok) throw new Error(data.error || '发起授权失败');
|
|
547
|
+
statusEl.textContent = '用' + esc(toolLabel) + '扫二维码,或打开链接完成授权。同意后此处会自动确认。';
|
|
548
|
+
// ASCII 二维码(lark-cli 本机生成;失败退化为纯链接)
|
|
549
|
+
fetch('api/cli/auth/qrcode?url=' + encodeURIComponent(data.verification_url))
|
|
550
|
+
.then((r) => (r.ok ? r.text() : ''))
|
|
551
|
+
.then((ascii) => {
|
|
552
|
+
const qr = overlay.querySelector('#cli-auth-qr');
|
|
553
|
+
if (ascii) {
|
|
554
|
+
qr.textContent = ascii;
|
|
555
|
+
qr.style.display = 'block';
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
.catch(() => {});
|
|
559
|
+
const openBtn = overlay.querySelector('#cli-auth-open');
|
|
560
|
+
openBtn.hidden = false;
|
|
561
|
+
openBtn.onclick = () => window.open(data.verification_url, '_blank');
|
|
562
|
+
const copyBtn = overlay.querySelector('#cli-auth-copy');
|
|
563
|
+
if (data.user_code) {
|
|
564
|
+
copyBtn.hidden = false;
|
|
565
|
+
copyBtn.onclick = async () => {
|
|
566
|
+
try {
|
|
567
|
+
await navigator.clipboard.writeText(data.user_code);
|
|
568
|
+
copyBtn.textContent = '已复制';
|
|
569
|
+
} catch {
|
|
570
|
+
copyBtn.textContent = data.user_code;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
overlay.querySelector('#cli-auth-link').innerHTML
|
|
574
|
+
= '<div class="skill-sub">验证码:<b>' + esc(data.user_code) + '</b></div>';
|
|
575
|
+
}
|
|
576
|
+
// 自调度串行轮询:上一次 complete 返回后才发起下一次(setInterval 会在
|
|
577
|
+
// 长轮询未返回时叠加并发子进程)。complete 单次后端限时 25s:
|
|
578
|
+
// 200 → 授权成功;202 → 仍在等待,3s 后重试;其它 → 终态错误停止。
|
|
579
|
+
const pollComplete = async () => {
|
|
580
|
+
if (closed) return;
|
|
581
|
+
try {
|
|
582
|
+
const r = await fetch('api/cli/auth/complete', {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
headers: { 'Content-Type': 'application/json' },
|
|
585
|
+
body: JSON.stringify({ tool }),
|
|
586
|
+
});
|
|
587
|
+
const d = await r.json().catch(() => ({}));
|
|
588
|
+
if (r.ok && d.ok) {
|
|
589
|
+
statusEl.textContent = '✓ 授权成功';
|
|
590
|
+
setTimeout(close, 800);
|
|
591
|
+
if (d.tool) renderCliData(lastStatus, { [tool]: d.tool });
|
|
592
|
+
else renderCli(lastStatus);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (r.status === 202 && d.pending !== false) {
|
|
596
|
+
statusEl.textContent = '正在等待你在授权页完成同意…';
|
|
597
|
+
pollTimer = setTimeout(pollComplete, 3000);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
statusEl.textContent = d.error || '授权失败,请重新发起';
|
|
601
|
+
} catch {
|
|
602
|
+
// 网络抖动继续轮询
|
|
603
|
+
pollTimer = setTimeout(pollComplete, 3000);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
pollTimer = setTimeout(pollComplete, 3000);
|
|
607
|
+
const expiresInSeconds = Number(data.expires_in) || 600;
|
|
608
|
+
expiryTimer = setTimeout(() => {
|
|
609
|
+
closed = true;
|
|
610
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
611
|
+
statusEl.textContent = '授权超时,请关闭后重试。';
|
|
612
|
+
}, Math.max(expiresInSeconds, 60) * 1000);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
statusEl.textContent = error.message || '发起授权失败';
|
|
615
|
+
}
|
|
475
616
|
};
|
|
476
617
|
|
|
477
618
|
/** CLI 一键升级:官方 tool update(ccpg 下载二进制可达分钟级)。 */
|
|
@@ -757,6 +898,16 @@ const refresh = async () => {
|
|
|
757
898
|
const gotoPlan = $('goto-plan-btn');
|
|
758
899
|
if (gotoPlan) gotoPlan.onclick = () => activateTab('plan');
|
|
759
900
|
}
|
|
901
|
+
if (s.logged_in && s.skillhub) {
|
|
902
|
+
const sh = s.skillhub;
|
|
903
|
+
const lines = [];
|
|
904
|
+
if (sh.registry) lines.push('<div class="kv"><span class="k">Registry</span><span class="v">' + esc(sh.registry) + '</span></div>');
|
|
905
|
+
else lines.push('<div class="kv"><span class="k">SkillHub</span><span class="v muted">未同步</span></div>');
|
|
906
|
+
if (sh.synced_at) lines.push('<div class="kv"><span class="k">上次同步</span><span class="v">' + new Date(sh.synced_at).toLocaleString() + '</span></div>');
|
|
907
|
+
if (sh.error) lines.push('<div class="kv"><span class="k">同步状态</span><span class="v" style="color:var(--err)">' + esc(sh.error) + '</span></div>');
|
|
908
|
+
lines.push('<div class="kv"><span class="k">环境变量</span><span class="v muted">已写入 agentone/skillhub.env(0600)</span></div>');
|
|
909
|
+
$('skillhub-body').innerHTML = lines.join('');
|
|
910
|
+
}
|
|
760
911
|
renderSkills(s);
|
|
761
912
|
renderAbout(s);
|
|
762
913
|
};
|
|
@@ -788,6 +939,11 @@ $('sync-btn').onclick = () => withLoading(
|
|
|
788
939
|
() => fetch('api/model/sync', { method: 'POST' }),
|
|
789
940
|
'平台模型已同步',
|
|
790
941
|
);
|
|
942
|
+
$('skillhub-sync-btn').onclick = () => withLoading(
|
|
943
|
+
$('skillhub-sync-btn'),
|
|
944
|
+
() => fetch('api/skillhub/sync', { method: 'POST' }),
|
|
945
|
+
'SkillHub 配置已同步',
|
|
946
|
+
);
|
|
791
947
|
logoutBtn.onclick = () => withLoading(
|
|
792
948
|
logoutBtn,
|
|
793
949
|
() => fetch('api/logout', { method: 'POST' }),
|
|
@@ -856,6 +1012,13 @@ export function statusPageHtml() {
|
|
|
856
1012
|
<button id="sync-btn" class="btn ghost small" data-loading="同步中…">重新同步模型</button>
|
|
857
1013
|
</div>
|
|
858
1014
|
</div>
|
|
1015
|
+
<div class="card">
|
|
1016
|
+
<h3>SkillHub 连接 <span class="muted">技能市场与 clawhub CLI 认证</span></h3>
|
|
1017
|
+
<div id="skillhub-body"></div>
|
|
1018
|
+
<div class="row" style="margin-top:10px">
|
|
1019
|
+
<button id="skillhub-sync-btn" class="btn ghost small" data-loading="同步中…">重新获取 SkillHub 配置</button>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
859
1022
|
</div>
|
|
860
1023
|
|
|
861
1024
|
<div class="tabpane" id="pane-plan" hidden>
|
package/lib/skillhub.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// 平台 SkillHub 连接配置自动落地:对齐平台托管容器的注入语义。
|
|
2
|
+
//
|
|
3
|
+
// 平台侧(app/executor/docker_executor.py start_gateway)给托管容器注入
|
|
4
|
+
// SKILLHUB_BASE_URL / SKILLHUB_TOKEN + CLAWHUB_* 兼容别名,hermes 另写
|
|
5
|
+
// /opt/data/.env 供 Gateway 子进程读取。独立/桌面版 dsh 通过本模块拿到
|
|
6
|
+
// 同等配置:
|
|
7
|
+
// 1. fetchSkillhubConfig 拉平台 /api/plugin/v1/skillhub/config
|
|
8
|
+
// (registry + 用户级个人 token,平台不下发默认/admin token);
|
|
9
|
+
// 2. applyPlatformSkillhub 写 $DSH_HOME/agentone/skillhub.env(0600,
|
|
10
|
+
// 4 个变量与平台同名单)——与托管容器 .env 对等的落盘形态;
|
|
11
|
+
// 3. clawhubLogin 本机有 clawhub CLI 时写入登录态(与平台容器
|
|
12
|
+
// 内安装命令同源:clawhub login --token … --registry …);无 clawhub
|
|
13
|
+
// 静默跳过。
|
|
14
|
+
// token 明文只进 0600 的 env 文件与 credentials.json(本就 owner-only)。
|
|
15
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { platformError, platformFetch } from './http.js';
|
|
19
|
+
import { runCliTool } from './proc.js';
|
|
20
|
+
import { resolveDshHome } from './store.js';
|
|
21
|
+
|
|
22
|
+
/** clawhub login/logout 超时:登录要打 registry,网络慢时放宽。 */
|
|
23
|
+
const CLAWHUB_TIMEOUT_MS = 60_000;
|
|
24
|
+
|
|
25
|
+
/** SkillHub token 形态校验:与平台 _is_skillhub_token 同口径(sk_ / sk- 前缀)。 */
|
|
26
|
+
export function isSkillhubToken(token) {
|
|
27
|
+
return typeof token === 'string' && /^(sk_|sk-)/.test(token);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function skillhubEnvPath(config) {
|
|
31
|
+
return join(resolveDshHome(config), 'agentone', 'skillhub.env');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 生成 skillhub.env 文本:标准变量 + 兼容别名,与平台容器注入同名单。 */
|
|
35
|
+
export function buildSkillhubEnv({ registryUrl, token }) {
|
|
36
|
+
return [
|
|
37
|
+
'# AgentOne 平台 SkillHub 配置(插件自动生成,登出时删除)',
|
|
38
|
+
`SKILLHUB_BASE_URL=${registryUrl}`,
|
|
39
|
+
`SKILLHUB_TOKEN=${token}`,
|
|
40
|
+
`CLAWHUB_REGISTRY=${registryUrl}`,
|
|
41
|
+
`CLAWHUB_TOKEN=${token}`,
|
|
42
|
+
'',
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 拉平台 SkillHub 连接配置;enabled=false 或失败时返回 { enabled:false }。 */
|
|
47
|
+
export async function fetchSkillhubConfig(platformUrl, accessToken) {
|
|
48
|
+
const response = await platformFetch(
|
|
49
|
+
`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/skillhub/config`,
|
|
50
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } },
|
|
51
|
+
);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw await platformError(response, '获取 SkillHub 配置失败');
|
|
54
|
+
}
|
|
55
|
+
return response.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeFileAtomic(path, text, mode) {
|
|
59
|
+
await mkdir(join(path, '..'), { recursive: true, mode: 0o700 });
|
|
60
|
+
const tmp = `${path}.agentone-${process.pid}-${Date.now()}.tmp`;
|
|
61
|
+
await writeFile(tmp, text, { mode });
|
|
62
|
+
await rename(tmp, path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 拉取并落地 SkillHub 配置(env 文件 + clawhub 登录态)。返回
|
|
67
|
+
* { registryUrl, token }。失败抛错(中文 message),由调用方记录到
|
|
68
|
+
* credentials 的 skillhub_error,不阻断登录/启动。
|
|
69
|
+
*/
|
|
70
|
+
export async function applyPlatformSkillhub(config, platformUrl, accessToken) {
|
|
71
|
+
const data = await fetchSkillhubConfig(platformUrl, accessToken);
|
|
72
|
+
if (!data?.enabled) {
|
|
73
|
+
throw new Error('平台未启用 SkillHub');
|
|
74
|
+
}
|
|
75
|
+
const registryUrl = String(data.registry_url || '').replace(/\/+$/, '');
|
|
76
|
+
const token = String(data.token || '');
|
|
77
|
+
if (!registryUrl || !isSkillhubToken(token)) {
|
|
78
|
+
throw new Error('平台返回的 SkillHub 配置不完整');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await writeFileAtomic(skillhubEnvPath(config), buildSkillhubEnv({ registryUrl, token }), 0o600);
|
|
82
|
+
// 插件自身进程 env 也设置一份:插件派生的子进程直接继承
|
|
83
|
+
process.env.SKILLHUB_BASE_URL = registryUrl;
|
|
84
|
+
process.env.SKILLHUB_TOKEN = token;
|
|
85
|
+
process.env.CLAWHUB_REGISTRY = registryUrl;
|
|
86
|
+
process.env.CLAWHUB_TOKEN = token;
|
|
87
|
+
|
|
88
|
+
await clawhubLogin(registryUrl, token).catch(() => {});
|
|
89
|
+
return { registryUrl, token };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 本机 clawhub 写登录态;未安装返回 false(静默跳过),其余错误上抛由调用方容错。 */
|
|
93
|
+
export async function clawhubLogin(registryUrl, token) {
|
|
94
|
+
try {
|
|
95
|
+
await runCliTool(
|
|
96
|
+
'clawhub',
|
|
97
|
+
['login', '--token', token, '--registry', registryUrl],
|
|
98
|
+
{ timeoutMs: CLAWHUB_TIMEOUT_MS },
|
|
99
|
+
);
|
|
100
|
+
return true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (/未找到 clawhub 命令/.test(error?.message || '')) return false;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 登出清理:删 env 文件、清进程 env、尽力 clawhub logout。 */
|
|
108
|
+
export async function removePlatformSkillhub(config, log) {
|
|
109
|
+
await rm(skillhubEnvPath(config), { force: true }).catch(() => {});
|
|
110
|
+
for (const key of ['SKILLHUB_BASE_URL', 'SKILLHUB_TOKEN', 'CLAWHUB_REGISTRY', 'CLAWHUB_TOKEN']) {
|
|
111
|
+
delete process.env[key];
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await runCliTool('clawhub', ['logout'], { timeoutMs: CLAWHUB_TIMEOUT_MS });
|
|
115
|
+
} catch {
|
|
116
|
+
// 无 clawhub 或登出失败都不阻断登出
|
|
117
|
+
}
|
|
118
|
+
if (log) log.info('[dsh-agentone] local skillhub config removed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 读取 env 文件内容(测试与状态展示用;文件不存在返回 null)。 */
|
|
122
|
+
export async function readSkillhubEnv(config) {
|
|
123
|
+
try {
|
|
124
|
+
const text = await readFile(skillhubEnvPath(config), 'utf8');
|
|
125
|
+
await chmod(skillhubEnvPath(config), 0o600).catch(() => {});
|
|
126
|
+
return text;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "AgentOne 平台集成插件:飞书登录、平台模型、SkillHub
|
|
3
|
+
"version": "0.5.17",
|
|
4
|
+
"description": "AgentOne 平台集成插件:飞书登录、平台模型、SkillHub 技能(含升级检测与一键升级)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错、状态页 UX 增强)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|