@hyzyn/dsh-docker 0.5.0 → 0.6.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.en.md +93 -34
- package/README.md +108 -32
- package/client.js +138 -26
- package/lib/index.js +50 -29
- package/lib/index.js.map +1 -1
- package/lib/ssh-exec.d.ts +61 -3
- package/lib/ssh-exec.js +142 -45
- package/lib/ssh-exec.js.map +1 -1
- package/package.json +4 -3
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
2
2
|
import { definePlugin } from '@hyzyn/dsh-kit';
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
4
|
import { DockerApi, assertBin, assertImageRef, assertRef, createRunner, parseImageHistoryJson, parseImageHistoryText, parseContainerEvent, parseEventsJson, parseImageInspectJson, parseInspectJson, parsePsJson, parseStatsJson, } from './docker.js';
|
|
5
|
-
import { RemoteExec, sshTarget } from './ssh-exec.js';
|
|
5
|
+
import { RemoteExec, setCredentialResolver, sshTarget } from './ssh-exec.js';
|
|
6
6
|
const TARGET_SCHEMA = z.object({
|
|
7
7
|
name: z.string().required(),
|
|
8
8
|
kind: z.union([z.const('local'), z.const('ssh')]).default('local'),
|
|
@@ -13,7 +13,7 @@ const TARGET_SCHEMA = z.object({
|
|
|
13
13
|
username: z.string().default(''),
|
|
14
14
|
auth: z.union([z.const('agent'), z.const('key'), z.const('password')]).default('agent'),
|
|
15
15
|
keyPath: z.string().default(''),
|
|
16
|
-
/** 建议写 `env:
|
|
16
|
+
/** 建议写 `env:NAME` 凭据引用(官方凭据层解析),避免明文落盘。 */
|
|
17
17
|
password: z.string().default(''),
|
|
18
18
|
passphrase: z.string().default(''),
|
|
19
19
|
agentForward: z.boolean().default(false),
|
|
@@ -679,6 +679,21 @@ const plugin = definePlugin({
|
|
|
679
679
|
return `\n- ${row.name} (${row.kind}) ${row.label}${state}`;
|
|
680
680
|
}).join('');
|
|
681
681
|
};
|
|
682
|
+
/**
|
|
683
|
+
* 容器行 → 一行文本。入参是**工具 schema 的扁平形状**(不是领域类型 `ContainerSummary`):
|
|
684
|
+
* `ports` 已经是拼好的字符串、id 在 `id` 上、可省字段用 `undefined` 表示「无」。
|
|
685
|
+
*
|
|
686
|
+
* 这里踩过一次:渲染器按领域类型写(`row.ports.map(...)` / `row.shortId` /
|
|
687
|
+
* `row.health === null`),而工具返回的是扁平形状——于是 `ports.map is not a function`
|
|
688
|
+
* 直接崩、`id=undefined`。render 的入参是 `unknown` + 一个不受检查的 `as` 断言,
|
|
689
|
+
* 所以 tsc 抓不到。现在两侧共用下面这些 Tool*Row 类型,形状一变就编译报错。
|
|
690
|
+
*/
|
|
691
|
+
const renderPsRow = (row) => {
|
|
692
|
+
const ports = row.ports === undefined || row.ports === '' ? '' : ' ports=' + row.ports;
|
|
693
|
+
const health = row.health === undefined ? '' : ` health=${row.health}`;
|
|
694
|
+
const compose = row.composeProject === undefined ? '' : ` compose=${row.composeProject}/${row.composeService ?? '-'}`;
|
|
695
|
+
return `\n- ${row.name} [${row.state}]${health} image=${row.image}${ports}${compose} id=${row.id}`;
|
|
696
|
+
};
|
|
682
697
|
/** 跨目标容器渲染:按目标分组,失败的目标单独一行说明(部分成功也要可读)。 */
|
|
683
698
|
const renderAggregatedContainers = (groups) => {
|
|
684
699
|
if (groups.length === 0)
|
|
@@ -690,13 +705,7 @@ const plugin = definePlugin({
|
|
|
690
705
|
if (!group.ok)
|
|
691
706
|
return `\n\n■ ${group.target}(${group.label})— 不可用:${group.error ?? '未知错误'}`;
|
|
692
707
|
const rows = group.containers ?? [];
|
|
693
|
-
return `\n\n■ ${group.target}(${group.label})— ${String(rows.length)} 个容器` + rows.map((
|
|
694
|
-
const ports = row.ports.length === 0
|
|
695
|
-
? ''
|
|
696
|
-
: ' ports=' + row.ports.map((p) => (p.hostPort === undefined ? `${String(p.containerPort)}/${p.protocol}` : `${String(p.hostPort)}→${String(p.containerPort)}/${p.protocol}`)).join(',');
|
|
697
|
-
const health = row.health === null ? '' : ` health=${row.health}`;
|
|
698
|
-
return `\n- ${row.name} [${row.state}]${health} image=${row.image}${ports} id=${row.shortId}`;
|
|
699
|
-
}).join('');
|
|
708
|
+
return `\n\n■ ${group.target}(${group.label})— ${String(rows.length)} 个容器` + rows.map(renderPsRow).join('');
|
|
700
709
|
}).join('');
|
|
701
710
|
};
|
|
702
711
|
const ATTENTION_LABEL = {
|
|
@@ -706,7 +715,7 @@ const plugin = definePlugin({
|
|
|
706
715
|
'exit-nonzero': '非零退出',
|
|
707
716
|
dead: '僵死',
|
|
708
717
|
};
|
|
709
|
-
/** 需关注列表渲染(单目标 /
|
|
718
|
+
/** 需关注列表渲染(单目标 / 跨目标共用)。入参同样是**工具扁平形状**,不是 AttentionItem。 */
|
|
710
719
|
const renderAttention = (groups) => {
|
|
711
720
|
const total = groups.reduce((sum, group) => sum + (group.items?.length ?? 0), 0);
|
|
712
721
|
if (total === 0 && groups.every((group) => group.ok))
|
|
@@ -720,38 +729,32 @@ const plugin = definePlugin({
|
|
|
720
729
|
return `\n\n■ ${group.target}(${group.label})` + items.map((item) => {
|
|
721
730
|
const reasons = item.reasons.map((reason) => ATTENTION_LABEL[reason] ?? reason).join(' + ');
|
|
722
731
|
const extra = [
|
|
723
|
-
item.exitCode ===
|
|
724
|
-
item.restartCount ===
|
|
732
|
+
item.exitCode === undefined ? '' : `exit=${String(item.exitCode)}`,
|
|
733
|
+
item.restartCount === undefined ? '' : `restarts=${String(item.restartCount)}`,
|
|
725
734
|
item.oomKilled ? 'OOMKilled=true' : '',
|
|
726
735
|
].filter((part) => part !== '').join(' ');
|
|
727
|
-
|
|
736
|
+
const health = item.health === undefined ? '' : '/' + item.health;
|
|
737
|
+
return `\n- ${item.name} [${item.state}${health}] ${reasons} image=${item.image}${extra === '' ? '' : ' ' + extra} id=${item.id}`;
|
|
728
738
|
}).join('');
|
|
729
739
|
}).join('');
|
|
730
740
|
};
|
|
731
741
|
const renderContainers = (target, rows) => {
|
|
732
742
|
if (rows.length === 0)
|
|
733
743
|
return `目标 ${target}:没有容器。`;
|
|
734
|
-
return `目标 ${target} 的容器(${String(rows.length)} 个):` + rows.map((
|
|
735
|
-
const ports = row.ports.length === 0
|
|
736
|
-
? ''
|
|
737
|
-
: ' ports=' + row.ports.map((p) => (p.hostPort === undefined ? `${String(p.containerPort)}/${p.protocol}` : `${String(p.hostPort)}→${String(p.containerPort)}/${p.protocol}`)).join(',');
|
|
738
|
-
const health = row.health === null ? '' : ` health=${row.health}`;
|
|
739
|
-
const compose = row.composeProject === null ? '' : ` compose=${row.composeProject}/${row.composeService ?? '-'}`;
|
|
740
|
-
return `\n- ${row.name} [${row.state}]${health} image=${row.image}${ports}${compose} id=${row.shortId}`;
|
|
741
|
-
}).join('');
|
|
744
|
+
return `目标 ${target} 的容器(${String(rows.length)} 个):` + rows.map(renderPsRow).join('');
|
|
742
745
|
};
|
|
743
746
|
const renderStats = (target, rows) => {
|
|
744
747
|
if (rows.length === 0)
|
|
745
748
|
return `目标 ${target}:没有运行中的容器。`;
|
|
746
|
-
return `目标 ${target} 的资源占用:` + rows.map((row) => `\n- ${row.name} cpu=${row.cpuPercent ===
|
|
749
|
+
return `目标 ${target} 的资源占用:` + rows.map((row) => `\n- ${row.name} cpu=${row.cpuPercent === undefined ? '?' : String(row.cpuPercent) + '%'} mem=${row.memUsage} (${row.memPercent === undefined ? '?' : String(row.memPercent) + '%'}) net=${row.netIO} block=${row.blockIO} pids=${row.pids === undefined ? '?' : String(row.pids)}`).join('');
|
|
747
750
|
};
|
|
748
751
|
const renderEvents = (target, since, rows) => {
|
|
749
752
|
if (rows.length === 0)
|
|
750
753
|
return `目标 ${target}:最近 ${since} 没有容器事件。`;
|
|
751
754
|
return `目标 ${target} 的容器事件(最近 ${since},${String(rows.length)} 条):` + rows.map((row) => {
|
|
752
|
-
const at = row.time ===
|
|
753
|
-
const exit = row.exitCode ===
|
|
754
|
-
const compose = row.composeProject ===
|
|
755
|
+
const at = row.time === undefined ? '--:--:--' : formatEventTime(row.time);
|
|
756
|
+
const exit = row.exitCode === undefined ? '' : ` exit=${String(row.exitCode)}`;
|
|
757
|
+
const compose = row.composeProject === undefined ? '' : ` compose=${row.composeProject}`;
|
|
755
758
|
return `\n- ${at} ${row.name} ${row.action}${exit}${compose} image=${row.image}`;
|
|
756
759
|
}).join('');
|
|
757
760
|
};
|
|
@@ -775,22 +778,25 @@ const plugin = definePlugin({
|
|
|
775
778
|
if (rows.length === 0)
|
|
776
779
|
return `目标 ${target}:没有网络。`;
|
|
777
780
|
return `目标 ${target} 的网络(${String(rows.length)} 个):` + rows.map((row) => {
|
|
778
|
-
const internal = row.internal ? ' internal=true' : '';
|
|
779
|
-
return `\n- ${row.name} driver=${row.driver} scope=${row.scope}${internal} id=${row.
|
|
781
|
+
const internal = row.internal === true ? ' internal=true' : '';
|
|
782
|
+
return `\n- ${row.name} driver=${row.driver} scope=${row.scope}${internal} id=${row.id}`;
|
|
780
783
|
}).join('');
|
|
781
784
|
};
|
|
782
785
|
const renderVolumes = (target, rows) => {
|
|
783
786
|
if (rows.length === 0)
|
|
784
787
|
return `目标 ${target}:没有卷。`;
|
|
785
788
|
return `目标 ${target} 的卷(${String(rows.length)} 个):` + rows.map((row) => {
|
|
786
|
-
const mount = row.mountpoint === '' ? '' : ` mount=${row.mountpoint}`;
|
|
789
|
+
const mount = row.mountpoint === undefined || row.mountpoint === '' ? '' : ` mount=${row.mountpoint}`;
|
|
787
790
|
return `\n- ${row.name} driver=${row.driver} scope=${row.scope}${mount}`;
|
|
788
791
|
}).join('');
|
|
789
792
|
};
|
|
790
793
|
const renderImages = (target, rows) => {
|
|
791
794
|
if (rows.length === 0)
|
|
792
795
|
return `目标 ${target}:没有镜像。`;
|
|
793
|
-
return `目标 ${target} 的镜像(${String(rows.length)} 个):` + rows.map((row) =>
|
|
796
|
+
return `目标 ${target} 的镜像(${String(rows.length)} 个):` + rows.map((row) => {
|
|
797
|
+
const created = row.createdSince === undefined || row.createdSince === '' ? '' : ' (' + row.createdSince + ')';
|
|
798
|
+
return `\n- ${row.reference} ${row.sizeText}${created} id=${row.id}`;
|
|
799
|
+
}).join('');
|
|
794
800
|
};
|
|
795
801
|
/** 镜像详情 + 构建历史(agent 工具 docker_image_inspect 的可读渲染)。 */
|
|
796
802
|
const renderImageDetail = (payload) => {
|
|
@@ -1749,6 +1755,21 @@ const plugin = definePlugin({
|
|
|
1749
1755
|
}
|
|
1750
1756
|
};
|
|
1751
1757
|
// tools 服务(可选):拿到后注册一次,能力开关变化时 refreshTools 重注册
|
|
1758
|
+
/*
|
|
1759
|
+
* 官方凭据层(可选,`@deepseek-ai/dsh-credentials` 提供的 `ctx.credentials`)。
|
|
1760
|
+
*
|
|
1761
|
+
* 配置里的 `env:NAME` 是一个**引用**(这正是官方的模式:配置只持引用,值归 provider),
|
|
1762
|
+
* 而 provider 会叠 `file`(`$DSH_HOME/.credentials.yaml`)/ `env` / `project-env` /
|
|
1763
|
+
* `user-env` 各层,并保证「每次操作重新解析」——改完下一个操作即生效,不必重启宿主。
|
|
1764
|
+
* 我们从前直接读 `process.env`,等于只认其中一层,而且拿的是进程启动时的快照。
|
|
1765
|
+
*
|
|
1766
|
+
* 服务缺失(老宿主 / 未装该 bundle)时不注册,`resolveSecret` 自己退回 `process.env`,
|
|
1767
|
+
* 行为与从前一致——所以这是**可选**依赖,不抬高 engines 下限。
|
|
1768
|
+
*/
|
|
1769
|
+
ctx.inject(['credentials'], (credCtx) => {
|
|
1770
|
+
setCredentialResolver(credCtx.credentials ?? null);
|
|
1771
|
+
return () => { setCredentialResolver(null); };
|
|
1772
|
+
});
|
|
1752
1773
|
ctx.inject(['tools'], (toolsCtx) => {
|
|
1753
1774
|
toolsCtx.effect(() => {
|
|
1754
1775
|
toolsApi = toolsCtx.tools;
|