dsh-app-deploy 0.3.0 → 0.3.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 CHANGED
@@ -31,7 +31,7 @@
31
31
  插件发布在 npm,通过 DSH 插件管理器直接安装:
32
32
 
33
33
  ```text
34
- plugin_manager → action: install_bundle → target: dsh-app-deploy@0.3.0
34
+ plugin_manager → action: install_bundle → target: dsh-app-deploy@0.3.1
35
35
  ```
36
36
 
37
37
  安装后刷新 DSH Web,左侧导航出现「部署」入口。
@@ -49,6 +49,10 @@ plugin_manager → action: install_bundle → target: dsh-app-deploy@0.3.0
49
49
 
50
50
  新装插件默认为空配置(不播种),环境通过「+ 添加环境」或「导入」创建。
51
51
 
52
+ ## AI 工具
53
+
54
+ 插件向 dsh Agent 注册 4 个工具:`deploy_list_envs`(列出环境与模块,密码已剥离)、`deploy_run`(执行部署并返回日志)、`deploy_history`(执行记录)、`ssh_test_connection`(SSH 连通性测试)。可直接让 AI 代查发布状态或执行部署。
55
+
52
56
  ## 使用
53
57
 
54
58
  单模块部署流程:
package/index.js CHANGED
@@ -7,6 +7,7 @@ import { initConfig } from './server/config.js'
7
7
  import { initHistory } from './server/history.js'
8
8
  import { handleRpc } from './server/rpc.js'
9
9
  import { handleDeploy, killActiveProcess } from './server/deployment.js'
10
+ import { registerTools } from './server/tools.js'
10
11
 
11
12
  // ponytail: linked workspace cannot resolve dsh's own packages; resolve from the dsh entry instead
