@linxin666/dsh-ssh 0.4.3 → 0.4.5
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.i18n.yaml +2 -2
- package/README.md +15 -2
- package/README.zh.md +15 -2
- package/lib/client.js +120 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +130 -18
- package/lib/types/client/api.d.ts +39 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/engine/cluster.d.ts +2 -0
- package/lib/types/engine/connection-pool.d.ts +2 -2
- package/lib/types/engine.d.ts +2 -1
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +17 -17
- package/src/client/api.ts +143 -3
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +2 -0
- package/src/engine/cluster.ts +3 -1
- package/src/engine/connection-pool.ts +47 -2
- package/src/engine.ts +3 -2
- package/src/index.ts +11 -3
- package/src/tool-surface.ts +95 -0
- package/src/tools.ts +15 -16
package/lib/index.js
CHANGED
|
@@ -1016,7 +1016,7 @@ function sweepPool(engine) {
|
|
|
1016
1016
|
* Run `fn` with a live client for `alias`, reconnecting (up to the
|
|
1017
1017
|
* attempt budget) when the connection broke mid-flight.
|
|
1018
1018
|
*/
|
|
1019
|
-
async function withClient(engine, alias, fn, attempts = 3) {
|
|
1019
|
+
async function withClient(engine, alias, fn, attempts = 3, signal) {
|
|
1020
1020
|
let lastError;
|
|
1021
1021
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1022
1022
|
let record = engine.pool.get(alias);
|
|
@@ -1032,6 +1032,7 @@ async function withClient(engine, alias, fn, attempts = 3) {
|
|
|
1032
1032
|
return result;
|
|
1033
1033
|
} catch (error) {
|
|
1034
1034
|
lastError = error;
|
|
1035
|
+
if (signal?.aborted === true) throw error;
|
|
1035
1036
|
if (!record.broken) throw error;
|
|
1036
1037
|
disposeRecord(engine, alias, record);
|
|
1037
1038
|
} finally {
|
|
@@ -1041,16 +1042,33 @@ async function withClient(engine, alias, fn, attempts = 3) {
|
|
|
1041
1042
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
1042
1043
|
}
|
|
1043
1044
|
/** Run one command on `alias` (reusing the pooled connection). */
|
|
1044
|
-
async function execCommand(engine, alias, command, timeoutMs) {
|
|
1045
|
+
async function execCommand(engine, alias, command, timeoutMs, signal) {
|
|
1045
1046
|
const started = Date.now();
|
|
1046
1047
|
const budget = timeoutMs !== void 0 && timeoutMs > 0 ? timeoutMs : engine.opts.defaultExecTimeoutMs;
|
|
1047
1048
|
return withClient(engine, alias, async (client) => {
|
|
1048
1049
|
return await new Promise((resolve, reject) => {
|
|
1050
|
+
let aborted = signal !== void 0 && signal.aborted;
|
|
1051
|
+
let kill = () => {};
|
|
1052
|
+
const onAbort = () => {
|
|
1053
|
+
aborted = true;
|
|
1054
|
+
kill();
|
|
1055
|
+
};
|
|
1056
|
+
if (signal !== void 0 && !signal.aborted) signal.addEventListener("abort", onAbort, { once: true });
|
|
1049
1057
|
client.exec(command, (error, stream) => {
|
|
1050
1058
|
if (error !== void 0) {
|
|
1059
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1051
1060
|
reject(error);
|
|
1052
1061
|
return;
|
|
1053
1062
|
}
|
|
1063
|
+
kill = () => {
|
|
1064
|
+
try {
|
|
1065
|
+
stream.signal("KILL");
|
|
1066
|
+
} catch {}
|
|
1067
|
+
try {
|
|
1068
|
+
stream.close();
|
|
1069
|
+
} catch {}
|
|
1070
|
+
};
|
|
1071
|
+
if (aborted) kill();
|
|
1054
1072
|
const stdout = {
|
|
1055
1073
|
text: "",
|
|
1056
1074
|
truncated: false
|
|
@@ -1061,10 +1079,21 @@ async function execCommand(engine, alias, command, timeoutMs) {
|
|
|
1061
1079
|
};
|
|
1062
1080
|
let timedOut = false;
|
|
1063
1081
|
let settled = false;
|
|
1082
|
+
/** One cancelled outcome: the remote channel was killed on caller request. */
|
|
1083
|
+
const cancelledResult = () => ({
|
|
1084
|
+
success: false,
|
|
1085
|
+
exitCode: null,
|
|
1086
|
+
timedOut: false,
|
|
1087
|
+
stdout: stdout.text,
|
|
1088
|
+
stderr: stderr.text,
|
|
1089
|
+
durationMs: Date.now() - started,
|
|
1090
|
+
error: "command cancelled by the caller"
|
|
1091
|
+
});
|
|
1064
1092
|
const finish = () => {
|
|
1065
1093
|
if (settled) return;
|
|
1066
1094
|
settled = true;
|
|
1067
1095
|
clearTimeout(timer);
|
|
1096
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1068
1097
|
resolve({
|
|
1069
1098
|
success: false,
|
|
1070
1099
|
exitCode: null,
|
|
@@ -1091,6 +1120,11 @@ async function execCommand(engine, alias, command, timeoutMs) {
|
|
|
1091
1120
|
if (settled) return;
|
|
1092
1121
|
settled = true;
|
|
1093
1122
|
clearTimeout(timer);
|
|
1123
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1124
|
+
if (aborted) {
|
|
1125
|
+
resolve(cancelledResult());
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1094
1128
|
if (typeof code !== "number" && !timedOut) {
|
|
1095
1129
|
reject(/* @__PURE__ */ new Error("ssh: connection lost mid-flight (channel closed without an exit status)"));
|
|
1096
1130
|
return;
|
|
@@ -1108,6 +1142,11 @@ async function execCommand(engine, alias, command, timeoutMs) {
|
|
|
1108
1142
|
if (settled) return;
|
|
1109
1143
|
settled = true;
|
|
1110
1144
|
clearTimeout(timer);
|
|
1145
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1146
|
+
if (aborted) {
|
|
1147
|
+
resolve(cancelledResult());
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1111
1150
|
reject(streamError);
|
|
1112
1151
|
});
|
|
1113
1152
|
});
|
|
@@ -1535,7 +1574,7 @@ async function cluster(engine, options) {
|
|
|
1535
1574
|
while (queue.length > 0) {
|
|
1536
1575
|
const entry = queue.shift();
|
|
1537
1576
|
try {
|
|
1538
|
-
const result = await execCommand(engine, entry.alias, options.command, options.timeoutMs);
|
|
1577
|
+
const result = await execCommand(engine, entry.alias, options.command, options.timeoutMs, options.signal);
|
|
1539
1578
|
results.push({
|
|
1540
1579
|
alias: entry.alias,
|
|
1541
1580
|
ok: result.success,
|
|
@@ -1595,8 +1634,8 @@ var SshEngine = class {
|
|
|
1595
1634
|
return entry === void 0 ? void 0 : this.store.summarize(entry);
|
|
1596
1635
|
}
|
|
1597
1636
|
/** Run one command on `alias` (reusing the pooled connection). */
|
|
1598
|
-
async exec(alias, command, timeoutMs) {
|
|
1599
|
-
return execCommand(this, alias, command, timeoutMs);
|
|
1637
|
+
async exec(alias, command, timeoutMs, signal) {
|
|
1638
|
+
return execCommand(this, alias, command, timeoutMs, signal);
|
|
1600
1639
|
}
|
|
1601
1640
|
/** Run one command against many hosts concurrently. */
|
|
1602
1641
|
async cluster(options) {
|
|
@@ -2594,7 +2633,7 @@ function renderTunnel(tunnel) {
|
|
|
2594
2633
|
function sshListTool(engine) {
|
|
2595
2634
|
return defineTool({
|
|
2596
2635
|
name: "ssh_list",
|
|
2597
|
-
description: "List configured SSH hosts (alias, host, user, auth, environment, tags, description). Use ssh_exec etc. with the alias.
|
|
2636
|
+
description: "List configured SSH hosts (alias, host, user, auth, environment, tags, description). Use ssh_exec etc. with the alias.",
|
|
2598
2637
|
parameters: { query: {
|
|
2599
2638
|
type: "string",
|
|
2600
2639
|
description: "Optional fuzzy match against alias, description, host, and tags."
|
|
@@ -2670,6 +2709,7 @@ function sshListTool(engine) {
|
|
|
2670
2709
|
},
|
|
2671
2710
|
render: (_args, value) => text(renderHosts(value.hosts ?? []))
|
|
2672
2711
|
},
|
|
2712
|
+
isConcurrencySafe: () => true,
|
|
2673
2713
|
async execute(args) {
|
|
2674
2714
|
return { hosts: engine.list(args.query).map(toAgentHostRow) };
|
|
2675
2715
|
}
|
|
@@ -2679,7 +2719,7 @@ function sshListTool(engine) {
|
|
|
2679
2719
|
function sshExecTool(engine) {
|
|
2680
2720
|
return defineTool({
|
|
2681
2721
|
name: "ssh_exec",
|
|
2682
|
-
description: "Execute a shell command on a REMOTE SSH host by alias; the command runs on the remote host, never on this machine. For commands on this machine, use the local bash tool. Prefer combining independent read-only queries into one command.
|
|
2722
|
+
description: "Execute a shell command on a REMOTE SSH host by alias; the command runs on the remote host, never on this machine. For commands on this machine, use the local bash tool. Prefer combining independent read-only queries into one command.",
|
|
2683
2723
|
parameters: {
|
|
2684
2724
|
alias: {
|
|
2685
2725
|
type: "string",
|
|
@@ -2730,9 +2770,9 @@ function sshExecTool(engine) {
|
|
|
2730
2770
|
},
|
|
2731
2771
|
render: (_args, value) => text(renderExec(value))
|
|
2732
2772
|
},
|
|
2733
|
-
async execute(args) {
|
|
2773
|
+
async execute(args, exec) {
|
|
2734
2774
|
try {
|
|
2735
|
-
return await engine.exec(args.alias, args.command, args.timeoutMs);
|
|
2775
|
+
return await engine.exec(args.alias, args.command, args.timeoutMs, exec.signal);
|
|
2736
2776
|
} catch (error) {
|
|
2737
2777
|
return {
|
|
2738
2778
|
success: false,
|
|
@@ -2751,7 +2791,7 @@ function sshExecTool(engine) {
|
|
|
2751
2791
|
function sshUploadTool(engine) {
|
|
2752
2792
|
return defineTool({
|
|
2753
2793
|
name: "ssh_upload",
|
|
2754
|
-
description: "Transfer a file FROM this machine (the dsh host) TO a remote SSH host. Use this only when the file must be copied to the remote host. Files that stay on this machine are handled with the local file tools (read / write / edit), not ssh_upload.
|
|
2794
|
+
description: "Transfer a file FROM this machine (the dsh host) TO a remote SSH host. Use this only when the file must be copied to the remote host. Files that stay on this machine are handled with the local file tools (read / write / edit), not ssh_upload.",
|
|
2755
2795
|
parameters: {
|
|
2756
2796
|
alias: {
|
|
2757
2797
|
type: "string",
|
|
@@ -2806,7 +2846,7 @@ function sshUploadTool(engine) {
|
|
|
2806
2846
|
function sshDownloadTool(engine) {
|
|
2807
2847
|
return defineTool({
|
|
2808
2848
|
name: "ssh_download",
|
|
2809
|
-
description: "Copy a remote FILE from a configured SSH host to this machine (the dsh host). Use this only when the source is on the remote host; files already on this machine are read with the local file tools (read / write / edit), not ssh_download. Directory download is not supported — download files individually.
|
|
2849
|
+
description: "Copy a remote FILE from a configured SSH host to this machine (the dsh host). Use this only when the source is on the remote host; files already on this machine are read with the local file tools (read / write / edit), not ssh_download. Directory download is not supported — download files individually.",
|
|
2810
2850
|
parameters: {
|
|
2811
2851
|
alias: {
|
|
2812
2852
|
type: "string",
|
|
@@ -2858,7 +2898,7 @@ function sshDownloadTool(engine) {
|
|
|
2858
2898
|
function sshTunnelTool(engine) {
|
|
2859
2899
|
return defineTool({
|
|
2860
2900
|
name: "ssh_tunnel",
|
|
2861
|
-
description: "Manage local port-forward tunnels to a configured SSH host. Start a tunnel to reach a remote internal service (database, web UI, API) through 127.0.0.1 on this machine.
|
|
2901
|
+
description: "Manage local port-forward tunnels to a configured SSH host. Start a tunnel to reach a remote internal service (database, web UI, API) through 127.0.0.1 on this machine.",
|
|
2862
2902
|
parameters: {
|
|
2863
2903
|
action: {
|
|
2864
2904
|
type: "string",
|
|
@@ -3053,7 +3093,7 @@ function sshTunnelTool(engine) {
|
|
|
3053
3093
|
function sshClusterTool(engine) {
|
|
3054
3094
|
return defineTool({
|
|
3055
3095
|
name: "ssh_cluster",
|
|
3056
|
-
description: "Run one command concurrently across selected SSH hosts; at least one aliases, environment, or tags filter is required.
|
|
3096
|
+
description: "Run one command concurrently across selected SSH hosts; at least one aliases, environment, or tags filter is required.",
|
|
3057
3097
|
parameters: {
|
|
3058
3098
|
command: {
|
|
3059
3099
|
type: "string",
|
|
@@ -3114,9 +3154,12 @@ function sshClusterTool(engine) {
|
|
|
3114
3154
|
},
|
|
3115
3155
|
render: (_args, value) => text(renderCluster(value.results ?? []))
|
|
3116
3156
|
},
|
|
3117
|
-
async execute(args) {
|
|
3157
|
+
async execute(args, exec) {
|
|
3118
3158
|
if (!(Array.isArray(args.aliases) && args.aliases.some((alias) => typeof alias === "string" && alias.trim() !== "") || typeof args.environment === "string" && args.environment.trim() !== "" || Array.isArray(args.tags) && args.tags.some((tag) => typeof tag === "string" && tag.trim() !== ""))) throw new Error("ssh_cluster requires aliases, environment, or tags to limit the target set");
|
|
3119
|
-
return { results: await engine.cluster(
|
|
3159
|
+
return { results: await engine.cluster({
|
|
3160
|
+
...args,
|
|
3161
|
+
signal: exec.signal
|
|
3162
|
+
}) };
|
|
3120
3163
|
}
|
|
3121
3164
|
});
|
|
3122
3165
|
}
|
|
@@ -3235,6 +3278,66 @@ function mountOnce(packageName, fn) {
|
|
|
3235
3278
|
return mount;
|
|
3236
3279
|
}
|
|
3237
3280
|
//#endregion
|
|
3281
|
+
//#region src/tool-surface.ts
|
|
3282
|
+
/**
|
|
3283
|
+
* Family-shared tool-surface conventions for the packages that register
|
|
3284
|
+
* model-facing tools.
|
|
3285
|
+
*
|
|
3286
|
+
* One home for the prompt-section placement and the visibility gate this
|
|
3287
|
+
* family previously restated per package.
|
|
3288
|
+
*
|
|
3289
|
+
* Modelled on the official tool packages:
|
|
3290
|
+
* - `@deepseek-ai/dsh-tool-subagent` renders its section through
|
|
3291
|
+
* `tools.get(name, context.scope) === undefined ? '' : text`, so guidance
|
|
3292
|
+
* disappears together with a restricted tool.
|
|
3293
|
+
* - 21 of the 22 official tool packages register their tools GLOBALLY and gate
|
|
3294
|
+
* them with a setting, not per agent. Only the Agent Teams tool package —
|
|
3295
|
+
* whose tools belong to one membership — installs into each `agent.ctx`.
|
|
3296
|
+
* This family follows the majority: registration stays global so the
|
|
3297
|
+
* registry's per-scope `restrict()` mask (which filters inherited global
|
|
3298
|
+
* tools and REJECTS scope-local names) can still withhold these tools.
|
|
3299
|
+
*
|
|
3300
|
+
* Structural types only: this copy lands in packages that do not all depend on
|
|
3301
|
+
* the same SDK service packages.
|
|
3302
|
+
*/
|
|
3303
|
+
/**
|
|
3304
|
+
* Prompt-section order for each package's tool guidance.
|
|
3305
|
+
*
|
|
3306
|
+
* These sit in the family band between the deployment persona and the SDK's
|
|
3307
|
+
* `PLAN_POLICY` (500). Values are owned here rather than written per package so
|
|
3308
|
+
* the host plugins whose guidance shares one prompt cannot drift into an
|
|
3309
|
+
* accidental tie.
|
|
3310
|
+
*/
|
|
3311
|
+
const PLUGIN_TOOL_SECTION_ORDERS = {
|
|
3312
|
+
ssh: 150,
|
|
3313
|
+
"task-board": 200
|
|
3314
|
+
};
|
|
3315
|
+
/**
|
|
3316
|
+
* Build a section provider that renders \`text\` only while at least one of
|
|
3317
|
+
* \`toolNames\` is visible to the rendering scope.
|
|
3318
|
+
*
|
|
3319
|
+
* The native convention: a tool that a restriction, a preset, or a missing
|
|
3320
|
+
* registry removed must not leave guidance behind telling the model to call it.
|
|
3321
|
+
* An unreadable registry renders the text unchanged — guidance that outlives a
|
|
3322
|
+
* momentary read failure is recoverable; guidance for a tool that is gone is
|
|
3323
|
+
* not.
|
|
3324
|
+
* @param tools - the scoped tool registry, when the deployment serves one.
|
|
3325
|
+
* @param toolNames - registered names the guidance describes.
|
|
3326
|
+
* @param text - the guidance rendered while they are reachable.
|
|
3327
|
+
* @returns the section text provider.
|
|
3328
|
+
*/
|
|
3329
|
+
function visibleToolText(tools, toolNames, text) {
|
|
3330
|
+
if (tools === void 0) return () => text;
|
|
3331
|
+
return (context) => {
|
|
3332
|
+
for (const name of toolNames) try {
|
|
3333
|
+
if (tools.get(name, context.scope) !== void 0) return text;
|
|
3334
|
+
} catch {
|
|
3335
|
+
return text;
|
|
3336
|
+
}
|
|
3337
|
+
return "";
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
//#endregion
|
|
3238
3341
|
//#region src/index.ts
|
|
3239
3342
|
/** Stable cordis plugin name. */
|
|
3240
3343
|
const name = "ssh";
|
|
@@ -3275,8 +3378,17 @@ function profileConfig(ctx) {
|
|
|
3275
3378
|
if (typeof config !== "object" || config === null || Array.isArray(config)) return void 0;
|
|
3276
3379
|
return config;
|
|
3277
3380
|
}
|
|
3278
|
-
/** Order of the announcement section within the tool-guidance band. */
|
|
3279
|
-
const SECTION_ORDER =
|
|
3381
|
+
/** Order of the announcement section within the shared tool-guidance band. */
|
|
3382
|
+
const SECTION_ORDER = PLUGIN_TOOL_SECTION_ORDERS.ssh;
|
|
3383
|
+
/** The six agent-tool names this plugin's announcement describes. */
|
|
3384
|
+
const SSH_TOOL_NAMES = [
|
|
3385
|
+
"ssh_list",
|
|
3386
|
+
"ssh_exec",
|
|
3387
|
+
"ssh_upload",
|
|
3388
|
+
"ssh_download",
|
|
3389
|
+
"ssh_tunnel",
|
|
3390
|
+
"ssh_cluster"
|
|
3391
|
+
];
|
|
3280
3392
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3281
3393
|
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 按 aliases/environment/tags 至少一种非空 selector 筛选后集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥、ProxyJump 跳板机(别名或 [user@]host[:port] 地址)与 OpenSSH 语义的 ProxyCommand(跳过堡垒机客户端场景,只能由用户在 GUI 配置,agent 不可写);Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。路径区分:本机(dsh host)上的文件与命令一律用本地工具(read / write / edit / bash),ssh_* 工具只针对远程主机上的路径。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
|
|
3282
3394
|
/**
|
|
@@ -3332,7 +3444,7 @@ function applyImpl(ctx, config) {
|
|
|
3332
3444
|
if (value.announceToAgent) disposeSection = ctx.systemPrompt.section({
|
|
3333
3445
|
name: "plugin:dsh-ssh",
|
|
3334
3446
|
order: SECTION_ORDER,
|
|
3335
|
-
text: SSH_GUIDANCE
|
|
3447
|
+
text: visibleToolText(ctx.tools, SSH_TOOL_NAMES, SSH_GUIDANCE)
|
|
3336
3448
|
});
|
|
3337
3449
|
disposeRoutes = ctx.effect(() => {
|
|
3338
3450
|
const disposers = routes.map((route) => ctx.webServer.register(route));
|
|
@@ -7,6 +7,44 @@ import { type ClusterResult, type ExecResult, type HostPayload, type ImportResul
|
|
|
7
7
|
export declare class SshApiError extends Error {
|
|
8
8
|
constructor(message: string);
|
|
9
9
|
}
|
|
10
|
+
/** The location facts a terminal socket URL is built from. */
|
|
11
|
+
interface PageLocation {
|
|
12
|
+
/** Page scheme with its colon (for example `https:` or `dsh-app:`). */
|
|
13
|
+
protocol: string;
|
|
14
|
+
/** Page authority, empty for a scheme that carries none. */
|
|
15
|
+
host: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The WebSocket URL the terminal route is reached at, or undefined when this
|
|
19
|
+
* page cannot carry one.
|
|
20
|
+
*
|
|
21
|
+
* A web page resolves the socket against its own origin, exactly as before.
|
|
22
|
+
*
|
|
23
|
+
* A page delivered by an application on this machine (the official DSH Desktop
|
|
24
|
+
* shell serves its Web GUI from `dsh-app://app/`) cannot carry a socket on its
|
|
25
|
+
* own scheme: the protocol handler forwards HTTP to the local Host but has no
|
|
26
|
+
* upgrade to forward. That is exactly the case the official transport hook
|
|
27
|
+
* covers - the shell publishes `__DSH_TRANSPORT__.streamBaseUrl`, the loopback
|
|
28
|
+
* authority of the Host it owns and already forwards every other request to
|
|
29
|
+
* (issue #1744). The socket is therefore dialed there instead, with the `ws`/
|
|
30
|
+
* `wss` scheme derived from that base, which is what the shell's
|
|
31
|
+
* `onBeforeSendHeaders` hook expects to see before it attaches its
|
|
32
|
+
* authority-bound credential.
|
|
33
|
+
*
|
|
34
|
+
* The base is used only on an application-delivered page. A web page that
|
|
35
|
+
* somehow carries the hook (the remote channel grants `ownsHost` to a paired
|
|
36
|
+
* LAN page, and that page is fenced behind a pairing channel) keeps resolving
|
|
37
|
+
* against its own origin, so this cannot reroute gated traffic onto a host the
|
|
38
|
+
* page is not entitled to reach. A blank authority is treated the same as an
|
|
39
|
+
* application scheme with no base: there is nothing to dial, and the caller
|
|
40
|
+
* gets the actionable reason instead of a socket that can only fail.
|
|
41
|
+
*
|
|
42
|
+
* @param location - the page location to read.
|
|
43
|
+
* @param search - the query string carrying `alias` or `session`.
|
|
44
|
+
* @param streamBaseUrl - `__DSH_TRANSPORT__.streamBaseUrl`, when the shell published one.
|
|
45
|
+
* @returns the absolute `ws:`/`wss:` URL, or undefined when the page cannot carry one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function terminalSocketUrl(location: PageLocation, search: string, streamBaseUrl?: string | undefined): string | undefined;
|
|
10
48
|
/** One open terminal connection (WebSocket JSON frames). */
|
|
11
49
|
export interface TerminalConnection {
|
|
12
50
|
/** Fired on the ready frame (shell is up); carries the host session id and alias. */
|
|
@@ -100,3 +138,4 @@ export declare class SshApi {
|
|
|
100
138
|
/** One terminal socket over either an alias (open) or a session id (attach). */
|
|
101
139
|
private terminalSocket;
|
|
102
140
|
}
|
|
141
|
+
export {};
|
|
@@ -89,6 +89,7 @@ export declare const zh: {
|
|
|
89
89
|
readonly 'terminal.ready': "终端已连接({alias})";
|
|
90
90
|
readonly 'terminal.exited': "终端已退出({alias})";
|
|
91
91
|
readonly 'terminal.error': "终端错误:{error}";
|
|
92
|
+
readonly 'terminal.noWebSocket': "当前页面不支持 WebSocket(应用外壳没有可连接的本地主机地址),请改用浏览器打开的 Web 界面使用终端。";
|
|
92
93
|
readonly 'terminal.auth.title': "二次身份验证 (2FA / 交互式认证)";
|
|
93
94
|
readonly 'terminal.auth.submit': "提交验证码";
|
|
94
95
|
readonly 'terminal.auth.cancel': "取消";
|
|
@@ -11,6 +11,8 @@ interface ClusterOptions {
|
|
|
11
11
|
tags?: string[];
|
|
12
12
|
timeoutMs?: number;
|
|
13
13
|
maxWorkers?: number;
|
|
14
|
+
/** Caller cancellation, forwarded to every host's command. */
|
|
15
|
+
signal?: AbortSignal;
|
|
14
16
|
}
|
|
15
17
|
/** Run one command against many hosts concurrently. */
|
|
16
18
|
export declare function cluster(engine: PoolEngine, options: ClusterOptions): Promise<ClusterResult[]>;
|
|
@@ -107,6 +107,6 @@ export declare function sweepPool(engine: PoolEngine): void;
|
|
|
107
107
|
* Run `fn` with a live client for `alias`, reconnecting (up to the
|
|
108
108
|
* attempt budget) when the connection broke mid-flight.
|
|
109
109
|
*/
|
|
110
|
-
export declare function withClient<T>(engine: PoolEngine, alias: string, fn: (client: Client) => Promise<T>, attempts?: number): Promise<T>;
|
|
110
|
+
export declare function withClient<T>(engine: PoolEngine, alias: string, fn: (client: Client) => Promise<T>, attempts?: number, signal?: AbortSignal): Promise<T>;
|
|
111
111
|
/** Run one command on `alias` (reusing the pooled connection). */
|
|
112
|
-
export declare function execCommand(engine: PoolEngine, alias: string, command: string, timeoutMs?: number): Promise<ExecResult>;
|
|
112
|
+
export declare function execCommand(engine: PoolEngine, alias: string, command: string, timeoutMs?: number, signal?: AbortSignal): Promise<ExecResult>;
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare class SshEngine {
|
|
|
36
36
|
/** One host summary by alias. */
|
|
37
37
|
find(alias: string): SshHostSummary | undefined;
|
|
38
38
|
/** Run one command on `alias` (reusing the pooled connection). */
|
|
39
|
-
exec(alias: string, command: string, timeoutMs?: number): Promise<ExecResult>;
|
|
39
|
+
exec(alias: string, command: string, timeoutMs?: number, signal?: AbortSignal): Promise<ExecResult>;
|
|
40
40
|
/** Run one command against many hosts concurrently. */
|
|
41
41
|
cluster(options: {
|
|
42
42
|
command: string;
|
|
@@ -45,6 +45,7 @@ export declare class SshEngine {
|
|
|
45
45
|
tags?: string[];
|
|
46
46
|
timeoutMs?: number;
|
|
47
47
|
maxWorkers?: number;
|
|
48
|
+
signal?: AbortSignal;
|
|
48
49
|
}): Promise<ClusterResult[]>;
|
|
49
50
|
/** Open a PTY shell session for the web terminal (standalone connection). */
|
|
50
51
|
openShell(alias: string, size: {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family-shared tool-surface conventions for the packages that register
|
|
3
|
+
* model-facing tools.
|
|
4
|
+
*
|
|
5
|
+
* One home for the prompt-section placement and the visibility gate this
|
|
6
|
+
* family previously restated per package.
|
|
7
|
+
*
|
|
8
|
+
* Modelled on the official tool packages:
|
|
9
|
+
* - `@deepseek-ai/dsh-tool-subagent` renders its section through
|
|
10
|
+
* `tools.get(name, context.scope) === undefined ? '' : text`, so guidance
|
|
11
|
+
* disappears together with a restricted tool.
|
|
12
|
+
* - 21 of the 22 official tool packages register their tools GLOBALLY and gate
|
|
13
|
+
* them with a setting, not per agent. Only the Agent Teams tool package —
|
|
14
|
+
* whose tools belong to one membership — installs into each `agent.ctx`.
|
|
15
|
+
* This family follows the majority: registration stays global so the
|
|
16
|
+
* registry's per-scope `restrict()` mask (which filters inherited global
|
|
17
|
+
* tools and REJECTS scope-local names) can still withhold these tools.
|
|
18
|
+
*
|
|
19
|
+
* Structural types only: this copy lands in packages that do not all depend on
|
|
20
|
+
* the same SDK service packages.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Prompt-section order for each package's tool guidance.
|
|
24
|
+
*
|
|
25
|
+
* These sit in the family band between the deployment persona and the SDK's
|
|
26
|
+
* `PLAN_POLICY` (500). Values are owned here rather than written per package so
|
|
27
|
+
* the host plugins whose guidance shares one prompt cannot drift into an
|
|
28
|
+
* accidental tie.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PLUGIN_TOOL_SECTION_ORDERS: {
|
|
31
|
+
readonly ssh: 150;
|
|
32
|
+
readonly 'task-board': 200;
|
|
33
|
+
};
|
|
34
|
+
/** One plugin id keyed by {@link PLUGIN_TOOL_SECTION_ORDERS}. */
|
|
35
|
+
export type PluginToolSectionId = keyof typeof PLUGIN_TOOL_SECTION_ORDERS;
|
|
36
|
+
/**
|
|
37
|
+
* Order of an external provider extension's own guidance section, right after
|
|
38
|
+
* the band of the plugin it extends.
|
|
39
|
+
*
|
|
40
|
+
* An extension names itself, not the plugin it plugs into, so its order stays
|
|
41
|
+
* out of {@link PLUGIN_TOOL_SECTION_ORDERS}: every package's copy of this module
|
|
42
|
+
* then carries no provider id. The slot still has one owner and cannot collide
|
|
43
|
+
* with a host plugin's band.
|
|
44
|
+
*/
|
|
45
|
+
export declare const EXTENSION_TOOL_SECTION_ORDER = 210;
|
|
46
|
+
/**
|
|
47
|
+
* The scoped tool lookup a section provider needs: the registry read for one
|
|
48
|
+
* assembly's scope. Matches `ctx.tools.get(name, scope)`.
|
|
49
|
+
*/
|
|
50
|
+
export interface ScopedToolLookup {
|
|
51
|
+
get(name: string, scope?: unknown): unknown;
|
|
52
|
+
}
|
|
53
|
+
/** One assembly context carrying the scope the section is rendered for. */
|
|
54
|
+
export interface SectionScopeContext {
|
|
55
|
+
scope?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a section provider that renders \`text\` only while at least one of
|
|
59
|
+
* \`toolNames\` is visible to the rendering scope.
|
|
60
|
+
*
|
|
61
|
+
* The native convention: a tool that a restriction, a preset, or a missing
|
|
62
|
+
* registry removed must not leave guidance behind telling the model to call it.
|
|
63
|
+
* An unreadable registry renders the text unchanged — guidance that outlives a
|
|
64
|
+
* momentary read failure is recoverable; guidance for a tool that is gone is
|
|
65
|
+
* not.
|
|
66
|
+
* @param tools - the scoped tool registry, when the deployment serves one.
|
|
67
|
+
* @param toolNames - registered names the guidance describes.
|
|
68
|
+
* @param text - the guidance rendered while they are reachable.
|
|
69
|
+
* @returns the section text provider.
|
|
70
|
+
*/
|
|
71
|
+
export declare function visibleToolText(tools: ScopedToolLookup | undefined, toolNames: readonly string[], text: string): (context: SectionScopeContext) => string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dsh": {
|
|
29
29
|
"engines": {
|
|
30
|
-
"dsh": ">=0.
|
|
30
|
+
"dsh": ">=0.2.0-rc.2"
|
|
31
31
|
},
|
|
32
32
|
"bundle": {
|
|
33
33
|
"patch": "./cordis.patch.yml"
|
|
@@ -50,26 +50,26 @@
|
|
|
50
50
|
"ws": "^8.18.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@deepseek-ai/dsh": ">=0.
|
|
53
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2",
|
|
54
54
|
"react": "^18.2.0",
|
|
55
55
|
"react-dom": "^18.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
59
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
60
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-layout": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
66
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
67
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
68
|
-
"@deepseek-ai/dsh-scope": "^0.
|
|
69
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
70
|
-
"@deepseek-ai/dsh-system-prompt": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-timeout": "^0.
|
|
72
|
-
"@deepseek-ai/dsh-tools": "^0.
|
|
59
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-scope": "^0.2.0-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-timeout": "^0.2.0-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
|
|
73
73
|
"@types/node": "^22.20.0",
|
|
74
74
|
"@types/react": "~18.3.1",
|
|
75
75
|
"@types/react-dom": "^18.3.5",
|