@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/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:VAR` 引用(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((row) => {
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 === null ? '' : `exit=${String(item.exitCode)}`,
724
- item.restartCount === null ? '' : `restarts=${String(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
- return `\n- ${item.name} [${item.state}${item.health === null ? '' : '/' + item.health}] ${reasons} image=${item.image}${extra === '' ? '' : ' ' + extra} id=${item.shortId}`;
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((row) => {
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 === null ? '?' : String(row.cpuPercent) + '%'} mem=${row.memUsage} (${row.memPercent === null ? '?' : String(row.memPercent) + '%'}) net=${row.netIO} block=${row.blockIO} pids=${row.pids === null ? '?' : String(row.pids)}`).join('');
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 === null ? '--:--:--' : formatEventTime(row.time);
753
- const exit = row.exitCode === null ? '' : ` exit=${String(row.exitCode)}`;
754
- const compose = row.composeProject === null ? '' : ` 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.shortId}`;
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) => `\n- ${row.reference} ${row.sizeText}${row.createdSince === '' ? '' : ' (' + row.createdSince + ')'} id=${row.shortId}`).join('');
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;