leniu-dev 2.0.0 → 2.0.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.
Files changed (55) hide show
  1. package/.claude/mysql-config.json +34 -0
  2. package/.claude/skills/jenkins-deploy/SKILL.md +21 -5
  3. package/.claude/skills/jenkins-deploy/assets/jk_build.py +29 -14
  4. package/.claude/skills/leniu-java-amount-handling/SKILL.md +461 -0
  5. package/.claude/skills/leniu-java-export/SKILL.md +570 -0
  6. package/.claude/skills/leniu-java-report-query-param/SKILL.md +291 -0
  7. package/.claude/skills/leniu-java-total-line/SKILL.md +196 -0
  8. package/.claude/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
  9. package/.claude/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
  10. package/.claude/skills/leniu-mealtime/SKILL.md +215 -0
  11. package/.claude/skills/leniu-report-customization/SKILL.md +415 -0
  12. package/.claude/skills/leniu-report-customization/references/table-fields.md +93 -0
  13. package/.claude/skills/leniu-report-standard-customization/SKILL.md +391 -0
  14. package/.claude/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
  15. package/.claude/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
  16. package/.claude/skills/loki-log-query/SKILL.md +25 -55
  17. package/.claude/skills/loki-log-query/environments.json +45 -0
  18. package/.claude/skills/mysql-debug/SKILL.md +6 -12
  19. package/.codex/skills/jenkins-deploy/SKILL.md +21 -5
  20. package/.codex/skills/jenkins-deploy/assets/env_param.template.json +51 -0
  21. package/.codex/skills/jenkins-deploy/assets/jk_build.py +415 -0
  22. package/.codex/skills/leniu-java-export/SKILL.md +570 -0
  23. package/.codex/skills/leniu-java-report-query-param/SKILL.md +291 -0
  24. package/.codex/skills/leniu-java-total-line/SKILL.md +196 -0
  25. package/.codex/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
  26. package/.codex/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
  27. package/.codex/skills/leniu-mealtime/SKILL.md +215 -0
  28. package/.codex/skills/leniu-report-customization/SKILL.md +415 -0
  29. package/.codex/skills/leniu-report-customization/references/table-fields.md +93 -0
  30. package/.codex/skills/leniu-report-standard-customization/SKILL.md +391 -0
  31. package/.codex/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
  32. package/.codex/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
  33. package/.codex/skills/loki-log-query/SKILL.md +25 -55
  34. package/.codex/skills/mysql-debug/SKILL.md +6 -12
  35. package/.cursor/skills/jenkins-deploy/SKILL.md +21 -5
  36. package/.cursor/skills/jenkins-deploy/assets/env_param.template.json +51 -0
  37. package/.cursor/skills/jenkins-deploy/assets/jk_build.py +415 -0
  38. package/.cursor/skills/leniu-java-export/SKILL.md +570 -0
  39. package/.cursor/skills/leniu-java-report-query-param/SKILL.md +291 -0
  40. package/.cursor/skills/leniu-java-total-line/SKILL.md +196 -0
  41. package/.cursor/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
  42. package/.cursor/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
  43. package/.cursor/skills/leniu-mealtime/SKILL.md +215 -0
  44. package/.cursor/skills/leniu-report-customization/SKILL.md +415 -0
  45. package/.cursor/skills/leniu-report-customization/references/table-fields.md +93 -0
  46. package/.cursor/skills/leniu-report-standard-customization/SKILL.md +391 -0
  47. package/.cursor/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
  48. package/.cursor/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
  49. package/.cursor/skills/loki-log-query/SKILL.md +25 -55
  50. package/.cursor/skills/mysql-debug/SKILL.md +6 -12
  51. package/bin/index.js +59 -15
  52. package/package.json +1 -1
  53. package/src/skills/jenkins-deploy/SKILL.md +150 -0
  54. package/src/skills/jenkins-deploy/assets/env_param.template.json +51 -0
  55. package/src/skills/jenkins-deploy/assets/jk_build.py +415 -0
@@ -25,35 +25,9 @@ description: |
25
25
 
26
26
  ## 多环境配置
27
27
 