12
13
  function hostRequire() {
@@ -22,7 +23,7 @@ const host = hostRequire()
22
23
  const schema = host('@deepseek-ai/schemastery')
23
24
  const z = schema.z ?? schema
24
25
 
25
- export const inject = ['webServer']
26
+ export const inject = ['webServer', 'tools']
26
27
  export const Config = z.object({
27
28
  dataDir: z.string().default(''),
28
29
  defaultProjectPath: z.string().default(''),
@@ -59,13 +60,19 @@ function resolveDataDir(config) {
59
60
 
60
61
  export function apply(ctx, config) {
61
62
  const dataDir = resolveDataDir(config)
62
- // 延迟初始化:apply 内不 await,首个请求时才读数据目录/初始化配置
63
+ // 延迟初始化:apply 内不 await,首个请求/工具调用时才读数据目录/初始化配置
63
64
  let ready
64
65
  const ensure = () => (ready ??= (async () => {
65
66
  await mkdir(dataDir, { recursive: true })
66
67
  await initConfig({ dataDir, defaultProjectPath: config?.defaultProjectPath || '' })
67
68
  await initHistory(dataDir)
68
69
  })())
70
+ // AI 工具:列出环境 / 执行部署 / 执行记录 / SSH 测试(defineTool 从 dsh 宿主解析,失败不影响面板)
71
+ try {
72
+ registerTools(ctx, host('@deepseek-ai/dsh-tools').defineTool, ensure)
73
+ } catch (err) {
74
+ console.warn('[dsh-app-deploy] 注册 AI 工具失败:', err?.message || err)
75
+ }
69
76
 
70
77
  ctx.effect(() => {
71
78
  const dispose = ctx.webServer.register({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-app-deploy",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "DSH 插件:按环境编排并执行构建(Gradle/Maven/前端)、多 SSH 目标发布、rsync/rclone 同步与远程命令的部署面板,支持连接测试,实时推送日志并保留执行记录。",
5
5
  "repository": "btboys/dsh-app-deploy",
6
6
  "type": "module",
@@ -463,4 +463,45 @@ export async function handleDeploy(req, res, url) {
463
463
  }
464
464
  }
465
465
 
466
+ // 供 AI 工具调用:同步执行一次部署并返回结果;与 SSE 入口共用部署锁、执行记录与日志尾部
467
+ export async function runDeploy(envId, moduleIds = []) {
468
+ const env = getEnvs().find(e => e.id === envId)
469
+ if (!env) throw new Error(`未知环境: ${envId}`)
470
+ if (running && !running.done) throw new Error(`已有部署进行中(${running.label}),请稍后再试`)
471
+ const want = (moduleIds || []).filter(Boolean)
472
+ const enabled = env.modules.filter(m => m.enabled)
473
+ // 未指定模块 = 全部启用模块(按配置顺序);指定则按给定顺序
474
+ const mods = want.length
475
+ ? want.map(id => enabled.find(m => m.id === id)).filter(Boolean)
476
+ : enabled
477
+ if (!mods.length) throw new Error('没有可部署的启用模块(检查模块 ID 与启用状态)')
478
+ if (want.length && mods.length !== want.length) {
479
+ const missing = want.filter(id => !enabled.some(m => m.id === id))
480
+ throw new Error(`模块不可用或已禁用: ${missing.join('、')}`)
481
+ }
482
+ const runTargets = [...new Set(mods.map(m => targetFor(env, m)))]
483
+ if (runTargets.some(t => !t.host || !t.user)) throw new Error('SSH 目标配置不完整')
484
+
485
+ const rec = {
486
+ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
487
+ envId: env.id, envName: env.name,
488
+ modules: mods.map(m => ({ id: m.id, name: m.name })),
489
+ startTime: Date.now(), endTime: null, durationMs: null,
490
+ status: 'running', exitCode: null, failedModule: null, failedStep: null, logTail: '',
491
+ }
492
+ getHistory().push(rec)
493
+ saveHistory().catch(() => {})
494
+ running = { clients: new Set(), done: false, label: `${env.name} / ${mods.map(m => m.name).join('+')}`, record: rec, logTail: '', lastCode: null }
495
+ broadcast(`>>> 开始部署 [AI 工具] ${env.name} / ${mods.map(m => m.name).join('+')}\n`)
496
+ await runDeployment(env, mods) // 结束后 running.done 已置位、rec 已补齐
497
+ return {
498
+ code: rec.exitCode,
499
+ status: rec.status,
500
+ durationMs: rec.durationMs,
501
+ failedModule: rec.failedModule,
502
+ failedStep: rec.failedStep,
503
+ logTail: rec.logTail,
504
+ }
505
+ }
506
+
466
507
  export function activeRecord() { return running && !running.done ? running.record : null }
@@ -0,0 +1,138 @@
1
+ // AI 可调用的部署工具:列出环境、执行部署、查执行记录、测试 SSH 连接
2
+ // 密码绝不暴露给模型(只返回 hasPassword 标记);密钥路径是本机路径,可返回
3
+ import { getEnvs } from './config.js'
4
+ import { runDeploy, testSsh, activeRecord } from './deployment.js'
5
+ import { getHistory } from './history.js'
6
+
7
+ // 环境摘要:供模型决定部署目标;剥离 password
8
+ export function envSummaryForModel(e) {
9
+ return {
10
+ id: e.id, name: e.name, note: e.note, group: e.group,
11
+ projectPath: e.projectPath, jdkHome: e.jdkHome || '', nodeHome: e.nodeHome || '', pmPath: e.pmPath || '',
12
+ sshs: (e.sshs || []).map(s => ({
13
+ id: s.id, name: s.name, host: s.host, port: s.port, user: s.user,
14
+ hasPassword: !!s.password, keyFile: s.keyFile || '', options: s.options || '',
15
+ })),
16
+ modules: (e.modules || []).map(m => ({
17
+ id: m.id, name: m.name, kind: m.kind, enabled: m.enabled, buildCmd: m.buildCmd, sshId: m.sshId || '',
18
+ localPath: m.localPath, remotePath: m.remotePath, postCommand: m.postCommand, healthCheckUrl: m.healthCheckUrl || '',
19
+ })),
20
+ }
21
+ }
22
+
23
+ const jsonRender = (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }]
24
+
25
+ /**
26
+ * 向 ctx.tools 注册 4 个工具。defineTool 由宿主解析(@deepseek-ai/dsh-tools),
27
+ * 插件自身不依赖该包,保证脱离 dsh 宿主时模块仍可加载(如单元测试)。
28
+ * @param {object} ctx cordis Context(需注入 tools 服务)
29
+ * @param {object} defineTool @deepseek-ai/dsh-tools 的 defineTool
30
+ * @param {() => Promise<void>} ensure 插件初始化(数据目录/配置/历史);工具调用不走 HTTP 路由,必须显式等待
31
+ */
32
+ export function registerTools(ctx, defineTool, ensure) {
33
+ // 每个工具执行前确保初始化完成(幂等,首次才真正读盘)
34
+ const ready = () => (ensure ? ensure() : Promise.resolve())
35
+ ctx.tools.register(defineTool({
36
+ name: 'deploy_list_envs',
37
+ description: 'List deploy environments configured in the dsh-app-deploy plugin: SSH targets (passwords stripped), build modules with their build commands, artifact paths and publish targets. Use before deploy_run to pick env/module IDs.',
38
+ parameters: {},
39
+ output: {
40
+ schema: {
41
+ type: 'object', additionalProperties: false,
42
+ properties: {
43
+ active: { type: 'object', additionalProperties: true }, // 进行中的部署(无则不返回)
44
+ envs: { type: 'array', required: true, items: { type: 'object', additionalProperties: true } },
45
+ },
46
+ },
47
+ render: jsonRender,
48
+ },
49
+ execute: async () => {
50
+ await ready()
51
+ const rec = activeRecord()
52
+ return {
53
+ ...(rec ? { active: { id: rec.id, env: rec.envName, modules: rec.modules } } : {}),
54
+ envs: getEnvs().map(envSummaryForModel),
55
+ }
56
+ },
57
+ }))
58
+
59
+ ctx.tools.register(defineTool({
60
+ name: 'deploy_run',
61
+ description: 'Run a deployment for one environment: clean → build each module (generic command: gradlew/mvn/npm…) → rsync/rclone to the module\'s SSH target → remote command → health check. Blocks until finished and returns the exit code plus the full log tail. Fails if another deployment is running. Omit modules to deploy all enabled modules in config order.',
62
+ parameters: {
63
+ env: { type: 'string', required: true, description: 'Environment ID (from deploy_list_envs)' },
64
+ modules: { type: 'array', items: { type: 'string' }, description: 'Module IDs in desired execution order. Omit for all enabled modules.' },
65
+ },
66
+ output: {
67
+ schema: {
68
+ type: 'object', additionalProperties: false,
69
+ properties: {
70
+ code: { type: 'integer', required: true },
71
+ status: { type: 'string', required: true },
72
+ durationMs: { type: 'integer' },
73
+ failedModule: { type: 'string' },
74
+ failedStep: { type: 'string' },
75
+ logTail: { type: 'string', required: true },
76
+ },
77
+ },
78
+ render: jsonRender,
79
+ },
80
+ execute: async (args) => {
81
+ await ready()
82
+ return runDeploy(args.env, args.modules || [])
83
+ },
84
+ }))
85
+
86
+ ctx.tools.register(defineTool({
87
+ name: 'deploy_history',
88
+ description: 'Recent deployment records (newest first): status, duration, per-module failure step, and a truncated log tail for failed runs. Filter by environment ID optionally.',
89
+ parameters: {
90
+ env: { type: 'string', description: 'Environment ID to filter by' },
91
+ limit: { type: 'integer', description: 'Max records, default 10' },
92
+ },
93
+ output: {
94
+ schema: {
95
+ type: 'object', additionalProperties: false,
96
+ properties: {
97
+ records: { type: 'array', required: true, items: { type: 'object', additionalProperties: true } },
98
+ },
99
+ },
100
+ render: jsonRender,
101
+ },
102
+ execute: async (args) => {
103
+ await ready()
104
+ const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 50)
105
+ let list = getHistory().slice()
106
+ if (args.env) list = list.filter(r => r.envId === args.env)
107
+ return { records: list.slice(-limit).reverse() }
108
+ },
109
+ }))
110
+
111
+ ctx.tools.register(defineTool({
112
+ name: 'ssh_test_connection',
113
+ description: 'Test an SSH connection (10s timeout): host key/agent auth via keyFile, or password via sshpass. Returns ok/latency or the server-reported error.',
114
+ parameters: {
115
+ host: { type: 'string', required: true },
116
+ port: { type: 'integer' },
117
+ user: { type: 'string', required: true },
118
+ password: { type: 'string', description: 'SSH password; omit to use keyFile or agent' },
119
+ keyFile: { type: 'string', description: 'Absolute private key path; takes precedence over options -i' },
120
+ options: { type: 'string', description: 'Extra ssh options, e.g. -o StrictHostKeyChecking=no' },
121
+ },
122
+ output: {
123
+ schema: {
124
+ type: 'object', additionalProperties: false,
125
+ properties: {
126
+ ok: { type: 'boolean', required: true },
127
+ ms: { type: 'integer' },
128
+ error: { type: 'string' },
129
+ },
130
+ },
131
+ render: jsonRender,
132
+ },
133
+ execute: async (args) => {
134
+ await ready()
135
+ return testSsh({ host: args.host, port: args.port || 22, user: args.user, password: args.password || '', keyFile: args.keyFile || '', options: args.options || '' })
136
+ },
137
+ }))
138
+ }