@morehao/dsh-skills-console 0.4.0 → 0.5.1
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 +44 -20
- package/client.js +1299 -248
- package/lib/core/hash.js +13 -1
- package/lib/core/intro-store.js +156 -0
- package/lib/core/intro.js +430 -0
- package/lib/core/inventory.js +45 -17
- package/lib/core/invocation.js +578 -0
- package/lib/core/paths.js +2 -0
- package/lib/core/remote.js +103 -1
- package/lib/core/service.js +1103 -9
- package/lib/core/state.js +106 -6
- package/lib/index.js +16 -0
- package/lib/invocation-support.json +46 -0
- package/lib/routes.js +60 -2
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/lib/core/remote.js
CHANGED
|
@@ -190,7 +190,14 @@ export async function snapshotFromTarball({
|
|
|
190
190
|
} catch (error) {
|
|
191
191
|
throw new RemoteError('scan-failed', `计算 ${skill.name} tree 失败: ${error.message}`, { cause: error });
|
|
192
192
|
}
|
|
193
|
-
entries.push({
|
|
193
|
+
entries.push({
|
|
194
|
+
name: skill.name,
|
|
195
|
+
skillPath: skill.skillPath,
|
|
196
|
+
treeSha,
|
|
197
|
+
// 订阅条目在界面上还要用一行原文描述帮用户决定装不装:只是 frontmatter 里作者自己的话,
|
|
198
|
+
// 不调模型、也不进提示词;截断避免 state.json 被超长描述撑大。
|
|
199
|
+
description: typeof skill.description === 'string' ? skill.description.slice(0, 600) : '',
|
|
200
|
+
});
|
|
194
201
|
const dir = skillDirOf(skill.skillPath);
|
|
195
202
|
if (dir && !seenDirs.has(dir)) {
|
|
196
203
|
seenDirs.add(dir);
|
|
@@ -332,3 +339,98 @@ export async function downloadTarball({
|
|
|
332
339
|
if (timer) clearTimeout(timer);
|
|
333
340
|
}
|
|
334
341
|
}
|
|
342
|
+
|
|
343
|
+
// ---- 订阅候选的 SKILL.md(AI 介绍用) -------------------------------------
|
|
344
|
+
// 只读单个文件、只允许 raw.githubusercontent.com;按 commit 读取,避免分支移动导致内容漂移。
|
|
345
|
+
|
|
346
|
+
export const REMOTE_SKILL_MD_MAX_BYTES = 512 * 1024;
|
|
347
|
+
export const REMOTE_SKILL_MD_TIMEOUT_MS = 20_000;
|
|
348
|
+
const COMMIT_SHA = /^[0-9a-f]{40}$/i;
|
|
349
|
+
|
|
350
|
+
/** 技能文件路径校验:快照里的 skillPath 也不盲信,逐段拒绝越界与特殊字符。 */
|
|
351
|
+
function assertSkillFilePath(skillPath) {
|
|
352
|
+
if (typeof skillPath !== 'string' || skillPath.length === 0 || skillPath.length > 400) {
|
|
353
|
+
throw new RemoteError('intro-source-failed', '技能路径非法');
|
|
354
|
+
}
|
|
355
|
+
if (skillPath.startsWith('/') || skillPath.includes('\\') || /^[A-Za-z]:/.test(skillPath)) {
|
|
356
|
+
throw new RemoteError('intro-source-failed', `技能路径不是仓库内相对路径: ${skillPath}`);
|
|
357
|
+
}
|
|
358
|
+
// eslint-disable-next-line no-control-regex
|
|
359
|
+
if (/[\u0000-\u001f]/.test(skillPath)) {
|
|
360
|
+
throw new RemoteError('intro-source-failed', '技能路径含控制字符');
|
|
361
|
+
}
|
|
362
|
+
const segments = skillPath.split('/');
|
|
363
|
+
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
364
|
+
throw new RemoteError('intro-source-failed', `技能路径含非法段: ${skillPath}`);
|
|
365
|
+
}
|
|
366
|
+
return segments.map(encodeURIComponent).join('/');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 读订阅快照里某个技能的 `SKILL.md`(用于按需生成能力介绍)。
|
|
371
|
+
*
|
|
372
|
+
* - `ref` 必须是快照里的 40 位 commit:订阅页的「介绍」要可复现,不能读会移动的分支;
|
|
373
|
+
* - 只允许 GitHub 的 raw host,路径由服务端从快照反查后传入,不接受客户端拼路径;
|
|
374
|
+
* - 大小 / 超时都有上限,超限抛 RemoteError,由 Service 转成 4xx/5xx。
|
|
375
|
+
*
|
|
376
|
+
* @returns {Promise<{ text: string, bytes: number, source: string, ref: string, skillPath: string }>}
|
|
377
|
+
*/
|
|
378
|
+
export async function fetchRemoteSkillMd({
|
|
379
|
+
source,
|
|
380
|
+
ref,
|
|
381
|
+
skillPath,
|
|
382
|
+
token = null,
|
|
383
|
+
fetchImpl = fetch,
|
|
384
|
+
maxBytes = REMOTE_SKILL_MD_MAX_BYTES,
|
|
385
|
+
timeoutMs = REMOTE_SKILL_MD_TIMEOUT_MS,
|
|
386
|
+
}) {
|
|
387
|
+
const parsed = parseSource(source);
|
|
388
|
+
if (!parsed) throw new RemoteError('invalid-source', `不是 GitHub 来源: ${source}`);
|
|
389
|
+
if (typeof ref !== 'string' || !COMMIT_SHA.test(ref)) {
|
|
390
|
+
throw new RemoteError('invalid-ref', '订阅介绍必须按 40 位 commit 读取');
|
|
391
|
+
}
|
|
392
|
+
const repo = `${parsed.owner}/${parsed.repo}`;
|
|
393
|
+
const relPath = assertSkillFilePath(skillPath);
|
|
394
|
+
const url = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/${ref.toLowerCase()}/${relPath}`;
|
|
395
|
+
|
|
396
|
+
const controller = new AbortController();
|
|
397
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
398
|
+
try {
|
|
399
|
+
const response = await fetchImpl(url, {
|
|
400
|
+
headers: {
|
|
401
|
+
'user-agent': 'dsh-skills-console',
|
|
402
|
+
accept: 'text/plain, text/markdown;q=0.9, */*;q=0.1',
|
|
403
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
404
|
+
},
|
|
405
|
+
signal: controller.signal,
|
|
406
|
+
});
|
|
407
|
+
if (!response?.ok) {
|
|
408
|
+
throw new RemoteError(
|
|
409
|
+
'intro-source-failed',
|
|
410
|
+
`读不到 ${repo}@${ref.slice(0, 7)} 的 ${skillPath}(HTTP ${response?.status ?? '?'})`,
|
|
411
|
+
{ status: response?.status ?? null },
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const text = await response.text();
|
|
415
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
416
|
+
if (bytes > maxBytes) {
|
|
417
|
+
throw new RemoteError(
|
|
418
|
+
'intro-source-too-large',
|
|
419
|
+
`${repo}@${ref.slice(0, 7)} 的 ${skillPath} 太大(${bytes} 字节),无法作为介绍素材`,
|
|
420
|
+
{ bytes, maxBytes },
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (!text.trim()) throw new RemoteError('intro-source-failed', `${repo} 的 ${skillPath} 是空文件`);
|
|
424
|
+
return { text, bytes, source: repo, ref: ref.toLowerCase(), skillPath };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (error instanceof RemoteError) throw error;
|
|
427
|
+
if (controller.signal.aborted) {
|
|
428
|
+
throw new RemoteError('intro-source-timeout', `读取 ${repo} 的 SKILL.md 超时(${timeoutMs}ms)`, { cause: error });
|
|
429
|
+
}
|
|
430
|
+
throw new RemoteError('intro-source-failed', `读取 ${repo} 的 SKILL.md 失败:${error?.message || '网络错误'}`, {
|
|
431
|
+
cause: error,
|
|
432
|
+
});
|
|
433
|
+
} finally {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
}
|
|
436
|
+
}
|