ai-engineering-init 1.17.0 → 1.17.2

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.
@@ -117,6 +117,7 @@ const instructions = `## 强制技能激活流程(必须执行)
117
117
  - skill-creator: 创建技能模板/技能脚手架/skill scaffold
118
118
  - leniu-report-scenario: 报表/报表开发/报表查询/报表导出/合计行/totalLine/汇总报表/定制报表/report_order_info/金额处理/分转元/餐次/mealtime/ReportBaseTotalVO/CustomNumberConverter
119
119
  - leniu-marketing-scenario: 营销/营销规则/消费规则/计价规则/充值规则/扣款规则/就餐规则/折扣/满减/限额/限次/补贴/赠送/满赠/管理费/MarketApi/RulePriceHandler/RuleRechargeHandler/RulePayHandler/MarketRule/规则定制
120
+ - jenkins-deploy: 打包/部署/Jenkins/构建/Portainer/发布到dev/发布到test/更新环境/自动部署
120
121
 
121
122
  ### 步骤 2 - 激活(逐个调用,等待每个完成)
122
123
 
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: jenkins-deploy
3
+ description: |
4
+ Jenkins + Portainer 自动打包部署技能。通过 Python 脚本调用 Jenkins API 构建项目,并触发 Portainer Webhook/Update 完成容器更新。
5
+
6
+ 触发场景:
7
+ - 需要将代码打包部署到 dev/test 环境
8
+ - 需要触发 Jenkins 构建 core 或 api 项目
9
+ - 需要更新 Portainer 容器服务
10
+ - 需要查看或修改构建配置(分支、环境、模式)
11
+ - 定制项目的打包部署
12
+
13
+ 触发词:打包、部署、Jenkins、构建、Portainer、发布到dev、发布到test、更新环境、自动部署
14
+ ---
15
+
16
+ # Jenkins + Portainer 自动打包部署
17
+
18
+ ## 架构
19
+
20
+ ```
21
+ 技能目录(随框架更新,不复制到项目):
22
+ ~/.claude/skills/jenkins-deploy/assets/jk_build.py ← 构建脚本
23
+
24
+ 全局配置(一次性配置,所有项目共享):
25
+ ~/.claude/jenkins-config.json ← 凭证(Jenkins/Portainer)
26
+
27
+ 项目本地(自动生成):
28
+ jenkins/last_cd_env.json ← 构建状态(环境、分支、模式)
29
+ ```
30
+
31
+ 脚本按 **本地 `.claude/` > 全局 `~/.claude/`** 优先级查找 `jenkins-config.json`。
32
+
33
+ ## 构建模式
34
+
35
+ | 模式 | 说明 | 执行步骤 |
36
+ |------|------|---------|
37
+ | `0` | 只构建 | 构建 core + api,不更新 Portainer |
38
+ | `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
39
+ | `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
40
+ | `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
41
+
42
+ ## 环境支持
43
+
44
+ | 环境 | 前缀 | Portainer 更新方式 |
45
+ |------|------|-------------------|
46
+ | dev1~15 | `dev` | Webhook 触发 |
47
+ | dev16~43 | `dev` | Force Update(xnzn-dev.xnzn.net) |
48
+ | dev44+ | `dev` | 只支持模式 0(手动更新) |
49
+ | test | `test` | Webhook 触发 |
50
+
51
+ ## 使用方式
52
+
53
+ ### 运行构建脚本
54
+
55
+ ```bash
56
+ python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
57
+ ```
58
+
59
+ 脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
60
+
61
+ ### 通过 AI 辅助部署
62
+
63
+ 当用户说"打包到 devX"时:
64
+
65
+ 1. 读取 `jenkins/last_cd_env.json` 获取上次参数
66
+ 2. 确认参数:环境、分支、模式
67
+ 3. 修改 `jenkins/last_cd_env.json` 写入新参数
68
+ 4. 执行 `python ~/.claude/skills/jenkins-deploy/assets/jk_build.py`
69
+
70
+ ## 配置文件
71
+
72
+ ### jenkins-config.json(凭证,全局)
73
+
74
+ 位置:`~/.claude/jenkins-config.json`(或本地 `.claude/jenkins-config.json`)
75
+
76
+ 模板:`.claude/skills/jenkins-deploy/assets/env_param.template.json`
77
+
78
+ > **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
79
+
80
+ ### last_cd_env.json(构建状态,项目本地)
81
+
82
+ 位置:`jenkins/last_cd_env.json`(脚本自动创建,无需手动初始化)
83
+
84
+ ```json
85
+ {
86
+ "build_mode": "1",
87
+ "cd_env": "dev1",
88
+ "core_param_branch": "master",
89
+ "api_param_branch": "master",
90
+ "api_param_folder": null
91
+ }
92
+ ```
93
+
94
+ ## 定制项目
95
+
96
+ 指定 `api_param_folder` 后,Jenkins Job 路径变为:
97
+
98
+ ```
99
+ {folder_name}/dev-后端-core
100
+ {folder_name}/dev-后端-api
101
+ ```
102
+
103
+ ## 首次初始化(团队成员)
104
+
105
+ 只需一步:配置全局凭证文件。
106
+
107
+ ```bash
108
+ # 方式 1:从团队成员拷贝
109
+ cp /path/to/teammate/jenkins-config.json ~/.claude/jenkins-config.json
110
+
111
+ # 方式 2:从模板创建,手动填写凭证
112
+ cp ~/.claude/skills/jenkins-deploy/assets/env_param.template.json ~/.claude/jenkins-config.json
113
+ # 然后替换 __JENKINS_*__ 和 __PORTAINER_*__ 占位符
114
+ ```
115
+
116
+ ### AI 初始化行为
117
+
118
+ 当技能被触发但 `jenkins-config.json` 不存在时:
119
+ 1. 提示"检测到尚未配置 Jenkins 凭证"
120
+ 2. 询问是否从模板创建
121
+ 3. 复制模板到 `~/.claude/jenkins-config.json`
122
+ 4. 提示用户填写凭证(或拷贝已有配置)
123
+
124
+ ## 依赖
125
+
126
+ ```bash
127
+ pip install python-jenkins requests
128
+ ```
129
+
130
+ ## 注意
131
+
132
+ - 本技能用于 dev/test 环境部署,**不涉及生产环境**
133
+ - 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
134
+ - 如果是代码构建错误排查,请使用 `bug-detective` 技能
@@ -0,0 +1,51 @@
1
+ {
2
+ "portainer_api_url": {
3
+ "dev": "https://devops-dev.xnzn.net/api",
4
+ "dev16": "https://xnzn-dev.xnzn.net/api",
5
+ "test": "https://devops-test.xnzn.net/api"
6
+ },
7
+ "portainer_user_name": {
8
+ "dev": "__PORTAINER_DEV_USER__",
9
+ "dev16": "__PORTAINER_DEV16_USER__",
10
+ "test": "__PORTAINER_TEST_USER__"
11
+ },
12
+ "portainer_user_pwd": {
13
+ "dev": "__PORTAINER_DEV_PWD__",
14
+ "dev16": "__PORTAINER_DEV16_PWD__",
15
+ "test": "__PORTAINER_TEST_PWD__"
16
+ },
17
+ "portainer_endpoints": {
18
+ "dev": "2",
19
+ "dev16": "1",
20
+ "test": "2"
21
+ },
22
+ "portainer_server_suffix": {
23
+ "dev": "_yunshitang-api",
24
+ "dev16": "_yunshitang-api-offline",
25
+ "test": "_yunshitang-api"
26
+ },
27
+ "core_job": {
28
+ "dev": "dev-tengyun-core",
29
+ "test": "test-tengyun-core"
30
+ },
31
+ "core_job_custom": {
32
+ "dev": "dev-后端-core",
33
+ "test": "test-后端-core"
34
+ },
35
+ "api_job": {
36
+ "dev": "dev-tengyun-yunshitang-api",
37
+ "test": "test-tengyun-yunshitang-api"
38
+ },
39
+ "api_job_custom": {
40
+ "dev": "dev-后端-api",
41
+ "test": "test-后端-api"
42
+ },
43
+ "jenkins_user_id": {
44
+ "dev": "__JENKINS_DEV_USER__",
45
+ "test": "__JENKINS_TEST_USER__"
46
+ },
47
+ "jenkins_api_token": {
48
+ "dev": "__JENKINS_DEV_TOKEN__",
49
+ "test": "__JENKINS_TEST_TOKEN__"
50
+ }
51
+ }
@@ -0,0 +1,400 @@
1
+ # 简单调用jenkins api,构建项目并触发webhook
2
+ import html
3
+ import json
4
+ import os
5
+ import sys
6
+ import time
7
+
8
+ import jenkins
9
+ import requests
10
+
11
+ # 预设默认值(可修改)
12
+ cd_env = '' # dev或test
13
+
14
+ # dev和test环境独立配置
15
+ jenkins_server_url = 'https://ci.xnzn.net' # jenkins地址
16
+ portainer_server_suffix = '' # portainer服务名后缀
17
+ core_timeout = 60 * 10 # core构建超时时间
18
+ api_timeout = 60 * 5 # api构建超时时间
19
+ portainer_user_name = '' # portainer用户名
20
+ portainer_user_pwd = '' # portainer密码
21
+ portainer_api_url = ''
22
+ portainer_endpoints = ''
23
+ jenkins_user_id = ''
24
+ jenkins_api_token = ''
25
+ core_job = ''
26
+ api_job = ''
27
+
28
+
29
+ # ~~~~~~~~~~~~~~~~~ 配置文件查找 ~~~~~~~~~~~~~~~~~~~~~~~~
30
+
31
+ def find_config_file(filename, search_paths):
32
+ """按优先级查找配置文件:本地 > 全局"""
33
+ for p in search_paths:
34
+ filepath = os.path.join(p, filename)
35
+ if os.path.exists(filepath):
36
+ return filepath
37
+ return None
38
+
39
+
40
+ # 配置文件搜索路径
41
+ project_dir = os.getcwd()
42
+ home_claude_dir = os.path.join(os.path.expanduser('~'), '.claude')
43
+
44
+ # jenkins-config.json 搜索路径:项目本地 .claude/ > 全局 ~/.claude/
45
+ config_search_paths = [
46
+ os.path.join(project_dir, '.claude'),
47
+ home_claude_dir,
48
+ ]
49
+
50
+ # last_cd_env.json 搜索路径:项目本地 jenkins/
51
+ state_dir = os.path.join(project_dir, 'jenkins')
52
+
53
+
54
+ # 执行job方法,通过等待时间来判断是否构建成功
55
+ def do_job_by_time(server, job_name, param_branch, param_env, wait_time):
56
+ queue_num = server.build_job(name=job_name,
57
+ parameters={'BRANCH': param_branch, 'VERSION': param_env},
58
+ token=jenkins_api_token)
59
+ print('创建queue... {queue_num}')
60
+ time.sleep(wait_time)
61
+ queue_info = server.get_queue_item(queue_num)
62
+
63
+ print('等待时间到,queue信息:' + str(queue_info))
64
+
65
+
66
+ # 执行job方法,通过请求判断结束,进度更准确
67
+ def do_job(server, job_name, param_branch, param_env, wait_time):
68
+
69
+ queue_num = server.build_job(name=job_name,
70
+ parameters={'BRANCH': param_branch, 'VERSION': param_env},
71
+ token=jenkins_api_token)
72
+ print(f'创建队列... {queue_num}')
73
+ job_number = None
74
+ # 记录开始构建时间
75
+ start_time = time.time()
76
+
77
+ while True:
78
+ time.sleep(10)
79
+ if time.time() - start_time > wait_time:
80
+ print("构建超时,结束")
81
+ exit(1)
82
+ queue_info = server.get_queue_item(queue_num)
83
+ if queue_info['why'] is not None:
84
+ why = queue_info['why']
85
+ print(f'队列等待执行器...{why}')
86
+ elif 'executable' in queue_info:
87
+ job_number = queue_info['executable']['number']
88
+ print(f'开始执行任务...{job_number}')
89
+ break
90
+ else:
91
+ print('执行结束')
92
+ break
93
+
94
+ # 间隔x秒查询构建结果
95
+ while True:
96
+ time.sleep(10)
97
+
98
+ # 如果超过3分钟,直接结束
99
+ if time.time() - start_time > wait_time:
100
+ print("构建超时,结束")
101
+ exit(1)
102
+ headers = {
103
+ "n": str(job_number),
104
+ }
105
+ # 如果job_name包含 '/',则在把'/'替换为/job/
106
+ if '/' in job_name:
107
+ job_name_url = job_name.replace('/', '/job/')
108
+ url = f'{jenkins_server_url}/job/{job_name_url}/buildHistory/ajax'
109
+ else:
110
+ url = f'{jenkins_server_url}/job/{job_name}/buildHistory/ajax'
111
+
112
+ req = requests.Request(method='POST', url=url, headers=headers)
113
+ response = server.jenkins_open(req)
114
+ # 解析response包含的html
115
+ html_str = html.unescape(response)
116
+ if 'Expected build number' in html_str: # 正在等待中
117
+ print(f'\r等待中...', end='')
118
+ continue
119
+ elif ('预计剩余时间' in html_str) or ('Estimated remaining time' in html_str): # 是否包含 预计剩余时间
120
+ try:
121
+ # 取出 '<td style="width:' 和 '%;" class="progress-bar-done"></td>' 之间的字符串
122
+ progress = html_str.split('<td style="width:')[1].split('%;" class="progress-bar-done"></td>')[0]
123
+ # 打印进度更新最后一行
124
+ print(f'\r构建中...{progress}%', end='')
125
+ except IndexError:
126
+ print("无法从HTML字符串中提取进度信息")
127
+ except Exception as e:
128
+ print(f"发生错误: {e}")
129
+ continue
130
+ else:
131
+ print(f"\r构建结束,耗时 {(time.time() - start_time):.2f} 秒", end='\n')
132
+ # 查询构建结果
133
+ build_info = server.get_build_info(job_name, job_number)
134
+ if build_info['result'] == 'SUCCESS':
135
+ print(f"构建成功 {html.unescape(build_info['url'])}")
136
+ else:
137
+ print(f"构建失败({build_info['result']}) {html.unescape(build_info['url'])}")
138
+ print(f"原始 build_info:{build_info}")
139
+ print(f"原始 html:{html_str}")
140
+ return False
141
+ break
142
+ return True
143
+
144
+
145
+ # 获取 Portainer JWT Token
146
+ def get_jwt_token():
147
+ login_url = f"{portainer_api_url}/auth"
148
+ login_data = {
149
+ "Username": portainer_user_name,
150
+ "Password": portainer_user_pwd,
151
+ }
152
+ response = requests.post(login_url, data=json.dumps(login_data))
153
+ token = response.json()["jwt"]
154
+ print('获取token成功')
155
+ return token
156
+
157
+
158
+ # 获取 Service ID
159
+ def get_server_id(token):
160
+ headers = {
161
+ "Authorization": f"Bearer {token}",
162
+ "Content-Type": "application/json"
163
+ }
164
+ url = portainer_api_url + f'/endpoints/{portainer_endpoints}/docker/services'
165
+ services_response = requests.get(url, headers=headers)
166
+ services = services_response.json()
167
+
168
+ service_name = cd_env + portainer_server_suffix
169
+ # Find service with matching name
170
+ target_service_id = None
171
+ for service in services:
172
+ if service["Spec"]["Name"] == service_name:
173
+ target_service_id = service["ID"]
174
+ break
175
+
176
+ print(f'查询{service_name}的service_id:{target_service_id}')
177
+ return target_service_id
178
+
179
+
180
+ # 获取 Service 的 WebHook
181
+ def get_service_web_hook():
182
+ pt_token = get_jwt_token()
183
+ service_id = get_server_id(pt_token)
184
+
185
+ headers = {
186
+ "Authorization": f"Bearer {pt_token}",
187
+ "Content-Type": "application/json"
188
+ }
189
+
190
+ url = portainer_api_url + f'/webhooks?filters=%7B%22ResourceID%22:%22{service_id}%22,%22EndpointID%22:{portainer_endpoints}%7D'
191
+ services_response = requests.get(url, headers=headers)
192
+ webhook_json = services_response.json()
193
+ target_webhook_token = None
194
+ # 是否存在webhook
195
+ if len(webhook_json) > 0:
196
+ target_webhook_token = webhook_json[0]['Token']
197
+ print('获取webhook_token:' + target_webhook_token)
198
+
199
+ # 尝试调用请求创建webhook
200
+ if target_webhook_token is None:
201
+ payload = {"ResourceID": service_id, "EndpointID": 2, "WebhookType": 1, "registryID": 0}
202
+ services_response = requests.post(portainer_api_url + "/webhooks", headers=headers, data=json.dumps(payload))
203
+ webhook_json = services_response.json()
204
+ if webhook_json is not None and 'Token' in webhook_json:
205
+ target_webhook_token = webhook_json['Token']
206
+ print('创建webhook_token结果:' + target_webhook_token)
207
+
208
+ return target_webhook_token
209
+
210
+
211
+ # 启动webhook(dev1-15)
212
+ def trigger_webhook():
213
+
214
+ wh_token = get_service_web_hook()
215
+
216
+ if wh_token is None:
217
+ print(f"{cd_env}环境没有配置webhook,结束")
218
+ exit(0)
219
+ else:
220
+ print("开始触发webhook")
221
+ r = requests.post(portainer_api_url + '/webhooks/' + wh_token)
222
+ print("webhook触发结果:" + str(r.status_code))
223
+
224
+ # 通过service id 更新
225
+ def update_portainer():
226
+
227
+ pt_token = get_jwt_token()
228
+ service_id = get_server_id(pt_token)
229
+
230
+ headers = {
231
+ "Authorization": f"Bearer {pt_token}",
232
+ "Content-Type": "application/json"
233
+ }
234
+
235
+ # 调用更新(PUT请求):
236
+ url = portainer_api_url + f'/endpoints/{portainer_endpoints}/forceupdateservice'
237
+ payload = {
238
+ "serviceID": service_id,
239
+ "pullImage": True,
240
+ }
241
+ services_response = requests.put(url, headers=headers, data=json.dumps(payload))
242
+ update_rsp_json = services_response.json()
243
+ print('更新结果:' + str(update_rsp_json))
244
+
245
+ # ~~~~~~~~~~~~~~~~~~ 开始输入 ~~~~~~~~~~~~~~~~~~~~~~~
246
+
247
+ # 打印欢迎信息
248
+ print('\n~~~~~~~~~~~~~~')
249
+ print('正在使用jenkins+portainer构建工具,目前支持dev和test环境的自动化构建+更新')
250
+ print('~~~~~~~~~~~~~~\n')
251
+
252
+ # 查找环境配置文件(jenkins-config.json)
253
+ config_file = find_config_file('jenkins-config.json', config_search_paths)
254
+ if config_file is None:
255
+ print('❌ 未找到 jenkins-config.json')
256
+ print(f' 请将配置文件放到以下任一位置:')
257
+ for p in config_search_paths:
258
+ print(f' - {os.path.join(p, "jenkins-config.json")}')
259
+ print(f'\n 或运行: npx ai-engineering-init config --type jenkins')
260
+ exit(1)
261
+ print(f'配置文件: {config_file}')
262
+
263
+ # 确保构建状态目录存在
264
+ os.makedirs(state_dir, exist_ok=True)
265
+ state_file = os.path.join(state_dir, 'last_cd_env.json')
266
+
267
+ # 读取上一次构建的环境(如果不存在则使用默认值)
268
+ if os.path.exists(state_file):
269
+ with open(state_file, 'r', encoding='utf-8') as f:
270
+ cd_env_json = json.load(f)
271
+ else:
272
+ cd_env_json = {
273
+ "build_mode": "1",
274
+ "cd_env": "dev1",
275
+ "core_param_branch": "master",
276
+ "api_param_branch": "master",
277
+ "api_param_folder": None
278
+ }
279
+
280
+ # 从终端获取用户输入模式
281
+ build_mode = input(f"请输入模式 0-只构建 1-全构建+更新 2-构建api+更新 3-只更新(预设{cd_env_json['build_mode']}):", ) or cd_env_json['build_mode']
282
+
283
+ # 从终端获取用户输入cd_env
284
+ cd_env = input(f"请输入环境(预设:{cd_env_json['cd_env']}):", ) or cd_env_json['cd_env']
285
+
286
+ # 从终端获取用户输入的分支
287
+ core_param_branch = input(f"请输入core分支(预设:{cd_env_json['core_param_branch']}):") or cd_env_json['core_param_branch']
288
+
289
+ # 从终端输入api分支
290
+ api_param_branch = input(f"请输入api分支(预设:{cd_env_json['api_param_branch']}):") or cd_env_json['api_param_branch']
291
+
292
+ # 从终端输入api文件夹名
293
+ api_param_folder = input(f"请输入定制工程文件夹名(空格或None表示不需要,预设:{cd_env_json['api_param_folder']}):") or cd_env_json['api_param_folder']
294
+
295
+ # 校验模式:
296
+ if build_mode is None or build_mode not in ('0', '1', '2', '3'):
297
+ print("模式错误,结束")
298
+ exit(1)
299
+ # 校验环境,如果不是dev或test开头的,直接退出
300
+ if cd_env is None or not cd_env.startswith('dev') and not cd_env.startswith('test'):
301
+ print("环境错误,结束")
302
+ exit(1)
303
+ # 校验分支
304
+ if core_param_branch is None:
305
+ print("分支错误,结束")
306
+ exit(1)
307
+ if api_param_branch is None:
308
+ print("分支错误,结束")
309
+ exit(1)
310
+ if api_param_folder is None or api_param_folder == '' or api_param_folder == ' ' or api_param_folder == 'none' or api_param_folder == 'None':
311
+ api_param_folder = None
312
+ # 如果dev44及以上环境,只能使用模式0:
313
+ if cd_env.startswith('dev') and int(cd_env[3:]) >= 44 and build_mode != '0':
314
+ print("dev43及以上环境只能使用模式0,结束")
315
+ exit(1)
316
+
317
+ # 保存到本地状态文件
318
+ cd_env_json['cd_env'] = cd_env
319
+ cd_env_json['core_param_branch'] = core_param_branch
320
+ cd_env_json['api_param_branch'] = api_param_branch
321
+ cd_env_json['api_param_folder'] = api_param_folder
322
+ cd_env_json['build_mode'] = build_mode
323
+ json.dump(cd_env_json, open(state_file, 'w', encoding='utf-8'), ensure_ascii=False)
324
+
325
+ # 根据环境输入,设置前缀
326
+ env_prefix = 'dev' if cd_env.startswith('dev') else 'test'
327
+
328
+ # 读取环境配置(从全局或本地 jenkins-config.json)
329
+ with open(config_file, 'r', encoding='utf-8') as f:
330
+ env_param = json.load(f)
331
+
332
+ jenkins_user_id = env_param['jenkins_user_id'][env_prefix]
333
+ jenkins_api_token = env_param['jenkins_api_token'][env_prefix]
334
+ core_job = env_param['core_job'][env_prefix]
335
+ api_job = env_param['api_job'][env_prefix]
336
+ if api_param_folder is not None:
337
+ core_job = api_param_folder + '/' + env_param['core_job_custom'][env_prefix]
338
+ api_job = api_param_folder + '/' + env_param['api_job_custom'][env_prefix]
339
+
340
+ # portainer配置(如果cd_env > dev15,使用另一套配置)
341
+ if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
342
+ portainer_user_name = env_param['portainer_user_name']['dev16']
343
+ portainer_user_pwd = env_param['portainer_user_pwd']['dev16']
344
+ portainer_api_url = env_param['portainer_api_url']['dev16']
345
+ portainer_endpoints = env_param['portainer_endpoints']['dev16']
346
+ portainer_server_suffix = env_param['portainer_server_suffix']['dev16']
347
+ else:
348
+ portainer_api_url = env_param['portainer_api_url'][env_prefix]
349
+ portainer_user_name = env_param['portainer_user_name'][env_prefix]
350
+ portainer_user_pwd = env_param['portainer_user_pwd'][env_prefix]
351
+ portainer_endpoints = env_param['portainer_endpoints'][env_prefix]
352
+ portainer_server_suffix = env_param['portainer_server_suffix'][env_prefix]
353
+
354
+ # ~~~~~~~~~~~~~~~~~ 开始打包 ~~~~~~~~~~~~~~~~~~~~~~~~
355
+
356
+ print('\n~~~~~~~~~~~~~~\n')
357
+ print('开始打包')
358
+ print("链接jenkins:" + jenkins_server_url)
359
+
360
+ # 实例化jenkins对象,连接远程的jenkins master server
361
+ j_server = jenkins.Jenkins(url=jenkins_server_url, username=jenkins_user_id, password=jenkins_api_token)
362
+ print("jenkins信息:" + j_server.get_whoami()['fullName'])
363
+
364
+ # 通过参数构建core
365
+ if int(build_mode) < 2:
366
+ print('开始构建core:' + core_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
367
+ rs = do_job(j_server, core_job, core_param_branch, cd_env, core_timeout)
368
+ if rs is False:
369
+ print('构建core失败,结束')
370
+ exit(1)
371
+ time.sleep(10)
372
+
373
+ if int(build_mode) < 3:
374
+ print('开始构建api:' + api_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
375
+ rs = do_job(j_server, api_job, api_param_branch, cd_env, api_timeout)
376
+ if rs is False:
377
+ print('构建api失败,结束')
378
+ exit(1)
379
+ time.sleep(2)
380
+
381
+ print('结束打包' + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
382
+
383
+ # ~~~~~~~~~~~~~~~~~ 开始更新 ~~~~~~~~~~~~~~~~~~~~~~~~
384
+
385
+ # 开始更新portainer
386
+ if int(build_mode) == 0:
387
+ print('\n~~~~~~~ 请自行更新portainer ~~~~~~~\n')
388
+ else:
389
+ print('\n~~~~~~~~~~~~~~\n')
390
+ print('开始更新portainer:' + portainer_api_url + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
391
+ # portainer配置(如果cd_env > dev15,使用update)
392
+ if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
393
+ # 通过update更新
394
+ update_portainer()
395
+ else:
396
+ # 触发webhook
397
+ trigger_webhook()
398
+
399
+
400
+ exit(0)
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: jenkins-deploy
3
+ description: |
4
+ Jenkins + Portainer 自动打包部署技能。通过 Python 脚本调用 Jenkins API 构建项目,并触发 Portainer Webhook/Update 完成容器更新。
5
+
6
+ 触发场景:
7
+ - 需要将代码打包部署到 dev/test 环境
8
+ - 需要触发 Jenkins 构建 core 或 api 项目
9
+ - 需要更新 Portainer 容器服务
10
+ - 需要查看或修改构建配置(分支、环境、模式)
11
+ - 定制项目的打包部署
12
+
13
+ 触发词:打包、部署、Jenkins、构建、Portainer、发布到dev、发布到test、更新环境、自动部署
14
+ ---
15
+
16
+ # Jenkins + Portainer 自动打包部署
17
+
18
+ ## 架构
19
+
20
+ ```
21
+ 技能目录(随框架更新,不复制到项目):
22
+ ~/.claude/skills/jenkins-deploy/assets/jk_build.py ← 构建脚本
23
+
24
+ 全局配置(一次性配置,所有项目共享):
25
+ ~/.claude/jenkins-config.json ← 凭证(Jenkins/Portainer)
26
+
27
+ 项目本地(自动生成):
28
+ jenkins/last_cd_env.json ← 构建状态(环境、分支、模式)
29
+ ```
30
+
31
+ 脚本按 **本地 `.claude/` > 全局 `~/.claude/`** 优先级查找 `jenkins-config.json`。
32
+
33
+ ## 构建模式
34
+
35
+ | 模式 | 说明 | 执行步骤 |
36
+ |------|------|---------|
37
+ | `0` | 只构建 | 构建 core + api,不更新 Portainer |
38
+ | `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
39
+ | `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
40
+ | `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
41
+
42
+ ## 环境支持
43
+
44
+ | 环境 | 前缀 | Portainer 更新方式 |
45
+ |------|------|-------------------|
46
+ | dev1~15 | `dev` | Webhook 触发 |
47
+ | dev16~43 | `dev` | Force Update(xnzn-dev.xnzn.net) |
48
+ | dev44+ | `dev` | 只支持模式 0(手动更新) |
49
+ | test | `test` | Webhook 触发 |
50
+
51
+ ## 使用方式
52
+
53
+ ### 运行构建脚本
54
+
55
+ ```bash
56
+ python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
57
+ ```
58
+
59
+ 脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
60
+
61
+ ### 通过 AI 辅助部署
62
+
63
+ 当用户说"打包到 devX"时:
64
+
65
+ 1. 读取 `jenkins/last_cd_env.json` 获取上次参数
66
+ 2. 确认参数:环境、分支、模式
67
+ 3. 修改 `jenkins/last_cd_env.json` 写入新参数
68
+ 4. 执行 `python ~/.claude/skills/jenkins-deploy/assets/jk_build.py`
69
+
70
+ ## 配置文件
71
+
72
+ ### jenkins-config.json(凭证,全局)
73
+
74
+ 位置:`~/.claude/jenkins-config.json`(或本地 `.claude/jenkins-config.json`)
75
+
76
+ 模板:`.claude/skills/jenkins-deploy/assets/env_param.template.json`
77
+
78
+ > **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
79
+
80
+ ### last_cd_env.json(构建状态,项目本地)
81
+
82
+ 位置:`jenkins/last_cd_env.json`(脚本自动创建,无需手动初始化)
83
+
84
+ ```json
85
+ {
86
+ "build_mode": "1",
87
+ "cd_env": "dev1",
88
+ "core_param_branch": "master",
89
+ "api_param_branch": "master",
90
+ "api_param_folder": null
91
+ }
92
+ ```
93
+
94
+ ## 定制项目
95
+
96
+ 指定 `api_param_folder` 后,Jenkins Job 路径变为:
97
+
98
+ ```
99
+ {folder_name}/dev-后端-core
100
+ {folder_name}/dev-后端-api
101
+ ```
102
+
103
+ ## 首次初始化(团队成员)
104
+
105
+ 只需一步:配置全局凭证文件。
106
+
107
+ ```bash
108
+ # 方式 1:从团队成员拷贝
109
+ cp /path/to/teammate/jenkins-config.json ~/.claude/jenkins-config.json
110
+
111
+ # 方式 2:从模板创建,手动填写凭证
112
+ cp ~/.claude/skills/jenkins-deploy/assets/env_param.template.json ~/.claude/jenkins-config.json
113
+ # 然后替换 __JENKINS_*__ 和 __PORTAINER_*__ 占位符
114
+ ```
115
+
116
+ ### AI 初始化行为
117
+
118
+ 当技能被触发但 `jenkins-config.json` 不存在时:
119
+ 1. 提示"检测到尚未配置 Jenkins 凭证"
120
+ 2. 询问是否从模板创建
121
+ 3. 复制模板到 `~/.claude/jenkins-config.json`
122
+ 4. 提示用户填写凭证(或拷贝已有配置)
123
+
124
+ ## 依赖
125
+
126
+ ```bash
127
+ pip install python-jenkins requests
128
+ ```
129
+
130
+ ## 注意
131
+
132
+ - 本技能用于 dev/test 环境部署,**不涉及生产环境**
133
+ - 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
134
+ - 如果是代码构建错误排查,请使用 `bug-detective` 技能
@@ -417,6 +417,10 @@ const skillMap = [
417
417
  {
418
418
  name: 'skill-creator',
419
419
  keywords: ['创建技能', '技能模板', '技能脚手架', 'skill scaffold', '新建skill']
420
+ },
421
+ {
422
+ name: 'jenkins-deploy',
423
+ keywords: ['打包', '部署', 'Jenkins', '构建', 'Portainer', '发布到dev', '发布到test', '更新环境', '自动部署']
420
424
  }
421
425
  ];
422
426
 
@@ -87,6 +87,7 @@ alwaysApply: true
87
87
  | 多租户/租户隔离/TenantEntity/tenantId/SaaS | `.cursor/skills/tenant-management/SKILL.md` |
88
88
  | Element UI/前端组件/el-table/el-form/管理页面 | `.cursor/skills/ui-pc/SKILL.md` |
89
89
  | Vuex/store/mapState/mapActions/状态管理 | `.cursor/skills/store-pc/SKILL.md` |
90
+ | 打包/部署/Jenkins/构建/Portainer/发布到dev/发布到test/自动部署 | `.cursor/skills/jenkins-deploy/SKILL.md` |
90
91
 
91
92
  ### 执行规则
92
93
 
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: jenkins-deploy
3
+ description: |
4
+ Jenkins + Portainer 自动打包部署技能。通过 Python 脚本调用 Jenkins API 构建项目,并触发 Portainer Webhook/Update 完成容器更新。
5
+
6
+ 触发场景:
7
+ - 需要将代码打包部署到 dev/test 环境
8
+ - 需要触发 Jenkins 构建 core 或 api 项目
9
+ - 需要更新 Portainer 容器服务
10
+ - 需要查看或修改构建配置(分支、环境、模式)
11
+ - 定制项目的打包部署
12
+
13
+ 触发词:打包、部署、Jenkins、构建、Portainer、发布到dev、发布到test、更新环境、自动部署
14
+ ---
15
+
16
+ # Jenkins + Portainer 自动打包部署
17
+
18
+ ## 架构
19
+
20
+ ```
21
+ 技能目录(随框架更新,不复制到项目):
22
+ ~/.claude/skills/jenkins-deploy/assets/jk_build.py ← 构建脚本
23
+
24
+ 全局配置(一次性配置,所有项目共享):
25
+ ~/.claude/jenkins-config.json ← 凭证(Jenkins/Portainer)
26
+
27
+ 项目本地(自动生成):
28
+ jenkins/last_cd_env.json ← 构建状态(环境、分支、模式)
29
+ ```
30
+
31
+ 脚本按 **本地 `.claude/` > 全局 `~/.claude/`** 优先级查找 `jenkins-config.json`。
32
+
33
+ ## 构建模式
34
+
35
+ | 模式 | 说明 | 执行步骤 |
36
+ |------|------|---------|
37
+ | `0` | 只构建 | 构建 core + api,不更新 Portainer |
38
+ | `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
39
+ | `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
40
+ | `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
41
+
42
+ ## 环境支持
43
+
44
+ | 环境 | 前缀 | Portainer 更新方式 |
45
+ |------|------|-------------------|
46
+ | dev1~15 | `dev` | Webhook 触发 |
47
+ | dev16~43 | `dev` | Force Update(xnzn-dev.xnzn.net) |
48
+ | dev44+ | `dev` | 只支持模式 0(手动更新) |
49
+ | test | `test` | Webhook 触发 |
50
+
51
+ ## 使用方式
52
+
53
+ ### 运行构建脚本
54
+
55
+ ```bash
56
+ python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
57
+ ```
58
+
59
+ 脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
60
+
61
+ ### 通过 AI 辅助部署
62
+
63
+ 当用户说"打包到 devX"时:
64
+
65
+ 1. 读取 `jenkins/last_cd_env.json` 获取上次参数
66
+ 2. 确认参数:环境、分支、模式
67
+ 3. 修改 `jenkins/last_cd_env.json` 写入新参数
68
+ 4. 执行 `python ~/.claude/skills/jenkins-deploy/assets/jk_build.py`
69
+
70
+ ## 配置文件
71
+
72
+ ### jenkins-config.json(凭证,全局)
73
+
74
+ 位置:`~/.claude/jenkins-config.json`(或本地 `.claude/jenkins-config.json`)
75
+
76
+ 模板:`.claude/skills/jenkins-deploy/assets/env_param.template.json`
77
+
78
+ > **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
79
+
80
+ ### last_cd_env.json(构建状态,项目本地)
81
+
82
+ 位置:`jenkins/last_cd_env.json`(脚本自动创建,无需手动初始化)
83
+
84
+ ```json
85
+ {
86
+ "build_mode": "1",
87
+ "cd_env": "dev1",
88
+ "core_param_branch": "master",
89
+ "api_param_branch": "master",
90
+ "api_param_folder": null
91
+ }
92
+ ```
93
+
94
+ ## 定制项目
95
+
96
+ 指定 `api_param_folder` 后,Jenkins Job 路径变为:
97
+
98
+ ```
99
+ {folder_name}/dev-后端-core
100
+ {folder_name}/dev-后端-api
101
+ ```
102
+
103
+ ## 首次初始化(团队成员)
104
+
105
+ 只需一步:配置全局凭证文件。
106
+
107
+ ```bash
108
+ # 方式 1:从团队成员拷贝
109
+ cp /path/to/teammate/jenkins-config.json ~/.claude/jenkins-config.json
110
+
111
+ # 方式 2:从模板创建,手动填写凭证
112
+ cp ~/.claude/skills/jenkins-deploy/assets/env_param.template.json ~/.claude/jenkins-config.json
113
+ # 然后替换 __JENKINS_*__ 和 __PORTAINER_*__ 占位符
114
+ ```
115
+
116
+ ### AI 初始化行为
117
+
118
+ 当技能被触发但 `jenkins-config.json` 不存在时:
119
+ 1. 提示"检测到尚未配置 Jenkins 凭证"
120
+ 2. 询问是否从模板创建
121
+ 3. 复制模板到 `~/.claude/jenkins-config.json`
122
+ 4. 提示用户填写凭证(或拷贝已有配置)
123
+
124
+ ## 依赖
125
+
126
+ ```bash
127
+ pip install python-jenkins requests
128
+ ```
129
+
130
+ ## 注意
131
+
132
+ - 本技能用于 dev/test 环境部署,**不涉及生产环境**
133
+ - 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
134
+ - 如果是代码构建错误排查,请使用 `bug-detective` 技能
package/AGENTS.md CHANGED
@@ -94,6 +94,7 @@
94
94
  | `collaborating-with-gemini` | 与 Gemini 协同设计 |
95
95
  | `codex-code-review` | 代码审查 |
96
96
  | `leniu-marketing-scenario` | 营销规则开发(折扣/满减/限额/限次/补贴/充值赠送/扣款/就餐规则) |
97
+ | `jenkins-deploy` | 打包部署(Jenkins 构建 + Portainer 更新,dev/test 环境) |
97
98
 
98
99
  ### OpenSpec 规格驱动开发技能(SDD)
99
100
 
package/bin/index.js CHANGED
@@ -586,6 +586,30 @@ function showDoneHint(toolKey) {
586
586
  console.log(` 2. 在 Codex 中使用 .codex/skills/ 下的技能`);
587
587
  console.log('');
588
588
  }
589
+ showJenkinsHint();
590
+ }
591
+
592
+ /** 检测 jenkins/ 是否已初始化,提示用户配置部署环境 */
593
+ function showJenkinsHint() {
594
+ const homeClaudeDir = path.join(os.homedir(), '.claude');
595
+ const globalConfig = path.join(homeClaudeDir, 'jenkins-config.json');
596
+ const localConfig = path.join(targetDir, '.claude', 'jenkins-config.json');
597
+ const skillAssetsDir = path.join(targetDir, '.claude', 'skills', 'jenkins-deploy', 'assets');
598
+ // 全局安装时也检查全局技能目录
599
+ const globalSkillDir = path.join(homeClaudeDir, 'skills', 'jenkins-deploy', 'assets');
600
+
601
+ const hasSkill = fs.existsSync(skillAssetsDir) || fs.existsSync(globalSkillDir);
602
+ const hasConfig = fs.existsSync(globalConfig) || fs.existsSync(localConfig);
603
+
604
+ // 技能存在但凭证未配置
605
+ if (hasSkill && !hasConfig) {
606
+ console.log(fmt('yellow', fmt('bold', '📦 Jenkins 部署凭证未配置')));
607
+ console.log(` 已安装 ${fmt('bold', 'jenkins-deploy')} 技能,但 ${fmt('bold', 'jenkins-config.json')} 尚未配置。`);
608
+ console.log(` 配置方式:`);
609
+ console.log(` 方式 1:从团队成员处拷贝 ${fmt('bold', '~/.claude/jenkins-config.json')}`);
610
+ console.log(` 方式 2:在 AI 对话中说 ${fmt('bold', '"初始化部署环境"')}`);
611
+ console.log('');
612
+ }
589
613
  }
590
614
 
591
615
  function run(selectedTool) {
@@ -748,6 +772,7 @@ function runUpdate(selectedTool) {
748
772
  console.log(` 强制更新保留文件: ${fmt('bold', hintCmd('update --force'))}`);
749
773
  }
750
774
  console.log('');
775
+ showJenkinsHint();
751
776
 
752
777
  if (totalFailed > 0) process.exitCode = 1;
753
778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-engineering-init",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "AI 工程化配置初始化工具 — 一键为 Claude Code、OpenAI Codex 等 AI 工具初始化 Skills 和项目规范",
5
5
  "keywords": [
6
6
  "claude-code",