28
- ### 配置文件查找顺序
28
+ 配置文件:`.claude/skills/loki-log-query/environments.json`
29
29
 
30
- ```
31
- 1. 本地项目:.claude/loki-config.json(新格式)
32
- 2. 本地项目:.claude/skills/loki-log-query/environments.json(旧格式,兼容)
33
- 3. 全局配置:~/.claude/loki-config.json(或 ~/.cursor/loki-config.json)
34
- ```
35
-
36
- 本地优先。全局配置用 `npx ai-engineering-init config --type loki --scope global` 创建。
37
-
38
- ### 配置结构(支持 range 范围匹配)
39
-
40
- ```json
41
- {
42
- "active": "monitor-dev",
43
- "environments": {
44
- "monitor-dev": {
45
- "name": "Monitor 开发环境",
46
- "url": "https://monitor-dev.xnzn.net/grafana",
47
- "token": "glsa_xxx",
48
- "aliases": ["mdev", "dev"],
49
- "range": "dev1~15",
50
- "projects": ["dev01","dev02","...","dev15"]
51
- }
52
- }
53
- }
54
- ```
55
-
56
- ### 环境列表(默认)
30
+ ### 环境列表
57
31
 
58
32
  | 环境别名 | 名称 | URL | 快捷词 |
59
33
  |----------|------|-----|--------|
@@ -63,34 +37,31 @@ description: |
63
37
  | `monitor02-dev` | Monitor02 开发环境 | `https://monitor02-dev.xnzn.net/grafana` | m02, monitor02 |
64
38
  | `monitor-tyy-dev` | Monitor 体验园开发环境 | `https://monitor-tyy-dev.xnzn.net/grafana` | tyy, 体验园 |
65
39
 
66
- ### 环境匹配规则(含 range)
40
+ ### 环境匹配规则
67
41
 
68
- | 用户说法 | 匹配方式 | 结果 |
69
- |---------|---------|------|
70
- | " test13 的日志" | 精确匹配 key/aliases | `test13` 环境 |
71
- | "去 dev10 查" | **range 匹配**:dev10 在 monitor-dev 的 projects 中 | `monitor-dev`,`project="dev10"` |
72
- | "去 monitor-dev " | 精确匹配 key | `monitor-dev` 环境 |
73
- | "切到体验园" | aliases 匹配 | `monitor-tyy-dev` 环境 |
74
- | 未指定环境 | 使用 `active` 字段 | 默认活跃环境 |
42
+ 用户说的话 匹配环境:
43
+ - "查 test13 的日志" → `test13`
44
+ - " monitor-dev " `monitor-dev`
45
+ - "切到体验园" `monitor-tyy-dev`
46
+ - "去 m02 查一下" `monitor02-dev`
47
+ - 未指定环境 使用 `active` 字段指定的默认环境
75
48
 
76
- ### 读取配置(含全局降级 + range 匹配)
49
+ ### 读取配置
77
50
 
78
51
  ```bash
79
- # 按优先级查找配置文件
80
- find_config() {
81
- local PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
82
- for f in \
83
- "${PROJECT_DIR}/.claude/loki-config.json" \
84
- "${PROJECT_DIR}/.claude/skills/loki-log-query/environments.json" \
85
- "${HOME}/.claude/loki-config.json" \
86
- "${HOME}/.cursor/loki-config.json"; do
87
- [ -f "$f" ] && echo "$f" && return
88
- done
52
+ SKILL_DIR="$CLAUDE_PROJECT_DIR/.claude/skills/loki-log-query"
53
+ ENV_FILE="${SKILL_DIR}/environments.json"
54
+
55
+ # 读取指定环境(参数: 环境别名)
56
+ read_env() {
57
+ local ENV_KEY="${1:-$(python3 -c "import json; print(json.load(open('${ENV_FILE}'))['active'])")}"
58
+ GRAFANA_URL=$(python3 -c "import json; print(json.load(open('${ENV_FILE}'))['environments']['${ENV_KEY}']['url'])")
59
+ TOKEN=$(python3 -c "import json; print(json.load(open('${ENV_FILE}'))['environments']['${ENV_KEY}']['token'])")
60
+ API="${GRAFANA_URL}/api/datasources/proxy/uid/loki/loki/api/v1"
61
+ echo "Environment: ${ENV_KEY} → ${GRAFANA_URL}"
89
62
  }
90
63
 
91
- ENV_FILE=$(find_config)
92
-
93
- # 通过别名或 range 查找环境 key + project
64
+ # 通过别名查找环境 key
94
65
  find_env() {
95
66
  python3 -c "
96
67
  import json
@@ -98,11 +69,10 @@ data = json.load(open('${ENV_FILE}'))
98
69
  alias = '${1}'.lower()
99
70
  for key, env in data['environments'].items():
100
71
  if alias == key or alias in env.get('aliases', []):
101
- print(f'{key}|'); exit()
102
- for key, env in data['environments'].items():
103
- if alias in env.get('projects', []):
104
- print(f'{key}|{alias}'); exit()
105
- print(f'{data[\"active\"]}|')
72
+ print(key)
73
+ break
74
+ else:
75
+ print(data['active'])
106
76
  "
107
77
  }
108
78
  ```
