ai-engineering-init 1.16.4 → 1.17.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/.claude/commands/init-config.md +154 -0
- package/.claude/hooks/skill-forced-eval.js +1 -0
- package/.claude/skills/jenkins-deploy/SKILL.md +207 -0
- package/.claude/skills/jenkins-deploy/assets/env_param.template.json +51 -0
- package/.claude/skills/jenkins-deploy/assets/jk_build.py +362 -0
- package/.claude/skills/jenkins-deploy/assets/last_cd_env.template.json +7 -0
- package/.claude/templates/env-config.md +27 -0
- package/.codex/skills/jenkins-deploy/SKILL.md +207 -0
- package/.cursor/hooks/cursor-skill-eval.js +4 -0
- package/.cursor/rules/skill-activation.mdc +1 -0
- package/.cursor/skills/jenkins-deploy/SKILL.md +207 -0
- package/.cursor/templates/env-config.md +27 -0
- package/AGENTS.md +1 -0
- package/bin/index.js +254 -1
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
项目使用 `jenkins/` 目录下的 Python 脚本实现自动化构建部署,流程:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Jenkins 构建 core → Jenkins 构建 api → Portainer 更新容器
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 文件位置
|
|
27
|
+
|
|
28
|
+
| 文件 | 作用 |
|
|
29
|
+
|------|------|
|
|
30
|
+
| `jenkins/jk_build.py` | 主构建脚本 |
|
|
31
|
+
| `jenkins/env_param.json` | 环境配置(Jenkins/Portainer 连接参数) |
|
|
32
|
+
| `jenkins/last_cd_env.json` | 上次构建参数(自动保存) |
|
|
33
|
+
|
|
34
|
+
## 构建模式
|
|
35
|
+
|
|
36
|
+
| 模式 | 说明 | 执行步骤 |
|
|
37
|
+
|------|------|---------|
|
|
38
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
39
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
40
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
41
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
42
|
+
|
|
43
|
+
## 环境支持
|
|
44
|
+
|
|
45
|
+
| 环境 | 前缀 | Jenkins Job | Portainer |
|
|
46
|
+
|------|------|-------------|-----------|
|
|
47
|
+
| 开发环境 | `dev` + 编号 | `dev-tengyun-core` / `dev-tengyun-yunshitang-api` | devops-dev.xnzn.net |
|
|
48
|
+
| 测试环境 | `test` + 编号 | `test-tengyun-core` / `test-tengyun-yunshitang-api` | devops-test.xnzn.net |
|
|
49
|
+
| dev16+ | `dev16` ~ `dev43` | 同 dev | xnzn-dev.xnzn.net(使用 forceupdateservice) |
|
|
50
|
+
| dev44+ | `dev44` 及以上 | 只支持模式 0 | 需手动更新 |
|
|
51
|
+
|
|
52
|
+
## 定制项目支持
|
|
53
|
+
|
|
54
|
+
当指定 `api_param_folder`(定制工程文件夹名)时,Jenkins Job 路径变为:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{folder_name}/dev-后端-core # 替代 dev-tengyun-core
|
|
58
|
+
{folder_name}/dev-后端-api # 替代 dev-tengyun-yunshitang-api
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 使用方式
|
|
62
|
+
|
|
63
|
+
### 直接运行脚本
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd jenkins && python jk_build.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
脚本会交互式询问:
|
|
70
|
+
1. **模式**(0/1/2/3)
|
|
71
|
+
2. **环境**(dev1, dev2, test1 等)
|
|
72
|
+
3. **core 分支**(如 release_5.56.0)
|
|
73
|
+
4. **api 分支**(如 master)
|
|
74
|
+
5. **定制工程文件夹**(可选,空格或 None 跳过)
|
|
75
|
+
|
|
76
|
+
### 通过 Claude 辅助部署
|
|
77
|
+
|
|
78
|
+
当用户说"打包到 devX"时:
|
|
79
|
+
|
|
80
|
+
1. **读取** `jenkins/last_cd_env.json` 获取上次构建参数
|
|
81
|
+
2. **确认参数**:环境、分支、模式、是否定制项目
|
|
82
|
+
3. **修改** `last_cd_env.json` 写入新参数
|
|
83
|
+
4. **执行** `cd jenkins && python jk_build.py`(脚本读取预设值,用户直接回车即可)
|
|
84
|
+
|
|
85
|
+
## 配置文件说明
|
|
86
|
+
|
|
87
|
+
### last_cd_env.json(构建参数)
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"build_mode": "0",
|
|
92
|
+
"cd_env": "dev63",
|
|
93
|
+
"core_param_branch": "release_5.56.0",
|
|
94
|
+
"api_param_branch": "master",
|
|
95
|
+
"api_param_folder": null
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| 字段 | 说明 |
|
|
100
|
+
|------|------|
|
|
101
|
+
| `build_mode` | 构建模式(0/1/2/3) |
|
|
102
|
+
| `cd_env` | 目标环境(如 dev1, test2) |
|
|
103
|
+
| `core_param_branch` | core 仓库分支 |
|
|
104
|
+
| `api_param_branch` | api 仓库分支 |
|
|
105
|
+
| `api_param_folder` | 定制项目文件夹(null 表示标准项目) |
|
|
106
|
+
|
|
107
|
+
### env_param.json(环境连接配置)
|
|
108
|
+
|
|
109
|
+
包含 Jenkins 和 Portainer 的连接参数(用户名、API Token、服务地址等),按 dev/test 环境分组。
|
|
110
|
+
|
|
111
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
112
|
+
|
|
113
|
+
## 构建流程详解
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
1. 连接 Jenkins(ci.xnzn.net)
|
|
117
|
+
2. 构建 core(参数:BRANCH + VERSION=环境名)
|
|
118
|
+
└─ 轮询构建进度,超时 10 分钟
|
|
119
|
+
3. 构建 api(参数同上)
|
|
120
|
+
└─ 轮询构建进度,超时 5 分钟
|
|
121
|
+
4. 更新 Portainer:
|
|
122
|
+
├─ dev1~15:Webhook 触发
|
|
123
|
+
│ └─ 获取 JWT → 查 Service ID → 获取/创建 Webhook → POST 触发
|
|
124
|
+
└─ dev16+:Force Update
|
|
125
|
+
└─ 获取 JWT → 查 Service ID → PUT forceupdateservice
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 常见操作
|
|
129
|
+
|
|
130
|
+
### 只改了 api 代码,快速部署
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 修改 last_cd_env.json 的 build_mode 为 "2",然后运行
|
|
134
|
+
cd jenkins && python jk_build.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 只需要更新容器(已在 Jenkins 手动构建完)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# build_mode 设为 "3"
|
|
141
|
+
cd jenkins && python jk_build.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 部署定制项目
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
// last_cd_env.json
|
|
148
|
+
{
|
|
149
|
+
"api_param_folder": "leniu-tengyun-wuhanxieheyiyuan"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 首次初始化(团队成员)
|
|
154
|
+
|
|
155
|
+
当用户说"初始化部署环境"或检测到 `jenkins/` 目录不存在时,执行以下步骤:
|
|
156
|
+
|
|
157
|
+
### 自动初始化流程
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# 1. 创建 jenkins 目录
|
|
161
|
+
mkdir -p jenkins
|
|
162
|
+
|
|
163
|
+
# 2. 从技能模板复制文件(SKILL_DIR 为技能目录路径)
|
|
164
|
+
cp .claude/skills/jenkins-deploy/assets/jk_build.py jenkins/jk_build.py
|
|
165
|
+
cp .claude/skills/jenkins-deploy/assets/last_cd_env.template.json jenkins/last_cd_env.json
|
|
166
|
+
|
|
167
|
+
# 3. 环境配置需要用户提供(含敏感凭证)
|
|
168
|
+
cp .claude/skills/jenkins-deploy/assets/env_param.template.json jenkins/env_param.json
|
|
169
|
+
|
|
170
|
+
# 4. 安装依赖
|
|
171
|
+
pip install python-jenkins requests
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 配置凭证
|
|
175
|
+
|
|
176
|
+
初始化后,`jenkins/env_param.json` 中的占位符需要替换为真实值:
|
|
177
|
+
|
|
178
|
+
| 占位符 | 说明 | 获取方式 |
|
|
179
|
+
|--------|------|---------|
|
|
180
|
+
| `__JENKINS_DEV_USER__` | Jenkins dev 账号 | 向团队负责人获取 |
|
|
181
|
+
| `__JENKINS_DEV_TOKEN__` | Jenkins dev API Token | Jenkins → 用户 → 设置 → API Token |
|
|
182
|
+
| `__JENKINS_TEST_USER__` | Jenkins test 账号 | 同上 |
|
|
183
|
+
| `__JENKINS_TEST_TOKEN__` | Jenkins test API Token | 同上 |
|
|
184
|
+
| `__PORTAINER_*_USER__` | Portainer 用户名 | 向团队负责人获取 |
|
|
185
|
+
| `__PORTAINER_*_PWD__` | Portainer 密码 | 向团队负责人获取 |
|
|
186
|
+
|
|
187
|
+
**快捷方式**:如果团队已有成员配置好,可直接拷贝对方的 `env_param.json` 文件。
|
|
188
|
+
|
|
189
|
+
### AI 初始化行为
|
|
190
|
+
|
|
191
|
+
当技能被触发但 `jenkins/` 不存在时:
|
|
192
|
+
1. 提示用户"检测到尚未初始化部署环境"
|
|
193
|
+
2. 询问是否执行初始化
|
|
194
|
+
3. 执行上述复制步骤
|
|
195
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
196
|
+
|
|
197
|
+
## 依赖
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install python-jenkins requests
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 注意
|
|
204
|
+
|
|
205
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
206
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
207
|
+
- 如果是代码构建错误排查,请使用 `bug-detective` 技能
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 环境配置模板
|
|
2
|
+
|
|
3
|
+
> 填写后运行 `npx ai-engineering-init config --from .claude/templates/env-config.md --scope global`
|
|
4
|
+
> 一键初始化 MySQL 数据库连接和 Loki 日志查询配置。
|
|
5
|
+
|
|
6
|
+
## MySQL 数据库连接
|
|
7
|
+
|
|
8
|
+
| 环境 | host | port | user | password | range | 描述 |
|
|
9
|
+
|------|------|------|------|----------|-------|------|
|
|
10
|
+
| local | 127.0.0.1 | 3306 | root | do@u.can | | 本地环境 |
|
|
11
|
+
| dev | YOUR_DEV_HOST | 3306 | YOUR_USER | YOUR_PASSWORD | 1~15 | 开发环境(dev1~dev15) |
|
|
12
|
+
| test | YOUR_TEST_HOST | 3306 | YOUR_USER | YOUR_PASSWORD | 1~30 | 测试环境(test1~test30) |
|
|
13
|
+
| prod | YOUR_PROD_HOST | 3306 | readonly | YOUR_PASSWORD | | 生产环境(只读) |
|
|
14
|
+
|
|
15
|
+
默认环境: local
|
|
16
|
+
|
|
17
|
+
## Loki 日志查询
|
|
18
|
+
|
|
19
|
+
| 环境 | 名称 | URL | Token | 别名 | range |
|
|
20
|
+
|------|------|-----|-------|------|-------|
|
|
21
|
+
| test13 | 测试13 | https://test13.xnzn.net/grafana | YOUR_TOKEN | test13,13 | |
|
|
22
|
+
| monitor-test | Monitor测试 | https://monitor-test.xnzn.net/grafana | YOUR_TOKEN | mtest | test1~12 |
|
|
23
|
+
| monitor-dev | Monitor开发 | https://monitor-dev.xnzn.net/grafana | YOUR_TOKEN | mdev,dev | dev1~15 |
|
|
24
|
+
| monitor02-dev | Monitor02开发 | https://monitor02-dev.xnzn.net/grafana | YOUR_TOKEN | m02,monitor02 | dev16~42 |
|
|
25
|
+
| monitor-tyy-dev | 体验园开发 | https://monitor-tyy-dev.xnzn.net/grafana | YOUR_TOKEN | tyy,体验园 | dev44~58 |
|
|
26
|
+
|
|
27
|
+
默认环境: test13
|
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
|
@@ -55,6 +55,7 @@ let submitIssue = false; // sync-back --submit
|
|
|
55
55
|
let configType = ''; // config --type <mysql|loki|all>
|
|
56
56
|
let configScope = ''; // config --scope <local|global>
|
|
57
57
|
let configAdd = false; // config --add
|
|
58
|
+
let configFrom = ''; // config --from <file.md>
|
|
58
59
|
|
|
59
60
|
for (let i = 0; i < args.length; i++) {
|
|
60
61
|
const arg = args[i];
|
|
@@ -121,6 +122,13 @@ for (let i = 0; i < args.length; i++) {
|
|
|
121
122
|
case '--add':
|
|
122
123
|
configAdd = true;
|
|
123
124
|
break;
|
|
125
|
+
case '--from':
|
|
126
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
127
|
+
console.error(fmt('red', `错误:${arg} 需要一个文件路径`));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
configFrom = path.resolve(args[++i]);
|
|
131
|
+
break;
|
|
124
132
|
case '--help': case '-h':
|
|
125
133
|
printHelp();
|
|
126
134
|
process.exit(0);
|
|
@@ -154,6 +162,7 @@ function printHelp() {
|
|
|
154
162
|
console.log(' --type <类型> config 时指定配置类型: mysql | loki | all');
|
|
155
163
|
console.log(' --scope <范围> config 时指定范围: local(当前项目) | global(全局 ~/)');
|
|
156
164
|
console.log(' --add config 时追加环境(不覆盖已有配置)');
|
|
165
|
+
console.log(' --from <文件> config 时从 Markdown 文件解析配置(跳过交互)');
|
|
157
166
|
console.log(' --help, -h 显示此帮助\n');
|
|
158
167
|
console.log('示例:');
|
|
159
168
|
console.log(' npx ai-engineering-init --tool claude');
|
|
@@ -173,6 +182,7 @@ function printHelp() {
|
|
|
173
182
|
console.log(' npx ai-engineering-init config --type all # 配置全部');
|
|
174
183
|
console.log(' npx ai-engineering-init config --type mysql --scope global # 全局配置(所有项目共享)');
|
|
175
184
|
console.log(' npx ai-engineering-init config --type mysql --add # 追加环境到已有配置');
|
|
185
|
+
console.log(' npx ai-engineering-init config --from env-config.md --scope global # 从 MD 文件一键初始化');
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
// ── 工具定义(init 用)────────────────────────────────────────────────────
|
|
@@ -576,6 +586,24 @@ function showDoneHint(toolKey) {
|
|
|
576
586
|
console.log(` 2. 在 Codex 中使用 .codex/skills/ 下的技能`);
|
|
577
587
|
console.log('');
|
|
578
588
|
}
|
|
589
|
+
showJenkinsHint();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** 检测 jenkins/ 是否已初始化,提示用户配置部署环境 */
|
|
593
|
+
function showJenkinsHint() {
|
|
594
|
+
const jenkinsDir = path.join(targetDir, 'jenkins');
|
|
595
|
+
const envParamFile = path.join(jenkinsDir, 'env_param.json');
|
|
596
|
+
const skillAssetsDir = path.join(targetDir, '.claude', 'skills', 'jenkins-deploy', 'assets');
|
|
597
|
+
|
|
598
|
+
// 技能 assets 存在但 jenkins/ 未初始化
|
|
599
|
+
if (fs.existsSync(skillAssetsDir) && !fs.existsSync(envParamFile)) {
|
|
600
|
+
console.log(fmt('yellow', fmt('bold', '📦 Jenkins 部署环境未初始化')));
|
|
601
|
+
console.log(` 项目包含 ${fmt('bold', 'jenkins-deploy')} 技能,但 jenkins/ 目录尚未配置。`);
|
|
602
|
+
console.log(` 初始化方式:`);
|
|
603
|
+
console.log(` 方式 1:在 AI 对话中说 ${fmt('bold', '"初始化部署环境"')}`);
|
|
604
|
+
console.log(` 方式 2:从团队成员处拷贝 ${fmt('bold', 'jenkins/')} 目录`);
|
|
605
|
+
console.log('');
|
|
606
|
+
}
|
|
579
607
|
}
|
|
580
608
|
|
|
581
609
|
function run(selectedTool) {
|
|
@@ -738,6 +766,7 @@ function runUpdate(selectedTool) {
|
|
|
738
766
|
console.log(` 强制更新保留文件: ${fmt('bold', hintCmd('update --force'))}`);
|
|
739
767
|
}
|
|
740
768
|
console.log('');
|
|
769
|
+
showJenkinsHint();
|
|
741
770
|
|
|
742
771
|
if (totalFailed > 0) process.exitCode = 1;
|
|
743
772
|
}
|
|
@@ -1151,8 +1180,14 @@ function runSyncBack(selectedTool, selectedSkill, doSubmit) {
|
|
|
1151
1180
|
// ── 环境配置初始化(MySQL / Loki)─────────────────────────────────────────
|
|
1152
1181
|
|
|
1153
1182
|
function runConfig() {
|
|
1183
|
+
// --from 模式:从 MD 文件解析配置(非交互式)
|
|
1184
|
+
if (configFrom) {
|
|
1185
|
+
runConfigFromFile(configFrom, configScope || 'global');
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1154
1189
|
if (!process.stdin.isTTY) {
|
|
1155
|
-
console.error(fmt('red', '错误:config
|
|
1190
|
+
console.error(fmt('red', '错误:config 命令需要交互式终端(或使用 --from <file> 跳过交互)'));
|
|
1156
1191
|
process.exit(1);
|
|
1157
1192
|
}
|
|
1158
1193
|
|
|
@@ -1571,6 +1606,224 @@ function writeLokiConfig(config, configPath, isGlobal) {
|
|
|
1571
1606
|
console.log(fmt('green', 'Loki 日志查询配置完成!'));
|
|
1572
1607
|
}
|
|
1573
1608
|
|
|
1609
|
+
// ── 从 Markdown 文件解析配置(非交互式)──────────────────────────────────────
|
|
1610
|
+
|
|
1611
|
+
function runConfigFromFile(filePath, scope) {
|
|
1612
|
+
if (!fs.existsSync(filePath)) {
|
|
1613
|
+
console.error(fmt('red', `错误:文件不存在 "${filePath}"`));
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1618
|
+
const isGlobal = scope === 'global';
|
|
1619
|
+
|
|
1620
|
+
console.log(fmt('blue', fmt('bold', `从 Markdown 文件解析配置:${filePath}`)));
|
|
1621
|
+
console.log(fmt('magenta', `配置范围:${isGlobal ? '全局(~/.claude/)' : '本地(当前项目)'}`));
|
|
1622
|
+
console.log('');
|
|
1623
|
+
|
|
1624
|
+
// 解析 MySQL 表格
|
|
1625
|
+
const mysqlConfig = parseMysqlFromMd(content);
|
|
1626
|
+
if (mysqlConfig) {
|
|
1627
|
+
const mysqlPath = isGlobal
|
|
1628
|
+
? path.join(HOME_DIR, '.claude', 'mysql-config.json')
|
|
1629
|
+
: path.join(targetDir, '.claude', 'mysql-config.json');
|
|
1630
|
+
|
|
1631
|
+
const configJson = JSON.stringify(mysqlConfig, null, 2) + '\n';
|
|
1632
|
+
const dir = path.dirname(mysqlPath);
|
|
1633
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1634
|
+
fs.writeFileSync(mysqlPath, configJson, 'utf-8');
|
|
1635
|
+
console.log(` ${fmt('green', '✔')} MySQL 配置(${Object.keys(mysqlConfig.environments).length} 个环境)→ ${mysqlPath}`);
|
|
1636
|
+
|
|
1637
|
+
// 全局同步到 cursor
|
|
1638
|
+
if (isGlobal && fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
|
|
1639
|
+
const cursorPath = path.join(HOME_DIR, '.cursor', 'mysql-config.json');
|
|
1640
|
+
fs.writeFileSync(cursorPath, configJson, 'utf-8');
|
|
1641
|
+
console.log(` ${fmt('green', '✔')} 已同步 → ${cursorPath}`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// 解析 Loki 表格
|
|
1646
|
+
const lokiConfig = parseLokiFromMd(content);
|
|
1647
|
+
if (lokiConfig) {
|
|
1648
|
+
const lokiPath = isGlobal
|
|
1649
|
+
? path.join(HOME_DIR, '.claude', 'loki-config.json')
|
|
1650
|
+
: path.join(targetDir, '.claude', 'loki-config.json');
|
|
1651
|
+
|
|
1652
|
+
const configJson = JSON.stringify(lokiConfig, null, 2) + '\n';
|
|
1653
|
+
const dir = path.dirname(lokiPath);
|
|
1654
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1655
|
+
fs.writeFileSync(lokiPath, configJson, 'utf-8');
|
|
1656
|
+
console.log(` ${fmt('green', '✔')} Loki 配置(${Object.keys(lokiConfig.environments).length} 个环境)→ ${lokiPath}`);
|
|
1657
|
+
|
|
1658
|
+
if (isGlobal && fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
|
|
1659
|
+
const cursorPath = path.join(HOME_DIR, '.cursor', 'loki-config.json');
|
|
1660
|
+
fs.writeFileSync(cursorPath, configJson, 'utf-8');
|
|
1661
|
+
console.log(` ${fmt('green', '✔')} 已同步 → ${cursorPath}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (!mysqlConfig && !lokiConfig) {
|
|
1666
|
+
console.error(fmt('red', '未在文件中找到 MySQL 或 Loki 配置表格。'));
|
|
1667
|
+
console.log('');
|
|
1668
|
+
console.log('期望格式(MySQL):');
|
|
1669
|
+
console.log(' | 环境 | host | port | user | password | range | 描述 |');
|
|
1670
|
+
console.log('');
|
|
1671
|
+
console.log('期望格式(Loki):');
|
|
1672
|
+
console.log(' | 环境 | 名称 | URL | Token | 别名 | range |');
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (!isGlobal) ensureGitignore(['mysql-config.json', 'loki-config.json']);
|
|
1677
|
+
|
|
1678
|
+
console.log('');
|
|
1679
|
+
console.log(fmt('green', fmt('bold', '配置初始化完成!')));
|
|
1680
|
+
if (isGlobal) {
|
|
1681
|
+
console.log(fmt('cyan', '技能按 本地(.claude/) → 全局(~/.claude/) 顺序查找,本地优先。'));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/** 从 Markdown 内容中解析 MySQL 配置表格 */
|
|
1686
|
+
function parseMysqlFromMd(content) {
|
|
1687
|
+
// 找到 "MySQL" 标题后的表格
|
|
1688
|
+
const mysqlSection = extractSection(content, /mysql|数据库/i);
|
|
1689
|
+
if (!mysqlSection) return null;
|
|
1690
|
+
|
|
1691
|
+
const rows = parseTable(mysqlSection);
|
|
1692
|
+
if (rows.length === 0) return null;
|
|
1693
|
+
|
|
1694
|
+
const environments = {};
|
|
1695
|
+
for (const row of rows) {
|
|
1696
|
+
const env = row['环境'] || row['env'] || '';
|
|
1697
|
+
if (!env) continue;
|
|
1698
|
+
|
|
1699
|
+
const host = row['host'] || '';
|
|
1700
|
+
const port = parseInt(row['port'] || '3306', 10);
|
|
1701
|
+
const user = row['user'] || '';
|
|
1702
|
+
const password = row['password'] || '';
|
|
1703
|
+
const range = row['range'] || '';
|
|
1704
|
+
const desc = row['描述'] || row['desc'] || `${env}环境`;
|
|
1705
|
+
|
|
1706
|
+
if (!host || host.startsWith('YOUR_')) continue; // 跳过未填写的占位符
|
|
1707
|
+
|
|
1708
|
+
environments[env] = { host, port, user, password, _desc: desc };
|
|
1709
|
+
if (range) environments[env].range = range;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (Object.keys(environments).length === 0) return null;
|
|
1713
|
+
|
|
1714
|
+
// 提取默认环境
|
|
1715
|
+
const defaultMatch = mysqlSection.match(/默认环境[::]\s*(\S+)/);
|
|
1716
|
+
const defaultEnv = defaultMatch ? defaultMatch[1] : Object.keys(environments)[0];
|
|
1717
|
+
|
|
1718
|
+
return {
|
|
1719
|
+
environments,
|
|
1720
|
+
default: defaultEnv,
|
|
1721
|
+
_comment: '从 Markdown 文件解析生成。支持 range 字段,查找顺序:本地 > 全局 ~/.claude/',
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/** 从 Markdown 内容中解析 Loki 配置表格 */
|
|
1726
|
+
function parseLokiFromMd(content) {
|
|
1727
|
+
const lokiSection = extractSection(content, /loki|日志/i);
|
|
1728
|
+
if (!lokiSection) return null;
|
|
1729
|
+
|
|
1730
|
+
const rows = parseTable(lokiSection);
|
|
1731
|
+
if (rows.length === 0) return null;
|
|
1732
|
+
|
|
1733
|
+
const environments = {};
|
|
1734
|
+
for (const row of rows) {
|
|
1735
|
+
const env = row['环境'] || row['env'] || '';
|
|
1736
|
+
if (!env) continue;
|
|
1737
|
+
|
|
1738
|
+
const name = row['名称'] || row['name'] || env;
|
|
1739
|
+
const url = row['url'] || row['URL'] || '';
|
|
1740
|
+
const token = row['token'] || row['Token'] || '';
|
|
1741
|
+
const aliasStr = row['别名'] || row['aliases'] || env;
|
|
1742
|
+
const aliases = aliasStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1743
|
+
const rangeStr = row['range'] || '';
|
|
1744
|
+
|
|
1745
|
+
if (!url || url.startsWith('YOUR_')) continue;
|
|
1746
|
+
|
|
1747
|
+
const envData = { name, url, token: token.startsWith('YOUR_') ? '' : token, aliases };
|
|
1748
|
+
if (rangeStr) {
|
|
1749
|
+
envData.range = rangeStr;
|
|
1750
|
+
envData.projects = expandRange(rangeStr);
|
|
1751
|
+
} else {
|
|
1752
|
+
envData.projects = [];
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
environments[env] = envData;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (Object.keys(environments).length === 0) return null;
|
|
1759
|
+
|
|
1760
|
+
const defaultMatch = lokiSection.match(/默认环境[::]\s*(\S+)/);
|
|
1761
|
+
const activeEnv = defaultMatch ? defaultMatch[1] : Object.keys(environments)[0];
|
|
1762
|
+
|
|
1763
|
+
return {
|
|
1764
|
+
_comment: '从 Markdown 文件解析生成。支持 range 字段,查找顺序:本地 > 全局 ~/.claude/',
|
|
1765
|
+
_setup: 'Token:Grafana → Administration → Service accounts → Add(Viewer)→ Add token',
|
|
1766
|
+
active: activeEnv,
|
|
1767
|
+
environments,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/** 从 Markdown 中按标题提取段落 */
|
|
1772
|
+
function extractSection(content, titlePattern) {
|
|
1773
|
+
const lines = content.split('\n');
|
|
1774
|
+
let start = -1;
|
|
1775
|
+
let end = lines.length;
|
|
1776
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1777
|
+
const line = lines[i];
|
|
1778
|
+
if (line.match(/^#+\s/) && titlePattern.test(line)) {
|
|
1779
|
+
start = i;
|
|
1780
|
+
const level = line.match(/^(#+)/)[1].length;
|
|
1781
|
+
// 找到同级或更高级标题作为结束
|
|
1782
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1783
|
+
const nextMatch = lines[j].match(/^(#+)\s/);
|
|
1784
|
+
if (nextMatch && nextMatch[1].length <= level) {
|
|
1785
|
+
end = j;
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (start === -1) return null;
|
|
1793
|
+
return lines.slice(start, end).join('\n');
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/** 解析 Markdown 表格为对象数组 */
|
|
1797
|
+
function parseTable(text) {
|
|
1798
|
+
const lines = text.split('\n');
|
|
1799
|
+
let headerLine = -1;
|
|
1800
|
+
|
|
1801
|
+
// 找到表头行(含 | 的行,下一行是分隔线 |---|)
|
|
1802
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
1803
|
+
if (lines[i].includes('|') && lines[i + 1] && lines[i + 1].match(/^\|[\s-:|]+\|$/)) {
|
|
1804
|
+
headerLine = i;
|
|
1805
|
+
break;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
if (headerLine === -1) return [];
|
|
1809
|
+
|
|
1810
|
+
const parseRow = (line) => line.split('|').map(s => s.trim()).filter(Boolean);
|
|
1811
|
+
const headers = parseRow(lines[headerLine]);
|
|
1812
|
+
const rows = [];
|
|
1813
|
+
|
|
1814
|
+
for (let i = headerLine + 2; i < lines.length; i++) {
|
|
1815
|
+
const line = lines[i];
|
|
1816
|
+
if (!line.includes('|')) break;
|
|
1817
|
+
const cells = parseRow(line);
|
|
1818
|
+
if (cells.length === 0) break;
|
|
1819
|
+
const row = {};
|
|
1820
|
+
headers.forEach((h, idx) => { row[h] = cells[idx] || ''; });
|
|
1821
|
+
rows.push(row);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
return rows;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1574
1827
|
// ── Config 工具函数 ─────────────────────────────────────────────────────────
|
|
1575
1828
|
|
|
1576
1829
|
function getLokiConfigPath() {
|