dsh-skill-hub 0.3.21 → 0.3.22
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/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +2 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +59 -5
- package/lib/types/client/locales/settings.d.ts +1 -1
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/protocol/config.d.ts +1 -1
- package/lib/types/routes/deps.d.ts +8 -1
- package/lib/types/routes/helpers.d.ts +11 -6
- package/lib/types/routes/http.d.ts +27 -2
- package/lib/types/routes.d.ts +29 -4
- package/package.json +1 -1
- package/src/client/locales/settings.ts +2 -2
- package/src/index.ts +15 -3
- package/src/protocol/config.ts +1 -1
- package/src/routes/deps.ts +8 -2
- package/src/routes/helpers.ts +18 -7
- package/src/routes/http.ts +31 -2
- package/src/routes.test.ts +70 -4
- package/src/routes.ts +49 -5
package/lib/index.js
CHANGED
|
@@ -2391,6 +2391,15 @@ function isLoopbackRequest(request) {
|
|
|
2391
2391
|
return false;
|
|
2392
2392
|
}
|
|
2393
2393
|
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Fallback fence for a host that mounts no Connection service (unit tests, and
|
|
2396
|
+
* any composition serving the webserver alone): the historical literal-loopback
|
|
2397
|
+
* check, so a missing service degrades to the previous behaviour instead of an
|
|
2398
|
+
* open or a dead route family.
|
|
2399
|
+
*/
|
|
2400
|
+
function loopbackOnlyFence(request) {
|
|
2401
|
+
return isLoopbackRequest(request) ? void 0 : 403;
|
|
2402
|
+
}
|
|
2394
2403
|
/** One JSON response. */
|
|
2395
2404
|
function writeJson(res, status, body) {
|
|
2396
2405
|
const payload = JSON.stringify(body);
|
|
@@ -2863,17 +2872,24 @@ function toDetail(skill) {
|
|
|
2863
2872
|
//#region src/routes/helpers.ts
|
|
2864
2873
|
/**
|
|
2865
2874
|
* Wrap one handler in the shared request fences. Order matches the
|
|
2866
|
-
* original per-handler prologue:
|
|
2875
|
+
* original per-handler prologue: trust fence → HTTP method → master
|
|
2867
2876
|
* switch → JSON body. The outer catch maps errors onto the JSON error
|
|
2868
2877
|
* body (preserving RepoFetchError status codes).
|
|
2878
|
+
*
|
|
2879
|
+
* The trust fence is the host's `/api` model (`deps.fence`) whenever the
|
|
2880
|
+
* composition mounts Connection, so a reverse-proxied deployment is judged by
|
|
2881
|
+
* the same authorities dsh already trusts; without that service the historical
|
|
2882
|
+
* literal-loopback check stands in.
|
|
2869
2883
|
*/
|
|
2870
2884
|
function createRoute(deps, spec) {
|
|
2885
|
+
const fence = deps.fence ?? loopbackOnlyFence;
|
|
2871
2886
|
return {
|
|
2872
2887
|
kind: spec.kind ?? "exact",
|
|
2873
2888
|
path: spec.path,
|
|
2874
2889
|
handler: async (req, res) => {
|
|
2875
|
-
|
|
2876
|
-
|
|
2890
|
+
const rejection = fence(req);
|
|
2891
|
+
if (rejection !== void 0) {
|
|
2892
|
+
writeError(res, rejection, rejection === 401 ? "unauthorized: browser session required" : "forbidden: untrusted authority");
|
|
2877
2893
|
return;
|
|
2878
2894
|
}
|
|
2879
2895
|
if (!spec.methods.includes(req.method)) {
|
|
@@ -4507,8 +4523,45 @@ async function sweepOrphanTrash(deps, home, failed) {
|
|
|
4507
4523
|
//#endregion
|
|
4508
4524
|
//#region src/routes.ts
|
|
4509
4525
|
/**
|
|
4526
|
+
* Build the family's trust fence from the host's `/api` model.
|
|
4527
|
+
*
|
|
4528
|
+
* `ctx.connection.requestRejection` is the documented hook for fencing another
|
|
4529
|
+
* Web route; with no Connection service (a webserver-only composition, or a
|
|
4530
|
+
* unit test) the literal-loopback check stands in, so the fallback is the old
|
|
4531
|
+
* behaviour rather than an open route family.
|
|
4532
|
+
*
|
|
4533
|
+
* The service is read **per request**, not once at mount: plugins start in
|
|
4534
|
+
* composition order, so a one-shot read at `apply()` time would latch the
|
|
4535
|
+
* fallback whenever this plugin happens to come up before Connection. A
|
|
4536
|
+
* persistent absence is almost certainly an upstream composition change rather
|
|
4537
|
+
* than a transient startup order, so the first miss fires `onMissing` once —
|
|
4538
|
+
* without that signal the fallback would silently reproduce issue #17 after
|
|
4539
|
+
* any future upstream rename of the service.
|
|
4540
|
+
* @param read - resolves `ctx.connection`, or `undefined` when absent.
|
|
4541
|
+
* @param onMissing - called once, the first time the service reads absent.
|
|
4542
|
+
* @returns the fence every route in this family runs first.
|
|
4543
|
+
*/
|
|
4544
|
+
function hostTrustFence(read, onMissing) {
|
|
4545
|
+
let missingReported = false;
|
|
4546
|
+
return (request) => {
|
|
4547
|
+
const connection = read();
|
|
4548
|
+
if (connection === void 0) {
|
|
4549
|
+
if (!missingReported) {
|
|
4550
|
+
missingReported = true;
|
|
4551
|
+
onMissing?.();
|
|
4552
|
+
}
|
|
4553
|
+
return loopbackOnlyFence(request);
|
|
4554
|
+
}
|
|
4555
|
+
try {
|
|
4556
|
+
return connection.requestRejection(request);
|
|
4557
|
+
} catch {
|
|
4558
|
+
return 403;
|
|
4559
|
+
}
|
|
4560
|
+
};
|
|
4561
|
+
}
|
|
4562
|
+
/**
|
|
4510
4563
|
* Build every /api/skill-hub route.
|
|
4511
|
-
* @param deps - skill registry view + sidecar store.
|
|
4564
|
+
* @param deps - skill registry view + sidecar store + trust fence.
|
|
4512
4565
|
* @returns the exact-path routes plus the family's 404 catch-all.
|
|
4513
4566
|
*/
|
|
4514
4567
|
function makeRoutes(deps) {
|
|
@@ -5014,7 +5067,7 @@ const CONFIG_FIELDS = [
|
|
|
5014
5067
|
/** Order of the announcement section within the tool-guidance band. */
|
|
5015
5068
|
const SECTION_ORDER = 152;
|
|
5016
5069
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
5017
|
-
const SKILL_HUB_GUIDANCE = ["本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents
|
|
5070
|
+
const SKILL_HUB_GUIDANCE = ["本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由受 dsh 自身的 /api 信任栅栏保护(回环、监听器绑定地址、--trusted-host 声明的 authority,外加浏览器会话认证),因此反代部署下与 GUI 同一套放行规则。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。", "The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → \"Skills\" is the management page; the plugin's configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with \"check all\" and \"update all\" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; the routes sit behind dsh's own /api trust fence (loopback, the listener's bind address, any --trusted-host authority, plus browser-session authentication), so a reverse-proxied deployment is judged exactly like the GUI around it. When the user mentions \"skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill\", this plugin is what they mean — collaborate accordingly."].join("\n\n");
|
|
5018
5071
|
/**
|
|
5019
5072
|
* Mount the skill hub routes and announcement.
|
|
5020
5073
|
* @param ctx - host plugin context carrying webServer/skills/systemPrompt/settings.
|
|
@@ -5105,6 +5158,7 @@ function apply(ctx, config) {
|
|
|
5105
5158
|
const disposers = makeRoutes({
|
|
5106
5159
|
skills: ctx.skills,
|
|
5107
5160
|
store,
|
|
5161
|
+
fence: hostTrustFence(() => ctx.get("connection"), () => ctx.logger.warn("[dsh-skill-hub] Connection service absent: routes fall back to literal-loopback; reverse-proxy deployments will 403 until it mounts")),
|
|
5108
5162
|
invalidate: () => {
|
|
5109
5163
|
providerControl?.invalidate();
|
|
5110
5164
|
},
|
|
@@ -21,7 +21,7 @@ export declare const zhSettings: {
|
|
|
21
21
|
readonly 'settings.statsScanMinutes': "自动统计间隔(分钟)";
|
|
22
22
|
readonly 'settings.statsScanMinutesHint': "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。";
|
|
23
23
|
readonly 'settings.githubToken': "GitHub Token";
|
|
24
|
-
readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token
|
|
24
|
+
readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅经 dsh 的 /api 信任栅栏可读(回环 / 绑定地址 / --trusted-host);留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。";
|
|
25
25
|
readonly 'settings.githubTokenPlaceholder': "ghp_… / github_pat_…(留空为匿名)";
|
|
26
26
|
readonly 'settings.inherit': "继承";
|
|
27
27
|
readonly 'settings.overridden': "已覆盖";
|
|
@@ -26,7 +26,7 @@ export declare const zh: {
|
|
|
26
26
|
readonly 'settings.statsScanMinutes': "自动统计间隔(分钟)";
|
|
27
27
|
readonly 'settings.statsScanMinutesHint': "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。";
|
|
28
28
|
readonly 'settings.githubToken': "GitHub Token";
|
|
29
|
-
readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token
|
|
29
|
+
readonly 'settings.githubTokenHint': "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅经 dsh 的 /api 信任栅栏可读(回环 / 绑定地址 / --trusted-host);留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。";
|
|
30
30
|
readonly 'settings.githubTokenPlaceholder': "ghp_… / github_pat_…(留空为匿名)";
|
|
31
31
|
readonly 'settings.inherit': "继承";
|
|
32
32
|
readonly 'settings.overridden': "已覆盖";
|
|
@@ -57,7 +57,7 @@ export type HubSettingsValue = {
|
|
|
57
57
|
statsWindowDays?: number;
|
|
58
58
|
/** 自动统计扫描间隔(分钟)。 */
|
|
59
59
|
statsScanMinutes?: number;
|
|
60
|
-
/** GitHub token
|
|
60
|
+
/** GitHub token(明文存本地设置文档,仅经 dsh 的 /api 信任栅栏可读;缺省为匿名)。 */
|
|
61
61
|
githubToken?: string;
|
|
62
62
|
};
|
|
63
63
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* routes 共享层 · 依赖视图:路由依赖接口、配置解析与可写技能解析(HTTP
|
|
3
|
-
* 围栏在 ./http.ts
|
|
3
|
+
* 围栏在 ./http.ts)。
|
|
4
4
|
*/
|
|
5
5
|
import type { ServerResponse } from 'node:http';
|
|
6
6
|
import type { SkillDefinition, SkillSummary } from '@deepseek-ai/dsh-skill';
|
|
7
7
|
import { type HubConfig, type WritableRoot } from '../protocol.ts';
|
|
8
8
|
import { type SkillHubStore } from '../store.ts';
|
|
9
|
+
import { type RouteFence } from './http.ts';
|
|
9
10
|
/** Sources the hub may toggle (the user-level filesystem roots). */
|
|
10
11
|
export declare function isWritableSource(source: string): source is WritableRoot;
|
|
11
12
|
/** Lookup options the hub forwards to the registry (cwd selects project roots). */
|
|
@@ -22,6 +23,12 @@ export interface SkillHubRouteDeps {
|
|
|
22
23
|
get(name: string, options?: SkillLookupLike): Promise<SkillDefinition | undefined>;
|
|
23
24
|
};
|
|
24
25
|
store: SkillHubStore;
|
|
26
|
+
/**
|
|
27
|
+
* Request fence for the whole family: returns the rejection status (401/403)
|
|
28
|
+
* or `undefined` to proceed. Production passes the host's own `/api` fence
|
|
29
|
+
* (`ctx.connection.requestRejection`); absent falls back to literal loopback.
|
|
30
|
+
*/
|
|
31
|
+
fence?: RouteFence;
|
|
25
32
|
/** DSH home override (tests isolate the writable roots; defaults to ~/.dsh). */
|
|
26
33
|
home?: string;
|
|
27
34
|
/** Invalidate the registry catalog cache after hub-driven mutations. */
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* routes
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(围栏/错误映射/读体)、
|
|
2
|
+
* routes 共享层:请求栅栏(宿主 /api 信任模型 → 方法 → 总开关 → JSON 体)、
|
|
3
|
+
* 统一错误映射、可写技能解析、目录/分组数据装配。handler 只写业务,从这里 import。
|
|
4
|
+
* 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(栅栏/错误映射/读体)、
|
|
6
5
|
* ./deps.ts(依赖视图/可写解析)、./collection.ts(集合装配)、
|
|
7
6
|
* ./catalog-data.ts(目录装配);对外导出面保持不变。
|
|
8
7
|
*/
|
|
9
8
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
9
|
import type { WebRoute, WebRouteKind } from '@deepseek-ai/dsh-host-webserver';
|
|
11
10
|
import { type SkillHubRouteDeps } from './deps.ts';
|
|
12
|
-
export { MAX_JSON_BODY_BYTES, STORE_ERROR_STATUS, isLoopbackRequest, pathExists, queryParam, readJsonBody, readString, readStrings, writeError, writeJson, writeRouteError, } from './http.ts';
|
|
11
|
+
export { MAX_JSON_BODY_BYTES, STORE_ERROR_STATUS, isLoopbackRequest, loopbackOnlyFence, pathExists, queryParam, readJsonBody, readString, readStrings, writeError, writeJson, writeRouteError, } from './http.ts';
|
|
12
|
+
export type { RouteFence } from './http.ts';
|
|
13
13
|
export { configOf, disabledGate, homeOf, isWritableSource, resolveWritableSkill, savedOf, } from './deps.ts';
|
|
14
14
|
export type { SkillHubRouteDeps, SkillLookupLike, WritableSkill, WritableSkillRefusal, WritableSkillResult, } from './deps.ts';
|
|
15
15
|
export { buildCollections, buildGroups } from './collection.ts';
|
|
@@ -45,8 +45,13 @@ export interface RouteSpec {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Wrap one handler in the shared request fences. Order matches the
|
|
48
|
-
* original per-handler prologue:
|
|
48
|
+
* original per-handler prologue: trust fence → HTTP method → master
|
|
49
49
|
* switch → JSON body. The outer catch maps errors onto the JSON error
|
|
50
50
|
* body (preserving RepoFetchError status codes).
|
|
51
|
+
*
|
|
52
|
+
* The trust fence is the host's `/api` model (`deps.fence`) whenever the
|
|
53
|
+
* composition mounts Connection, so a reverse-proxied deployment is judged by
|
|
54
|
+
* the same authorities dsh already trusts; without that service the historical
|
|
55
|
+
* literal-loopback check stands in.
|
|
51
56
|
*/
|
|
52
57
|
export declare function createRoute(deps: SkillHubRouteDeps, spec: RouteSpec): WebRoute;
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* routes 共享层 · HTTP
|
|
3
|
-
*
|
|
2
|
+
* routes 共享层 · HTTP 围栏:请求信任栅栏(复用宿主 `/api` 模型,见 RouteFence)、
|
|
3
|
+
* JSON 响应与错误映射、请求体读取、查询/字段读取与文件存在性检查。
|
|
4
4
|
*/
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
6
|
/** Cap on JSON request bodies (toggle/create payloads are tiny). */
|
|
7
7
|
export declare const MAX_JSON_BODY_BYTES: number;
|
|
8
8
|
/** Loopback literal check plus browser same-origin markers. */
|
|
9
9
|
export declare function isLoopbackRequest(request: IncomingMessage): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The route family's request fence: the HTTP status a request is rejected
|
|
12
|
+
* with (`401` unauthenticated / `403` untrusted authority), or `undefined`
|
|
13
|
+
* when it may reach the handler.
|
|
14
|
+
*
|
|
15
|
+
* dsh owns **one** browser-trust model for the whole `/api` tree — loopback,
|
|
16
|
+
* the listener's own bind address, every `--trusted-host` authority, then the
|
|
17
|
+
* browser session — and publishes it as `ctx.connection.requestRejection`,
|
|
18
|
+
* whose contract is written for exactly this case ("apply Connection's
|
|
19
|
+
* Host/Origin checks ... to another Web route"). Our paths are exact routes
|
|
20
|
+
* registered beside the shared `/api` prefix, so the host's own fence never
|
|
21
|
+
* sees them; asking its entry point instead of re-deriving the rule keeps a
|
|
22
|
+
* single trust model rather than a second, stricter one. Issue #17 is what the
|
|
23
|
+
* second one cost: behind a reverse proxy the literals checkable in this file
|
|
24
|
+
* are the gateway address and the public domain, so every route answered 403
|
|
25
|
+
* while the rest of the GUI worked.
|
|
26
|
+
*/
|
|
27
|
+
export type RouteFence = (request: IncomingMessage) => number | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Fallback fence for a host that mounts no Connection service (unit tests, and
|
|
30
|
+
* any composition serving the webserver alone): the historical literal-loopback
|
|
31
|
+
* check, so a missing service degrades to the previous behaviour instead of an
|
|
32
|
+
* open or a dead route family.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loopbackOnlyFence(request: IncomingMessage): number | undefined;
|
|
10
35
|
/** One JSON response. */
|
|
11
36
|
export declare function writeJson(res: ServerResponse, status: number, body: unknown): void;
|
|
12
37
|
/** One JSON error response. */
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -3,20 +3,45 @@
|
|
|
3
3
|
* official registry + hub-disabled skills + discovery diagnostics), skill
|
|
4
4
|
* detail, enable/disable toggle, new-skill scaffold, user groups (tags +
|
|
5
5
|
* origin collections), and upstream source tracking (check/sync/follow
|
|
6
|
-
* upstream deletion into a restorable trash). Every route carries
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* upstream deletion into a restorable trash). Every route carries the host's
|
|
7
|
+
* own `/api` trust fence (loopback, the listener's bind address, each
|
|
8
|
+
* `--trusted-host` authority, then the browser session) — these endpoints
|
|
9
|
+
* rename files under the user's skill roots, so they must never be laxer than
|
|
10
|
+
* the GUI around them (issue #17).
|
|
9
11
|
*
|
|
10
12
|
* 按域拆分后的聚合入口:各域 handler 在 ./routes/<domain>.ts,共享围栏在
|
|
11
13
|
* ./routes/helpers.ts,节流/任务状态在 ./routes/route-state.ts。
|
|
12
14
|
* 老 `from './routes.ts'`(makeRoutes + SkillHubRouteDeps)保持可用。
|
|
13
15
|
*/
|
|
16
|
+
import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection';
|
|
14
17
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
15
18
|
import { type SkillHubRouteDeps } from './routes/helpers.ts';
|
|
19
|
+
import { type RouteFence } from './routes/http.ts';
|
|
16
20
|
export type { SkillHubRouteDeps, SkillLookupLike } from './routes/helpers.ts';
|
|
21
|
+
export type { RouteFence } from './routes/http.ts';
|
|
22
|
+
/**
|
|
23
|
+
* Build the family's trust fence from the host's `/api` model.
|
|
24
|
+
*
|
|
25
|
+
* `ctx.connection.requestRejection` is the documented hook for fencing another
|
|
26
|
+
* Web route; with no Connection service (a webserver-only composition, or a
|
|
27
|
+
* unit test) the literal-loopback check stands in, so the fallback is the old
|
|
28
|
+
* behaviour rather than an open route family.
|
|
29
|
+
*
|
|
30
|
+
* The service is read **per request**, not once at mount: plugins start in
|
|
31
|
+
* composition order, so a one-shot read at `apply()` time would latch the
|
|
32
|
+
* fallback whenever this plugin happens to come up before Connection. A
|
|
33
|
+
* persistent absence is almost certainly an upstream composition change rather
|
|
34
|
+
* than a transient startup order, so the first miss fires `onMissing` once —
|
|
35
|
+
* without that signal the fallback would silently reproduce issue #17 after
|
|
36
|
+
* any future upstream rename of the service.
|
|
37
|
+
* @param read - resolves `ctx.connection`, or `undefined` when absent.
|
|
38
|
+
* @param onMissing - called once, the first time the service reads absent.
|
|
39
|
+
* @returns the fence every route in this family runs first.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hostTrustFence(read: () => HostConnectionHandle | undefined, onMissing?: () => void): RouteFence;
|
|
17
42
|
/**
|
|
18
43
|
* Build every /api/skill-hub route.
|
|
19
|
-
* @param deps - skill registry view + sidecar store.
|
|
44
|
+
* @param deps - skill registry view + sidecar store + trust fence.
|
|
20
45
|
* @returns the exact-path routes plus the family's 404 catch-all.
|
|
21
46
|
*/
|
|
22
47
|
export declare function makeRoutes(deps: SkillHubRouteDeps): WebRoute[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"description": "In-GUI skill hub for DeepSeek Harness (dsh): browse the full local skill catalog from the official ctx.skills registry (every root + third-party providers), toggle skills on/off, inspect bodies, surface frontmatter diagnostics, and scaffold new skills — plus a codex-style skill market (built-in catalog, upstream update checks, one-click update-all) with tracked source sync. The full manager beyond the read-only dsh-skill-manager browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -21,7 +21,7 @@ export const zhSettings = {
|
|
|
21
21
|
'settings.statsScanMinutes': '自动统计间隔(分钟)',
|
|
22
22
|
'settings.statsScanMinutesHint': '后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。',
|
|
23
23
|
'settings.githubToken': 'GitHub Token',
|
|
24
|
-
'settings.githubTokenHint': '市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token
|
|
24
|
+
'settings.githubTokenHint': '市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅经 dsh 的 /api 信任栅栏可读(回环 / 绑定地址 / --trusted-host);留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。',
|
|
25
25
|
'settings.githubTokenPlaceholder': 'ghp_… / github_pat_…(留空为匿名)',
|
|
26
26
|
'settings.inherit': '继承',
|
|
27
27
|
'settings.overridden': '已覆盖',
|
|
@@ -59,7 +59,7 @@ export const enSettings: Record<keyof typeof zhSettings, string> = {
|
|
|
59
59
|
'settings.statsScanMinutes': 'Auto stats interval (minutes)',
|
|
60
60
|
'settings.statsScanMinutesHint': 'How often the background session-log scan runs (min 1); heavy scans automatically stretch it.',
|
|
61
61
|
'settings.githubToken': 'GitHub token',
|
|
62
|
-
'settings.githubTokenHint': 'Market/sources use the GitHub API: 60 req/h anonymous, 5000/hr with a token. A fine-grained token needs no permissions. Stored plaintext in local settings, loopback-
|
|
62
|
+
'settings.githubTokenHint': 'Market/sources use the GitHub API: 60 req/h anonymous, 5000/hr with a token. A fine-grained token needs no permissions. Stored plaintext in local settings, reachable only through dsh\'s /api trust fence (loopback / bind address / --trusted-host); empty means anonymous (or GITHUB_TOKEN env).',
|
|
63
63
|
'settings.githubTokenPlaceholder': 'ghp_… / github_pat_… (empty = anonymous)',
|
|
64
64
|
'settings.inherit': 'Inherit',
|
|
65
65
|
'settings.overridden': 'Overridden',
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type { SettingsForms, SettingsNamespace, SettingsPathOp } from '@deepseek
|
|
|
14
14
|
// loader's `fiber.runtime.Config` handling) only exist here. Every official
|
|
15
15
|
// plugin imports it under this name.
|
|
16
16
|
import z from '@deepseek-ai/schemastery'
|
|
17
|
+
// Type-only, for the `ctx.connection` augmentation (the `/api` trust fence).
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-connection'
|
|
17
19
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
18
20
|
import type {} from '@deepseek-ai/dsh-skill'
|
|
19
21
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
@@ -21,7 +23,7 @@ import type {} from '@deepseek-ai/dsh-session-query'
|
|
|
21
23
|
import type {} from '@deepseek-ai/dsh-settings'
|
|
22
24
|
import { HUB_CONFIG_DEFAULTS, HUB_ENTRY_ID, HEX_COLOR_RE, resolveHubConfig, type HubConfig } from './protocol.ts'
|
|
23
25
|
import { SkillHubProvider } from './provider.ts'
|
|
24
|
-
import { makeRoutes } from './routes.ts'
|
|
26
|
+
import { hostTrustFence, makeRoutes } from './routes.ts'
|
|
25
27
|
import { createSkillStatsReader, asPersistenceSeam, type SessionPersistenceLike, type SessionQueryLike, type SkillStatsReader } from './stats.ts'
|
|
26
28
|
import { SkillHubStore } from './store.ts'
|
|
27
29
|
import { reconcileDisabledSkills, pruneStaleDisabledSkills } from './reconcile.ts'
|
|
@@ -132,8 +134,8 @@ const SECTION_ORDER = 152
|
|
|
132
134
|
|
|
133
135
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
134
136
|
export const SKILL_HUB_GUIDANCE = [
|
|
135
|
-
'本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents
|
|
136
|
-
'The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → "Skills" is the management page; the plugin\'s configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with "check all" and "update all" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; routes
|
|
137
|
+
'本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由受 dsh 自身的 /api 信任栅栏保护(回环、监听器绑定地址、--trusted-host 声明的 authority,外加浏览器会话认证),因此反代部署下与 GUI 同一套放行规则。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。',
|
|
138
|
+
'The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → "Skills" is the management page; the plugin\'s configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with "check all" and "update all" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; the routes sit behind dsh\'s own /api trust fence (loopback, the listener\'s bind address, any --trusted-host authority, plus browser-session authentication), so a reverse-proxied deployment is judged exactly like the GUI around it. When the user mentions "skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill", this plugin is what they mean — collaborate accordingly.'
|
|
137
139
|
].join('\n\n')
|
|
138
140
|
|
|
139
141
|
/**
|
|
@@ -262,6 +264,16 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
262
264
|
const disposers = makeRoutes({
|
|
263
265
|
skills: ctx.skills,
|
|
264
266
|
store,
|
|
267
|
+
// The host's own `/api` trust fence (loopback ∪ bind address ∪
|
|
268
|
+
// --trusted-host ∪ browser session). Read per request: Connection
|
|
269
|
+
// may be provided after this plugin starts, and a one-shot read here
|
|
270
|
+
// would latch the literal-loopback fallback for the whole process.
|
|
271
|
+
// A *persistent* absence means the upstream composition changed —
|
|
272
|
+
// say so once instead of silently re-creating issue #17.
|
|
273
|
+
fence: hostTrustFence(
|
|
274
|
+
() => ctx.get('connection'),
|
|
275
|
+
() => ctx.logger.warn('[dsh-skill-hub] Connection service absent: routes fall back to literal-loopback; reverse-proxy deployments will 403 until it mounts'),
|
|
276
|
+
),
|
|
265
277
|
invalidate: () => { providerControl?.invalidate() },
|
|
266
278
|
stats,
|
|
267
279
|
config: current,
|
package/src/protocol/config.ts
CHANGED
package/src/routes/deps.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* routes 共享层 · 依赖视图:路由依赖接口、配置解析与可写技能解析(HTTP
|
|
3
|
-
* 围栏在 ./http.ts
|
|
3
|
+
* 围栏在 ./http.ts)。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ServerResponse } from 'node:http'
|
|
@@ -8,7 +8,7 @@ import type { SkillDefinition, SkillSummary } from '@deepseek-ai/dsh-skill'
|
|
|
8
8
|
import { resolveHubConfig, type HubConfig, type WritableRoot } from '../protocol.ts'
|
|
9
9
|
import { rootOfPath } from '../skillfs.ts'
|
|
10
10
|
import { dshHome, type SkillHubStore } from '../store.ts'
|
|
11
|
-
import { writeError } from './http.ts'
|
|
11
|
+
import { writeError, type RouteFence } from './http.ts'
|
|
12
12
|
|
|
13
13
|
/** Sources the hub may toggle (the user-level filesystem roots). */
|
|
14
14
|
export function isWritableSource(source: string): source is WritableRoot {
|
|
@@ -27,6 +27,12 @@ export interface SkillHubRouteDeps {
|
|
|
27
27
|
get(name: string, options?: SkillLookupLike): Promise<SkillDefinition | undefined>
|
|
28
28
|
}
|
|
29
29
|
store: SkillHubStore
|
|
30
|
+
/**
|
|
31
|
+
* Request fence for the whole family: returns the rejection status (401/403)
|
|
32
|
+
* or `undefined` to proceed. Production passes the host's own `/api` fence
|
|
33
|
+
* (`ctx.connection.requestRejection`); absent falls back to literal loopback.
|
|
34
|
+
*/
|
|
35
|
+
fence?: RouteFence
|
|
30
36
|
/** DSH home override (tests isolate the writable roots; defaults to ~/.dsh). */
|
|
31
37
|
home?: string
|
|
32
38
|
/** Invalidate the registry catalog cache after hub-driven mutations. */
|
package/src/routes/helpers.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* routes
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(围栏/错误映射/读体)、
|
|
2
|
+
* routes 共享层:请求栅栏(宿主 /api 信任模型 → 方法 → 总开关 → JSON 体)、
|
|
3
|
+
* 统一错误映射、可写技能解析、目录/分组数据装配。handler 只写业务,从这里 import。
|
|
4
|
+
* 本文件是聚合出口(barrel):实现按域拆到 ./http.ts(栅栏/错误映射/读体)、
|
|
6
5
|
* ./deps.ts(依赖视图/可写解析)、./collection.ts(集合装配)、
|
|
7
6
|
* ./catalog-data.ts(目录装配);对外导出面保持不变。
|
|
8
7
|
*/
|
|
@@ -10,12 +9,13 @@
|
|
|
10
9
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
11
10
|
import type { WebRoute, WebRouteKind } from '@deepseek-ai/dsh-host-webserver'
|
|
12
11
|
import { disabledGate, type SkillHubRouteDeps } from './deps.ts'
|
|
13
|
-
import {
|
|
12
|
+
import { loopbackOnlyFence, readJsonBody, writeError, writeRouteError } from './http.ts'
|
|
14
13
|
|
|
15
14
|
export {
|
|
16
15
|
MAX_JSON_BODY_BYTES,
|
|
17
16
|
STORE_ERROR_STATUS,
|
|
18
17
|
isLoopbackRequest,
|
|
18
|
+
loopbackOnlyFence,
|
|
19
19
|
pathExists,
|
|
20
20
|
queryParam,
|
|
21
21
|
readJsonBody,
|
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
writeJson,
|
|
26
26
|
writeRouteError,
|
|
27
27
|
} from './http.ts'
|
|
28
|
+
export type { RouteFence } from './http.ts'
|
|
28
29
|
export {
|
|
29
30
|
configOf,
|
|
30
31
|
disabledGate,
|
|
@@ -83,16 +84,26 @@ export interface RouteSpec {
|
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Wrap one handler in the shared request fences. Order matches the
|
|
86
|
-
* original per-handler prologue:
|
|
87
|
+
* original per-handler prologue: trust fence → HTTP method → master
|
|
87
88
|
* switch → JSON body. The outer catch maps errors onto the JSON error
|
|
88
89
|
* body (preserving RepoFetchError status codes).
|
|
90
|
+
*
|
|
91
|
+
* The trust fence is the host's `/api` model (`deps.fence`) whenever the
|
|
92
|
+
* composition mounts Connection, so a reverse-proxied deployment is judged by
|
|
93
|
+
* the same authorities dsh already trusts; without that service the historical
|
|
94
|
+
* literal-loopback check stands in.
|
|
89
95
|
*/
|
|
90
96
|
export function createRoute(deps: SkillHubRouteDeps, spec: RouteSpec): WebRoute {
|
|
97
|
+
const fence = deps.fence ?? loopbackOnlyFence
|
|
91
98
|
return {
|
|
92
99
|
kind: spec.kind ?? 'exact',
|
|
93
100
|
path: spec.path,
|
|
94
101
|
handler: async (req, res) => {
|
|
95
|
-
|
|
102
|
+
const rejection = fence(req)
|
|
103
|
+
if (rejection !== undefined) {
|
|
104
|
+
writeError(res, rejection, rejection === 401 ? 'unauthorized: browser session required' : 'forbidden: untrusted authority')
|
|
105
|
+
return
|
|
106
|
+
}
|
|
96
107
|
if (!spec.methods.includes(req.method as 'GET' | 'POST')) {
|
|
97
108
|
writeError(res, 405, 'method not allowed: ' + (req.method ?? ''))
|
|
98
109
|
return
|
package/src/routes/http.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* routes 共享层 · HTTP
|
|
3
|
-
*
|
|
2
|
+
* routes 共享层 · HTTP 围栏:请求信任栅栏(复用宿主 `/api` 模型,见 RouteFence)、
|
|
3
|
+
* JSON 响应与错误映射、请求体读取、查询/字段读取与文件存在性检查。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { stat } from 'node:fs/promises'
|
|
@@ -35,6 +35,35 @@ export function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The route family's request fence: the HTTP status a request is rejected
|
|
40
|
+
* with (`401` unauthenticated / `403` untrusted authority), or `undefined`
|
|
41
|
+
* when it may reach the handler.
|
|
42
|
+
*
|
|
43
|
+
* dsh owns **one** browser-trust model for the whole `/api` tree — loopback,
|
|
44
|
+
* the listener's own bind address, every `--trusted-host` authority, then the
|
|
45
|
+
* browser session — and publishes it as `ctx.connection.requestRejection`,
|
|
46
|
+
* whose contract is written for exactly this case ("apply Connection's
|
|
47
|
+
* Host/Origin checks ... to another Web route"). Our paths are exact routes
|
|
48
|
+
* registered beside the shared `/api` prefix, so the host's own fence never
|
|
49
|
+
* sees them; asking its entry point instead of re-deriving the rule keeps a
|
|
50
|
+
* single trust model rather than a second, stricter one. Issue #17 is what the
|
|
51
|
+
* second one cost: behind a reverse proxy the literals checkable in this file
|
|
52
|
+
* are the gateway address and the public domain, so every route answered 403
|
|
53
|
+
* while the rest of the GUI worked.
|
|
54
|
+
*/
|
|
55
|
+
export type RouteFence = (request: IncomingMessage) => number | undefined
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fallback fence for a host that mounts no Connection service (unit tests, and
|
|
59
|
+
* any composition serving the webserver alone): the historical literal-loopback
|
|
60
|
+
* check, so a missing service degrades to the previous behaviour instead of an
|
|
61
|
+
* open or a dead route family.
|
|
62
|
+
*/
|
|
63
|
+
export function loopbackOnlyFence(request: IncomingMessage): number | undefined {
|
|
64
|
+
return isLoopbackRequest(request) ? undefined : 403
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
/** One JSON response. */
|
|
39
68
|
export function writeJson(res: ServerResponse, status: number, body: unknown): void {
|
|
40
69
|
const payload = JSON.stringify(body)
|