@@ -27,28 +27,22 @@ description: |
27
27
 
28
28
  ---
29
29
 
30
- ## 连接信息获取(四级降级,自动查找)
30
+ ## 连接信息获取(三级降级,自动查找)
31
31
 
32
32
  当本技能被激活时,**按以下优先级获取数据库连接信息**:
33
33
 
34
34
  ### 优先级 1:用户对话中指定(最高优先级)
35
35
 
36
36
  用户直接给出连接信息,或指定环境名:
37
- - "连 dev 环境查一下" → 使用配置中 dev 环境
38
- - "去 dev10 查" → **范围匹配** dev 环境(range: "1~15")
37
+ - "连 dev 环境查一下" → 使用 `.claude/mysql-config.json` 中 dev 环境的配置
39
38
  - 直接给出 host/port/user/password → 直接使用
40
39
 
41
- ### 优先级 2:本地项目 `.claude/mysql-config.json`
40
+ ### 优先级 2:`.claude/mysql-config.json`(显式配置,可选)
42
41
 
43
- 项目目录下的配置文件,优先于全局配置。
42
+ 如果文件存在且当前环境的 password 不是占位符 `YOUR_PASSWORD`,使用该配置。
43
+ **此文件为可选**,主要用于连接非本地环境(dev/prod 远程数据库)。
44
44
 
45
- ### 优先级 3:全局配置(所有项目共享)
46
-
47
- 全局配置文件(通过 `npx ai-engineering-init config --scope global` 创建)。
48
- 查找路径:`~/.claude/mysql-config.json` 或 `~/.cursor/mysql-config.json`(取决于使用的工具)。
49
- **推荐**:公司统一的数据库连接信息放全局,项目特定的覆盖放本地。
50
-
51
- ### 优先级 4:工程配置文件(零配置,本地开发默认)
45
+ ### 优先级 3:工程配置文件(零配置,本地开发默认)
52
46
 
53
47
  从项目的 `bootstrap-dev.yml` 中自动提取连接信息:
54
48
 
@@ -58,14 +58,30 @@ python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
58
58
 
59
59
  脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
60
60
 
61
+ ### 命令行模式(推荐,AI 调用)
62
+
63
+ ```bash
64
+ python3 ~/.claude/skills/jenkins-deploy/assets/jk_build.py \
65
+ --mode 2 --env dev10 --api-branch release_xxx --folder 常州技师学院
66
+ ```
67
+
68
+ | 参数 | 缩写 | 说明 |
69
+ |------|------|------|
70
+ | `--mode` | `-m` | 构建模式(0/1/2/3) |
71
+ | `--env` | `-e` | 目标环境(dev10, test1) |
72
+ | `--core-branch` | | core 分支名 |
73
+ | `--api-branch` | | api 分支名 |
74
+ | `--folder` | `-f` | 定制项目文件夹名 |
75
+
76
+ 未传的参数自动使用 `last_cd_env.json` 中的上次值。
77
+
61
78
  ### 通过 AI 辅助部署
