dsh-agentone 0.5.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 +104 -0
- package/cordis.patch.yml +5 -0
- package/lib/auth.js +111 -0
- package/lib/client.js +218 -0
- package/lib/http.js +41 -0
- package/lib/index.js +895 -0
- package/lib/model.js +200 -0
- package/lib/page.js +857 -0
- package/lib/proc.js +113 -0
- package/lib/skill.js +132 -0
- package/lib/store.js +60 -0
- package/package.json +41 -0
package/lib/proc.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// dsh plugin CLI 转发器:唯一的外部进程调用点。
|
|
2
|
+
//
|
|
3
|
+
// 官方 `dsh plugin --profile web <args>` 内部就是 pnpm 转发 + profile
|
|
4
|
+
// 初始化 + dsh.profile.bundles reconcile,全部复用。这里只封装进程执行
|
|
5
|
+
// (execFile 参数数组、绝不经过 shell)与错误翻译;包名/版本白名单在
|
|
6
|
+
// index.js 的调用侧执行。
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
/** 从 pnpm 冗长输出里提取对用户有意义的错误行。 */
|
|
13
|
+
function firstErrorLines(raw) {
|
|
14
|
+
const lines = String(raw).split('\n').map((line) => line.trim()).filter(Boolean);
|
|
15
|
+
const meaningful = lines.filter(
|
|
16
|
+
(line) => /ERR_|error|ERROR|404|ENOTFOUND|ETIMEDOUT|ECONNREFUSED/.test(line)
|
|
17
|
+
&& !/^✓|^Done|Progress/.test(line),
|
|
18
|
+
);
|
|
19
|
+
const picked = (meaningful.length ? meaningful : lines).slice(0, 3).join(';');
|
|
20
|
+
return picked.slice(0, 300);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 运行一次性命令。webServer 注册的处理器是同步 Node handler 场景下的
|
|
25
|
+
* async 函数;execFile 不经过 shell,参数数组中的每个元素都是独立参数,
|
|
26
|
+
* 不存在拼接解释执行。
|
|
27
|
+
*/
|
|
28
|
+
export async function runCliTool(tool, args, { timeoutMs = 300000 } = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const { stdout, stderr } = await execFileAsync(tool, args, {
|
|
31
|
+
timeout: timeoutMs,
|
|
32
|
+
killSignal: 'SIGKILL',
|
|
33
|
+
env: process.env,
|
|
34
|
+
});
|
|
35
|
+
return { stdout, stderr };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT') {
|
|
38
|
+
throw new Error(`未找到 ${tool} 命令,请确认已安装`);
|
|
39
|
+
}
|
|
40
|
+
if (error.killed) {
|
|
41
|
+
throw new Error('命令执行超时,请稍后重试');
|
|
42
|
+
}
|
|
43
|
+
const message = firstErrorLines(`${error.stderr || ''}\n${error.stdout || ''}`);
|
|
44
|
+
throw new Error(message || `命令执行失败(退出码 ${error.code ?? '未知'})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** dsh plugin 子命令:转发给官方 CLI。 */
|
|
49
|
+
export function dshPluginCli(args) {
|
|
50
|
+
return runCliTool('dsh', ['plugin', '--profile', 'web', ...args]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** pnpm 子命令(当前仅 outdated 查询)。 */
|
|
54
|
+
export function pnpmCli(args, timeoutMs) {
|
|
55
|
+
return runCliTool('pnpm', args, { timeoutMs });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 探测本机 CLI 工具状态。返回 'missing'(未安装)/ 'authorized' /
|
|
60
|
+
* 'unauthorized'(已装未授权)。与平台 instance_service 的判定口径一致:
|
|
61
|
+
* lark-cli auth status → identities.{user-default,user}.tokenStatus
|
|
62
|
+
* ∈ {valid, needs_refresh} 或 status=ready + available=true
|
|
63
|
+
* ccpg-cli CCPG_FULL_CLI=1 auth status → 顶层 tokenStatus
|
|
64
|
+
* ∈ {valid, needs_refresh}(access 过期但 refresh 可用仍算已授权)
|
|
65
|
+
*/
|
|
66
|
+
export async function probeCliTool(tool, { timeoutMs = 15000 } = {}) {
|
|
67
|
+
let stdout;
|
|
68
|
+
try {
|
|
69
|
+
({ stdout } = await execFileAsync(tool, ['auth', 'status'], {
|
|
70
|
+
timeout: timeoutMs,
|
|
71
|
+
killSignal: 'SIGKILL',
|
|
72
|
+
env: tool === 'ccpg-cli' ? { ...process.env, CCPG_FULL_CLI: '1' } : process.env,
|
|
73
|
+
}));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === 'ENOENT') return 'missing';
|
|
76
|
+
// 探测失败不区分超时/非零退出:统一按已装但状态未知处理
|
|
77
|
+
stdout = `${error.stdout || ''}`;
|
|
78
|
+
}
|
|
79
|
+
return larkStyleAuthorized(stdout) || ccpgStyleAuthorized(stdout) ? 'authorized' : 'unauthorized';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** lark-cli 形态:identities 下的 tokenStatus 或新版 status/available 组合。 */
|
|
83
|
+
function larkStyleAuthorized(raw) {
|
|
84
|
+
const data = parseJsonObject(raw);
|
|
85
|
+
const identities = data?.identities;
|
|
86
|
+
if (!identities || typeof identities !== 'object') return false;
|
|
87
|
+
for (const key of ['user-default', 'user']) {
|
|
88
|
+
const identity = identities[key];
|
|
89
|
+
if (!identity || typeof identity !== 'object') continue;
|
|
90
|
+
if (['valid', 'needs_refresh'].includes(identity.tokenStatus)) return true;
|
|
91
|
+
if (identity.status === 'ready' && identity.available === true) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** ccpg-cli 形态:顶层 tokenStatus;lark-cli 的 _authList 兜底同判。 */
|
|
97
|
+
function ccpgStyleAuthorized(raw) {
|
|
98
|
+
const data = parseJsonObject(raw);
|
|
99
|
+
if (!data) return false;
|
|
100
|
+
if (['valid', 'needs_refresh'].includes(data.tokenStatus)) return true;
|
|
101
|
+
return Array.isArray(data._authList) && data._authList.some(
|
|
102
|
+
(item) => item && ['valid', 'needs_refresh'].includes(item.tokenStatus),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseJsonObject(raw) {
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(String(raw).trim());
|
|
109
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/lib/skill.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// dsh-agentone 技能同步(M3):平台 /api/plugin/v1/skills 用户级通道。
|
|
2
|
+
//
|
|
3
|
+
// 平台代理下载(SkillHub token 不出平台),本插件负责:
|
|
4
|
+
// 1. listPlatformSkills 拉套餐技能 + 台账安装状态;
|
|
5
|
+
// 2. installPlatformSkill 下载 zip → 解包到 $DSH_HOME/skills/<ns--slug>/
|
|
6
|
+
// (目录名保留 ns--slug 原样,见平台侧 namespaced-dir 约定)→ 回报
|
|
7
|
+
// 版本指纹(X-Skill-* 响应头);
|
|
8
|
+
// 3. removePlatformSkill 卸载(平台删台账 + 本机删目录)。
|
|
9
|
+
// 解包用 fflate(纯 JS 无原生依赖);zip 条目名做穿越清洗,落盘先写临时
|
|
10
|
+
// 目录再原子改名,失败不留半成品。
|
|
11
|
+
import { mkdir, rm, writeFile, rename } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { unzipSync } from 'fflate';
|
|
14
|
+
|
|
15
|
+
import { platformError, platformFetch } from './http.js';
|
|
16
|
+
import { resolveDshHome } from './store.js';
|
|
17
|
+
|
|
18
|
+
/** 技能包下载超时:zip 体积可达数十 MB,比普通 JSON 调用放宽。 */
|
|
19
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
20
|
+
|
|
21
|
+
function skillsRoot(config) {
|
|
22
|
+
return join(resolveDshHome(config), 'skills');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function skillDir(config, canonicalSlug) {
|
|
26
|
+
return join(skillsRoot(config), canonicalSlug);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function listPlatformSkills(platformUrl, accessToken) {
|
|
30
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/skills`, {
|
|
31
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) throw await platformError(response, '获取平台技能列表失败');
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** zip 条目名清洗:拒绝绝对路径/上跳/空名,返回安全相对路径。 */
|
|
38
|
+
function safeEntryName(name) {
|
|
39
|
+
const normalized = String(name || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
40
|
+
if (!normalized || normalized.includes('../') || normalized === '..') return null;
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 拆技能标识:含 `--` 拆 ns/slug,否则按平台兼容规则归 global 命名空间。 */
|
|
45
|
+
export function splitSkillSlug(canonicalSlug) {
|
|
46
|
+
const separator = canonicalSlug.indexOf('--');
|
|
47
|
+
if (separator > 0) {
|
|
48
|
+
return {
|
|
49
|
+
namespace: canonicalSlug.slice(0, separator),
|
|
50
|
+
slug: canonicalSlug.slice(separator + 2),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { namespace: 'global', slug: canonicalSlug };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function installPlatformSkill(platformUrl, accessToken, canonicalSlug, options = {}) {
|
|
57
|
+
const base = platformUrl.replace(/\/+$/, '');
|
|
58
|
+
const { namespace, slug } = splitSkillSlug(canonicalSlug);
|
|
59
|
+
if (!slug) throw new Error(`技能标识格式不正确:${canonicalSlug}`);
|
|
60
|
+
|
|
61
|
+
const downloadResponse = await platformFetch(
|
|
62
|
+
`${base}/api/plugin/v1/skills/${namespace}/${slug}/download`,
|
|
63
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } },
|
|
64
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
65
|
+
);
|
|
66
|
+
if (!downloadResponse.ok) {
|
|
67
|
+
throw await platformError(downloadResponse, `技能包下载失败(HTTP ${downloadResponse.status})`);
|
|
68
|
+
}
|
|
69
|
+
const version = downloadResponse.headers.get('x-skill-version') || '';
|
|
70
|
+
const fingerprint = downloadResponse.headers.get('x-skill-fingerprint') || '';
|
|
71
|
+
if (!version || !fingerprint) throw new Error('技能包响应缺少版本信息');
|
|
72
|
+
|
|
73
|
+
const archive = new Uint8Array(await downloadResponse.arrayBuffer());
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = unzipSync(archive);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error('技能包不是有效的 zip 文件');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const root = skillsRoot(options.config || {});
|
|
82
|
+
const finalDir = join(root, canonicalSlug);
|
|
83
|
+
const stagingDir = `${finalDir}.__staging`;
|
|
84
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => {});
|
|
85
|
+
await mkdir(stagingDir, { recursive: true });
|
|
86
|
+
try {
|
|
87
|
+
let fileCount = 0;
|
|
88
|
+
for (const [name, bytes] of Object.entries(entries)) {
|
|
89
|
+
if (name.endsWith('/')) continue;
|
|
90
|
+
const safeName = safeEntryName(name);
|
|
91
|
+
if (!safeName) throw new Error(`技能包含不安全路径:${name}`);
|
|
92
|
+
const target = join(stagingDir, safeName);
|
|
93
|
+
await mkdir(join(target, '..'), { recursive: true });
|
|
94
|
+
await writeFile(target, bytes);
|
|
95
|
+
fileCount += 1;
|
|
96
|
+
}
|
|
97
|
+
if (fileCount === 0) throw new Error('技能包为空');
|
|
98
|
+
await rm(finalDir, { recursive: true, force: true }).catch(() => {});
|
|
99
|
+
await rename(stagingDir, finalDir);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => {});
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const confirmParams = options.source === 'plan' ? '?source=plan' : '';
|
|
106
|
+
const confirmResponse = await platformFetch(
|
|
107
|
+
`${base}/api/plugin/v1/skills/${namespace}/${slug}/installed${confirmParams}`,
|
|
108
|
+
{
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ version, fingerprint }),
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
if (!confirmResponse.ok) {
|
|
115
|
+
throw await platformError(confirmResponse, '技能安装回报失败');
|
|
116
|
+
}
|
|
117
|
+
return { canonical_slug: canonicalSlug, version, files: Object.keys(entries).length };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function removePlatformSkill(platformUrl, accessToken, canonicalSlug, options = {}) {
|
|
121
|
+
const base = platformUrl.replace(/\/+$/, '');
|
|
122
|
+
const { namespace, slug } = splitSkillSlug(canonicalSlug);
|
|
123
|
+
if (!slug) throw new Error(`技能标识格式不正确:${canonicalSlug}`);
|
|
124
|
+
await platformFetch(`${base}/api/plugin/v1/skills/${namespace}/${slug}`, {
|
|
125
|
+
method: 'DELETE',
|
|
126
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
127
|
+
}).catch(() => {});
|
|
128
|
+
await rm(skillDir(options.config || {}, canonicalSlug), { recursive: true, force: true }).catch(
|
|
129
|
+
() => {},
|
|
130
|
+
);
|
|
131
|
+
return { canonical_slug: canonicalSlug };
|
|
132
|
+
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 平台凭据持久化:$DSH_HOME/agentone/credentials.json(0600,仅本机插件可读)。
|
|
2
|
+
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
export function resolveDshHome() {
|
|
7
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function storeDir(config) {
|
|
11
|
+
return config.storeDir || join(resolveDshHome(), 'agentone');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function credentialsPath(config) {
|
|
15
|
+
return join(storeDir(config), 'credentials.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 读取凭据。文件损坏(截断/手改坏)不抛出:把坏文件移开为
|
|
20
|
+
* credentials.json.corrupt,返回 null 回到未登录态——否则 logout 也依赖
|
|
21
|
+
* 读取成功,用户会被永久卡在「已登录但一切接口报错」的状态。
|
|
22
|
+
*/
|
|
23
|
+
export async function loadCredentials(config) {
|
|
24
|
+
const path = credentialsPath(config);
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = await readFile(path, 'utf8');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error?.code === 'ENOENT') return null;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(raw);
|
|
34
|
+
if (!data || typeof data.access_token !== 'string') throw new Error('not a credentials object');
|
|
35
|
+
return data;
|
|
36
|
+
} catch {
|
|
37
|
+
await copyFile(path, `${path}.corrupt`).catch(() => {});
|
|
38
|
+
await rm(path, { force: true }).catch(() => {});
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function saveCredentials(config, credentials) {
|
|
44
|
+
const dir = storeDir(config);
|
|
45
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
46
|
+
await writeFile(credentialsPath(config), JSON.stringify(credentials, null, 2) + '\n', {
|
|
47
|
+
mode: 0o600,
|
|
48
|
+
flag: 'w',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 读取-合并-写回:模型同步结果等增量字段落盘。 */
|
|
53
|
+
export async function updateCredentials(config, patch) {
|
|
54
|
+
const current = (await loadCredentials(config)) || {};
|
|
55
|
+
await saveCredentials(config, { ...current, ...patch });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function clearCredentials(config) {
|
|
59
|
+
await rm(credentialsPath(config), { force: true });
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-agentone",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "AgentOne 平台集成插件:飞书登录、平台模型、SkillHub 技能、套餐申请、插件管理、lark-cli / ccpg-cli 探测(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错、状态页 UX 增强)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"files": [
|
|
13
|
+
"lib/",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"dsh",
|
|
19
|
+
"dsh-plugin",
|
|
20
|
+
"agentone"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"fflate": "^0.8.3",
|
|
24
|
+
"yaml": "^2.5.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@deepseek-ai/cordis": "*",
|
|
28
|
+
"@deepseek-ai/schemastery": "*"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"inject": [
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
37
|
+
],
|
|
38
|
+
"platform": "web"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|