62
79
 
63
- 当用户说"打包到 devX"时:
80
+ 当用户说"打包到 devX"时,直接用命令行参数调用,不再需要 heredoc 模拟交互:
64
81
 
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`
82
+ ```bash
83
+ python3 ~/.claude/skills/jenkins-deploy/assets/jk_build.py --mode 2 --env dev10 --api-branch master
84
+ ```
69
85
 
70
86
  ## 配置文件
71
87
 
@@ -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,415 @@
1
+ # 简单调用jenkins api,构建项目并触发webhook
2
+ import argparse
3
+ import html
4
+ import json
5
+ import os
6
+ import sys
7
+ import time
8
+
9
+ import jenkins
10
+ import requests
11
+
12
+ # 预设默认值(可修改)
13
+ cd_env = '' # dev或test
14
+
15
+ # dev和test环境独立配置
16
+ jenkins_server_url = 'https://ci.xnzn.net' # jenkins地址
17
+ portainer_server_suffix = '' # portainer服务名后缀
18
+ core_timeout = 60 * 10 # core构建超时时间
19
+ api_timeout = 60 * 5 # api构建超时时间
20
+ portainer_user_name = '' # portainer用户名
21
+ portainer_user_pwd = '' # portainer密码
22
+ portainer_api_url = ''
23
+ portainer_endpoints = ''
24
+ jenkins_user_id = ''
25
+ jenkins_api_token = ''
26
+ core_job = ''
27
+ api_job = ''
28
+
29
+
30
+ # ~~~~~~~~~~~~~~~~~ 配置文件查找 ~~~~~~~~~~~~~~~~~~~~~~~~
31
+
32
+ def find_config_file(filename, search_paths):
33
+ """按优先级查找配置文件:本地 > 全局"""
34
+ for p in search_paths:
35
+ filepath = os.path.join(p, filename)
36
+ if os.path.exists(filepath):
37
+ return filepath
38
+ return None
39
+
40
+
41
+ # 配置文件搜索路径
42
+ project_dir = os.getcwd()
43
+ home_claude_dir = os.path.join(os.path.expanduser('~'), '.claude')
44
+
45
+ # jenkins-config.json 搜索路径:项目本地 .claude/ > 全局 ~/.claude/
46
+ config_search_paths = [
47
+ os.path.join(project_dir, '.claude'),
48
+ home_claude_dir,
49
+ ]
50
+
51
+ # last_cd_env.json 搜索路径:项目本地 jenkins/
52
+ state_dir = os.path.join(project_dir, 'jenkins')
53
+
54
+
55
+ # 执行job方法,通过等待时间来判断是否构建成功
56
+ def do_job_by_time(server, job_name, param_branch, param_env, wait_time):
57
+ queue_num = server.build_job(name=job_name,
58
+ parameters={'BRANCH': param_branch, 'VERSION': param_env},
59
+ token=jenkins_api_token)
60
+ print('创建queue... {queue_num}')
61
+ time.sleep(wait_time)
62
+ queue_info = server.get_queue_item(queue_num)
63
+
64
+ print('等待时间到,queue信息:' + str(queue_info))
65
+
66
+
67
+ # 执行job方法,通过请求判断结束,进度更准确
68
+ def do_job(server, job_name, param_branch, param_env, wait_time):
69
+
70
+ queue_num = server.build_job(name=job_name,
71
+ parameters={'BRANCH': param_branch, 'VERSION': param_env},
72
+ token=jenkins_api_token)
73
+ print(f'创建队列... {queue_num}')
74
+ job_number = None
75
+ # 记录开始构建时间
76
+ start_time = time.time()
77
+
78
+ while True:
79
+ time.sleep(10)
80
+ if time.time() - start_time > wait_time:
81
+ print("构建超时,结束")
82
+ exit(1)
83
+ queue_info = server.get_queue_item(queue_num)
84
+ if queue_info['why'] is not None:
85
+ why = queue_info['why']
86
+ print(f'队列等待执行器...{why}')
87
+ elif 'executable' in queue_info:
88
+ job_number = queue_info['executable']['number']
89
+ print(f'开始执行任务...{job_number}')
90
+ break
91
+ else:
92
+ print('执行结束')
93
+ break
94
+
95
+ # 间隔x秒查询构建结果
96
+ while True:
97
+ time.sleep(10)
98
+
99
+ # 如果超过3分钟,直接结束
100
+ if time.time() - start_time > wait_time:
101
+ print("构建超时,结束")
102
+ exit(1)
103
+ headers = {
104
+ "n": str(job_number),
105
+ }
106
+ # 如果job_name包含 '/',则在把'/'替换为/job/
107
+ if '/' in job_name:
108
+ job_name_url = job_name.replace('/', '/job/')
109
+ url = f'{jenkins_server_url}/job/{job_name_url}/buildHistory/ajax'
110
+ else:
111
+ url = f'{jenkins_server_url}/job/{job_name}/buildHistory/ajax'
112
+
113
+ req = requests.Request(method='POST', url=url, headers=headers)
114
+ response = server.jenkins_open(req)
115
+ # 解析response包含的html
116
+ html_str = html.unescape(response)
117
+ if 'Expected build number' in html_str: # 正在等待中
118
+ print(f'\r等待中...', end='')
119
+ continue
120
+ elif ('预计剩余时间' in html_str) or ('Estimated remaining time' in html_str): # 是否包含 预计剩余时间
121
+ try:
122
+ # 取出 '<td style="width:' 和 '%;" class="progress-bar-done"></td>' 之间的字符串
123
+ progress = html_str.split('<td style="width:')[1].split('%;" class="progress-bar-done"></td>')[0]
124
+ # 打印进度更新最后一行
125
+ print(f'\r构建中...{progress}%', end='')
126
+ except IndexError:
127
+ print("无法从HTML字符串中提取进度信息")
128
+ except Exception as e:
129
+ print(f"发生错误: {e}")
130
+ continue
131
+ else:
132
+ print(f"\r构建结束,耗时 {(time.time() - start_time):.2f} 秒", end='\n')
133
+ # 查询构建结果
134
+ build_info = server.get_build_info(job_name, job_number)
135
+ if build_info['result'] == 'SUCCESS':
136
+ print(f"构建成功 {html.unescape(build_info['url'])}")
137
+ else:
138
+ print(f"构建失败({build_info['result']}) {html.unescape(build_info['url'])}")
139
+ print(f"原始 build_info:{build_info}")
140
+ print(f"原始 html:{html_str}")
141
+ return False
142
+ break
143
+ return True
144
+
145
+
146
+ # 获取 Portainer JWT Token
147
+ def get_jwt_token():
148
+ login_url = f"{portainer_api_url}/auth"
149
+ login_data = {
150
+ "Username": portainer_user_name,
151
+ "Password": portainer_user_pwd,
152
+ }
153
+ response = requests.post(login_url, data=json.dumps(login_data))
154
+ token = response.json()["jwt"]
155
+ print('获取token成功')
156
+ return token
157
+
158
+
159
+ # 获取 Service ID
160
+ def get_server_id(token):
161
+ headers = {
162
+ "Authorization": f"Bearer {token}",
163
+ "Content-Type": "application/json"
164
+ }
165
+ url = portainer_api_url + f'/endpoints/{portainer_endpoints}/docker/services'
166
+ services_response = requests.get(url, headers=headers)
167
+ services = services_response.json()
168
+
169
+ service_name = cd_env + portainer_server_suffix
170
+ # Find service with matching name
171
+ target_service_id = None
172
+ for service in services:
173
+ if service["Spec"]["Name"] == service_name:
174
+ target_service_id = service["ID"]
175
+ break
176
+
177
+ print(f'查询{service_name}的service_id:{target_service_id}')
178
+ return target_service_id
179
+
180
+
181
+ # 获取 Service 的 WebHook
182
+ def get_service_web_hook():
183
+ pt_token = get_jwt_token()
184
+ service_id = get_server_id(pt_token)
185
+
186
+ headers = {
187
+ "Authorization": f"Bearer {pt_token}",
188
+ "Content-Type": "application/json"
189
+ }
190
+
191
+ url = portainer_api_url + f'/webhooks?filters=%7B%22ResourceID%22:%22{service_id}%22,%22EndpointID%22:{portainer_endpoints}%7D'
192
+ services_response = requests.get(url, headers=headers)
193
+ webhook_json = services_response.json()
194
+ target_webhook_token = None
195
+ # 是否存在webhook
196
+ if len(webhook_json) > 0:
197
+ target_webhook_token = webhook_json[0]['Token']
198
+ print('获取webhook_token:' + target_webhook_token)
199
+
200
+ # 尝试调用请求创建webhook
201
+ if target_webhook_token is None:
202
+ payload = {"ResourceID": service_id, "EndpointID": 2, "WebhookType": 1, "registryID": 0}
203
+ services_response = requests.post(portainer_api_url + "/webhooks", headers=headers, data=json.dumps(payload))
204
+ webhook_json = services_response.json()
205
+ if webhook_json is not None and 'Token' in webhook_json:
206
+ target_webhook_token = webhook_json['Token']
207
+ print('创建webhook_token结果:' + target_webhook_token)
208
+
209
+ return target_webhook_token
210
+
211
+
212
+ # 启动webhook(dev1-15)
213
+ def trigger_webhook():
214
+
215
+ wh_token = get_service_web_hook()
216
+
217
+ if wh_token is None:
218
+ print(f"{cd_env}环境没有配置webhook,结束")
219
+ exit(0)
220
+ else:
221
+ print("开始触发webhook")
222
+ r = requests.post(portainer_api_url + '/webhooks/' + wh_token)
223
+ print("webhook触发结果:" + str(r.status_code))
224
+
225
+ # 通过service id 更新
226
+ def update_portainer():
227
+
228
+ pt_token = get_jwt_token()
229
+ service_id = get_server_id(pt_token)
230
+
231
+ headers = {
232
+ "Authorization": f"Bearer {pt_token}",
233
+ "Content-Type": "application/json"
234
+ }
235
+
236
+ # 调用更新(PUT请求):
237
+ url = portainer_api_url + f'/endpoints/{portainer_endpoints}/forceupdateservice'
238
+ payload = {
239
+ "serviceID": service_id,
240
+ "pullImage": True,
241
+ }
242
+ services_response = requests.put(url, headers=headers, data=json.dumps(payload))
243
+ update_rsp_json = services_response.json()
244
+ print('更新结果:' + str(update_rsp_json))
245
+
246
+ # ~~~~~~~~~~~~~~~~~~ 参数解析 ~~~~~~~~~~~~~~~~~~~~~~~
247
+
248
+ parser = argparse.ArgumentParser(description='Jenkins + Portainer 自动构建部署工具')
249
+ parser.add_argument('--mode', '-m', type=str, help='构建模式: 0-只构建 1-全构建+更新 2-构建api+更新 3-只更新')
250
+ parser.add_argument('--env', '-e', type=str, help='目标环境,如 dev10, test1')
251
+ parser.add_argument('--core-branch', type=str, help='core 分支名')
252
+ parser.add_argument('--api-branch', type=str, help='api 分支名')
253
+ parser.add_argument('--folder', '-f', type=str, help='定制工程文件夹名(如 常州技师学院),不传则为标准项目')
254
+ args = parser.parse_args()
255
+
256
+ # 判断是否为命令行模式(传了任意参数)
257
+ cli_mode = any([args.mode, args.env, args.core_branch, args.api_branch, args.folder])
258
+
259
+ # ~~~~~~~~~~~~~~~~~~ 开始输入 ~~~~~~~~~~~~~~~~~~~~~~~
260
+
261
+ # 打印欢迎信息
262
+ print('\n~~~~~~~~~~~~~~')
263
+ print('正在使用jenkins+portainer构建工具,目前支持dev和test环境的自动化构建+更新')
264
+ print('~~~~~~~~~~~~~~\n')
265
+
266
+ # 查找环境配置文件(jenkins-config.json)
267
+ config_file = find_config_file('jenkins-config.json', config_search_paths)
268
+ if config_file is None:
269
+ print('❌ 未找到 jenkins-config.json')
270
+ print(f' 请将配置文件放到以下任一位置:')
271
+ for p in config_search_paths:
272
+ print(f' - {os.path.join(p, "jenkins-config.json")}')
273
+ print(f'\n 或运行: npx ai-engineering-init config --type jenkins')
274
+ exit(1)
275
+ print(f'配置文件: {config_file}')
276
+
277
+ # 确保构建状态目录存在
278
+ os.makedirs(state_dir, exist_ok=True)
279
+ state_file = os.path.join(state_dir, 'last_cd_env.json')
280
+
281
+ # 读取上一次构建的环境(如果不存在则使用默认值)
282
+ if os.path.exists(state_file):
283
+ with open(state_file, 'r', encoding='utf-8') as f:
284
+ cd_env_json = json.load(f)
285
+ else:
286
+ cd_env_json = {
287
+ "build_mode": "1",
288
+ "cd_env": "dev1",
289
+ "core_param_branch": "master",
290
+ "api_param_branch": "master",
291
+ "api_param_folder": None
292
+ }
293
+
294
+ if cli_mode:
295
+ # 命令行模式:直接使用参数,缺省的用上次的值
296
+ build_mode = args.mode or cd_env_json['build_mode']
297
+ cd_env = args.env or cd_env_json['cd_env']
298
+ core_param_branch = args.core_branch or cd_env_json['core_param_branch']
299
+ api_param_branch = args.api_branch or cd_env_json['api_param_branch']
300
+ api_param_folder = args.folder if args.folder is not None else cd_env_json.get('api_param_folder')
301
+ print(f'命令行模式: mode={build_mode} env={cd_env} core={core_param_branch} api={api_param_branch} folder={api_param_folder}')
302
+ else:
303
+ # 交互模式:从终端获取用户输入
304
+ build_mode = input(f"请输入模式 0-只构建 1-全构建+更新 2-构建api+更新 3-只更新(预设{cd_env_json['build_mode']}):", ) or cd_env_json['build_mode']
305
+ cd_env = input(f"请输入环境(预设:{cd_env_json['cd_env']}):", ) or cd_env_json['cd_env']
306
+ core_param_branch = input(f"请输入core分支(预设:{cd_env_json['core_param_branch']}):") or cd_env_json['core_param_branch']
307
+ api_param_branch = input(f"请输入api分支(预设:{cd_env_json['api_param_branch']}):") or cd_env_json['api_param_branch']
308
+ api_param_folder = input(f"请输入定制工程文件夹名(空格或None表示不需要,预设:{cd_env_json['api_param_folder']}):") or cd_env_json['api_param_folder']
309
+
310
+ # 校验模式:
311
+ if build_mode is None or build_mode not in ('0', '1', '2', '3'):
312
+ print("模式错误,结束")
313
+ exit(1)
314
+ # 校验环境,如果不是dev或test开头的,直接退出
315
+ if cd_env is None or not cd_env.startswith('dev') and not cd_env.startswith('test'):
316
+ print("环境错误,结束")
317
+ exit(1)
318
+ # 校验分支
319
+ if core_param_branch is None:
320
+ print("分支错误,结束")
321
+ exit(1)
322
+ if api_param_branch is None:
323
+ print("分支错误,结束")
324
+ exit(1)
325
+ if api_param_folder is None or api_param_folder == '' or api_param_folder == ' ' or api_param_folder == 'none' or api_param_folder == 'None':
326
+ api_param_folder = None
327
+ # 如果dev44及以上环境,只能使用模式0:
328
+ if cd_env.startswith('dev') and int(cd_env[3:]) >= 44 and build_mode != '0':
329
+ print("dev43及以上环境只能使用模式0,结束")
330
+ exit(1)
331
+
332
+ # 保存到本地状态文件
333
+ cd_env_json['cd_env'] = cd_env
334
+ cd_env_json['core_param_branch'] = core_param_branch
335
+ cd_env_json['api_param_branch'] = api_param_branch
336
+ cd_env_json['api_param_folder'] = api_param_folder
337
+ cd_env_json['build_mode'] = build_mode
338
+ json.dump(cd_env_json, open(state_file, 'w', encoding='utf-8'), ensure_ascii=False)
339
+
340
+ # 根据环境输入,设置前缀
341
+ env_prefix = 'dev' if cd_env.startswith('dev') else 'test'
342
+
343
+ # 读取环境配置(从全局或本地 jenkins-config.json)
344
+ with open(config_file, 'r', encoding='utf-8') as f:
345
+ env_param = json.load(f)
346
+
347
+ jenkins_user_id = env_param['jenkins_user_id'][env_prefix]
348
+ jenkins_api_token = env_param['jenkins_api_token'][env_prefix]
349
+ core_job = env_param['core_job'][env_prefix]
350
+ api_job = env_param['api_job'][env_prefix]
351
+ if api_param_folder is not None:
352
+ core_job = api_param_folder + '/' + env_param['core_job_custom'][env_prefix]
353
+ api_job = api_param_folder + '/' + env_param['api_job_custom'][env_prefix]
354
+
355
+ # portainer配置(如果cd_env > dev15,使用另一套配置)
356
+ if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
357
+ portainer_user_name = env_param['portainer_user_name']['dev16']
358
+ portainer_user_pwd = env_param['portainer_user_pwd']['dev16']
359
+ portainer_api_url = env_param['portainer_api_url']['dev16']
360
+ portainer_endpoints = env_param['portainer_endpoints']['dev16']
361
+ portainer_server_suffix = env_param['portainer_server_suffix']['dev16']
362
+ else:
363
+ portainer_api_url = env_param['portainer_api_url'][env_prefix]
364
+ portainer_user_name = env_param['portainer_user_name'][env_prefix]
365
+ portainer_user_pwd = env_param['portainer_user_pwd'][env_prefix]
366
+ portainer_endpoints = env_param['portainer_endpoints'][env_prefix]
367
+ portainer_server_suffix = env_param['portainer_server_suffix'][env_prefix]
368
+
369
+ # ~~~~~~~~~~~~~~~~~ 开始打包 ~~~~~~~~~~~~~~~~~~~~~~~~
370
+
371
+ print('\n~~~~~~~~~~~~~~\n')
372
+ print('开始打包')
373
+ print("链接jenkins:" + jenkins_server_url)
374
+
375
+ # 实例化jenkins对象,连接远程的jenkins master server
376
+ j_server = jenkins.Jenkins(url=jenkins_server_url, username=jenkins_user_id, password=jenkins_api_token)
377
+ print("jenkins信息:" + j_server.get_whoami()['fullName'])
378
+
379
+ # 通过参数构建core
380
+ if int(build_mode) < 2:
381
+ print('开始构建core:' + core_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
382
+ rs = do_job(j_server, core_job, core_param_branch, cd_env, core_timeout)
383
+ if rs is False:
384
+ print('构建core失败,结束')
385
+ exit(1)
386
+ time.sleep(10)
387
+
388
+ if int(build_mode) < 3:
389
+ print('开始构建api:' + api_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
390
+ rs = do_job(j_server, api_job, api_param_branch, cd_env, api_timeout)
391
+ if rs is False:
392
+ print('构建api失败,结束')
393
+ exit(1)
394
+ time.sleep(2)
395
+
396
+ print('结束打包' + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
397
+
398
+ # ~~~~~~~~~~~~~~~~~ 开始更新 ~~~~~~~~~~~~~~~~~~~~~~~~
399
+
400
+ # 开始更新portainer
401
+ if int(build_mode) == 0:
402
+ print('\n~~~~~~~ 请自行更新portainer ~~~~~~~\n')
403
+ else:
404
+ print('\n~~~~~~~~~~~~~~\n')
405
+ print('开始更新portainer:' + portainer_api_url + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
406
+ # portainer配置(如果cd_env > dev15,使用update)
407
+ if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
408
+ # 通过update更新
409
+ update_portainer()
410
+ else:
411
+ # 触发webhook
412
+ trigger_webhook()
413
+
414
+
415
+ exit(0)