Compare commits

...
5 Commits
Author SHA1 Message Date
ken.li 5426b573c1 fix(jira_env): 为JIRA_BASE_URL添加默认值并优化错误提示
修改require_credentials函数,为JIRA_BASE_URL添加默认值"https://n2nafe.atlassian.net"
优化错误提示信息,明确指导用户如何配置和获取API token
2026-04-02 17:59:12 +08:00
ken.li 44b13c96f6 feat(jira): 增强提交检查和文档路径规范
- 添加已推送提交检查,避免不安全的 amend 操作
- 规范文档保存路径为 `doc/{日期}` 格式
- 扩展 markdown 解析支持表格和更多格式
- 优化 Jira 搜索 API 调用方式
2026-04-02 17:38:39 +08:00
ken.li 062e9a652b Merge branch 'main' of http://192.168.3.110:3000/AFE_SZ_DEV/agent-skills 2026-03-23 13:40:14 +08:00
ken.li 57fca4e468 feat(g3fo-commit-jira): 新增无 MCP 的 Jira CLI 并增强 AI 执行流程约束
- 新增 `jira_cli.py` 脚本,支持在不依赖 Atlassian MCP 的环境下通过同一 Jira Token 进行搜索、创建 Issue 和评论
- 新增共享配置模块 `jira_env.py`,统一加载 `jira_upload.env` 并支持在技能根目录查找配置文件
- 更新 `SKILL.md` 和参考文档,增加「AI 执行契约」和详细的前置条件说明,强制 Step 1→5 线性执行顺序
- 新增 `agent_execution_checklist.md` 检查清单,防止 AI 跳步、误用工具或未等用户确认就绑定 Issue
- 改进 `jira_cli.py` 的评论和描述生成,将常见 Markdown 语法转换为 Jira ADF 格式以获得更好的渲染效果
2026-03-23 13:38:41 +08:00
ken.li cd6c8a0450 docs(g3fo-docs): 添加中间件版本清单文档并更新技能指南
- 新增 `middleware-versions.md` 作为标准版本参考文档
- 在技能指南中更新资源引用,添加版本清单问答规范
- 明确 Web Server 与 AP Server 的角色划分及输出要求
2026-03-23 13:35:08 +08:00
12 changed files with 1331 additions and 87 deletions
+108
View File
@@ -0,0 +1,108 @@
# 影响分析报告 — g3fo-commit-jira 无 MCP 支持
## 1. 改动概览
- **背景**:技能原依赖 Cursor Atlassian MCP 完成 Jira 搜索/创建/评论;其它 IDE 无 MCP 时无法完成流程。
- **目标**:在仅配置 `jira_upload.env`(与上传附件相同 Token)时,通过脚本完成查询、创建 Issue、添加评论。
- **涉及模块**:`skills/g3fo-commit-jira/scripts/`、`SKILL.md`、`references/workflow.md`、`references/env_config.md`。
- **改动类型**:功能新增(脚本)+ 文档更新。
## 2. 方法级改动分析
| 项 | 说明 |
|----|------|
| 新增 `jira_env.py` | 统一加载 `jira_upload.env`,支持在技能根目录查找配置文件。 |
| 新增 `jira_cli.py` | `myself` / `search` / `issue` / `create` / `comment` REST 调用。 |
| 修改 `upload_attachment.py` | 改为引用 `jira_env`,行为与原先一致(多一层技能目录 config 查找)。 |
| SKILL / workflow / env_config | 描述无 MCP 路径与命令示例。 |
**与原有逻辑差异**:上传附件 URL、认证方式未变;配置查找增加 `skills/g3fo-commit-jira/jira_upload.env`。
## 3. 调用方与影响范围
- **调用方**:Agent 按 SKILL 执行;用户手动运行脚本。
- **破坏性变更**:否。未配置 env 时 `upload_attachment` 仍报错;原 MCP 流程仍可用。
- **边界**:Jira Server/Data Center 与 Cloud API 差异未专门适配(与现有上传脚本一致,面向 Cloud)。
## 4. 风险与回滚
- **风险级别**:低。新脚本失败时退回 MCP 或仅本地 amend。
- **回滚**:删除 `jira_cli.py`/`jira_env.py` 并恢复 `upload_attachment.py` 内联配置逻辑;回滚文档。
- **回滚方式是否简单**:是。
## 5. 验证与测试
- `python -m py_compile` 通过;`jira_cli.py --help` 正常。
- 真实 Jira 调用需用户环境凭证,未在 CI 中执行。
---
# 影响分析报告 — jira_cli Markdown → ADF 渲染(评论/描述)
## 1. 改动概览
- **背景**:REST 脚本原先把正文整段当作 ADF 段落,`###` 等在 Jira 界面显示为原文,与 Atlassian 插件/MCP 的 Markdown 体验不一致。
- **目标**:在 `jira_cli.py` 内将常见 Markdown 转为 ADF(heading、list、strong、code、link、codeBlock、rule、blockquote),使 `comment` / `create` 的展示接近网页富文本。
- **涉及模块**:`scripts/jira_cli.py`、`SKILL.md`。
- **改动类型**:功能增强(无 API 变更)。
## 2. 方法级改动分析
| 项 | 说明 |
|----|------|
| `markdown_to_adf`(新) | 按行解析 Markdown,输出 ADF `doc`。 |
| `plain_to_adf` | 改为委托 `markdown_to_adf`,保持调用方不变。 |
| `parse_inline_adf`(新) | 行内 `**`、`` ` ``、`[text](url)`。 |
**与原有逻辑差异**:非 Markdown 行仍按段落(含换行 hardBreak)输出;纯文本行为与旧版「多段段落」接近,但连续单行不再强制 `\\n\\n` 才分段。
## 3. 调用方与影响范围
- **调用方**:`cmd_comment`、`cmd_create` 经 `plain_to_adf` → `markdown_to_adf`。
- **破坏性变更**:否。极端表格/复杂 MD 语法未实现,可能仍以普通文本行展示。
- **边界**:与 Jira Cloud ADF 一致;Server/DC 若 API 不同需单独验证。
## 4. 风险与回滚
- **风险级别**:低。若某 ADF 节点被实例拒绝,可回退 `plain_to_adf` 为旧实现。
- **回滚方式是否简单**:是(恢复旧 `plain_to_adf` 单函数)。
## 5. 验证与测试
- 本地 `markdown_to_adf` 样例 JSON 结构校验;`py_compile` 通过。
- 完整评论 POST 需对接真实 Jira。
---
# 影响分析报告 — AI 执行流程防走偏(SKILL / workflow / checklist)
## 1. 改动概览
- **背景**:不同 AI 执行本技能时偶发跳步、先写 Jira 再看 diff、占位 Key 上传、误用 MCP 冒充附件等。
- **目标**:在文档层强制 **Step 1→5 线性顺序**、门禁表、红线与 Step 3 交互规则;新增逐步检查清单供 Agent 对照。
- **涉及模块**:`SKILL.md`、`references/workflow.md`、新增 `references/agent_execution_checklist.md`。
- **改动类型**:文档 / 流程约束增强(无脚本行为变更)。
## 2. 方法级改动分析
| 项 | 说明 |
|----|------|
| `SKILL.md` | 前置「AI 执行契约」、各 Step「前置」说明、frontmatter description 强调线性流程。 |
| `workflow.md` | 目录增加「AI 执行顺序提醒」,链回 SKILL 与 checklist。 |
| `agent_execution_checklist.md`(新) | Step 0~5 打勾表 + 走偏速查。 |
**与原有逻辑差异**:仅约束 Agent 阅读与执行顺序;不改变 Python/API 行为。
## 3. 调用方与影响范围
- **调用方**:所有读取本技能的 Agent。
- **破坏性变更**:否。用户手动跑脚本不受影响。
## 4. 风险与回滚
- **风险级别**:低。文档过长可能略增 token;可精简 checklist。
- **回滚方式是否简单**:是(还原 SKILL/workflow、删除 checklist)。
## 5. 验证与测试
- 文档结构与人读一致性自检;无自动化测试。
+134 -32
View File
@@ -1,6 +1,6 @@
---
name: g3fo-commit-jira
description: G3FO 项目 Git 提交规范自动化工具。强制要求提供 git 修订号(hash),支持用户直接提供 Jira 编号或自动查找/创建 Jira 任务。按任务类型(漏洞修复/新功能/重构等)判定需附加的文档(开发计划、影响分析、任务摘要),生成对应 MD 并按公司规范命名(<JIRA-ID>_Dev_Plan.md / _Impact_Analysis.md / _Task_Summary.md)上传到 Jira 附件,将影响分析写入评论,最后将 Jira 编号追加到 commit message 末尾后提交(不 push)。适用于 G3FO/G3SF 项目的所有 git 提交场景。
description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按 Step 1→5 线性执行**(见 SKILL 内「AI 执行契约」),每步完成后再进入下一步;强制 git 修订号校验与 HEAD 校验(amend 场景);Jira 附件仅用 upload_attachment.py。按任务类型生成文档并上传,评论影响分析,最后 amend 追加 Jira、禁止 push。详见 references/agent_execution_checklist.md。
---
# G3FO Commit Jira 技能
@@ -12,56 +12,101 @@ description: G3FO 项目 Git 提交规范自动化工具。强制要求提供 gi
---
## AI 执行契约(防走偏,**必读且优先于即兴发挥**)
### 1. 线性流程,禁止跳步
必须按 **Step 1 → Step 2 → Step 3 → Step 4 → Step 5** 顺序执行。**每完成一步**,在回复中用一句话标明 **`Step N 已完成`**,再进入下一步。禁止在未完成前置步骤时执行后续操作。
| 在未完成… | 禁止执行… |
|-----------|-----------|
| **Step 1**:`git rev-parse <hash>` 成功;若需 amend,已确认该 hash **就是当前 HEAD** | 调用 Jira、生成报告正文、落盘 `<KEY>_*.md`、`git commit --amend` |
| **Step 2**:已对照政策表写出**任务类型** + 三文档各是否需要(✅/❌) | 批量生成无关文档,或该写报告却跳过 |
| **Step 3**:已持有**真实** Jira Issue Key(用户给出 / 用户从列表选定 / 新建命令返回的 key) | 以真实路径上传附件、写绑定该 Issue 的评论(禁止用 `JIRA-XXX` 等占位 Key 落盘上传) |
| **Step 4**:本任务在 Jira 侧应做的上传/评论/建单已按政策做完 | `git commit --amend` |
### 2. 红线(违反即视为流程错误)
1. **禁止 `git push`**。本技能只做到 amend 为止。
2. **禁止跳过** `git rev-parse <hash>`;amend 场景下禁止在 **hash ≠ HEAD** 时仍执行 amend。
3. **禁止 amend 已推送的提交**(除非用户明确要求并确认风险)。使用 `git branch -r --contains <hash>` 检测。
4. **禁止用 Atlassian MCP 或其它方式冒充「已上传 MD 附件」**;上传文件 **必须** 使用 `scripts/upload_attachment.py`(MCP 无可靠上传能力时不得虚构成功)。
5. **禁止**在用户未选定 Issue、也未完成新建并取得 Key 的情况下,把某 Key 写进 commit message。
6. **禁止**未读 `references/jira_commit_docs_policy.md`(或本 SKILL 中的条件表)就默认「三份全要」或「一律不要文档」。
7. **禁止**在用户已提供 Jira 编号时仍执行搜索、展示列表或创建新 Issue。
### 3. Step 3 交互规则(易走偏)
- 用户**已给** Jira 编号 → **直接进入 Step 4**,Key = 用户给的编号,**禁止执行搜索、展示列表或创建新 Issue**。
- 用户**未给** → 搜索展示列表后,**必须等待用户输入 1~N 或 0**(或明确同意新建),**禁止**擅自替用户选一个 Issue 绑定。
- 搜索为空 → 可进入新建流程;新建成功后 Key 以 API 返回为准。
### 4. 自检
逐步执行时可对照 **`references/agent_execution_checklist.md`** 逐项确认。
---
## 前置要求
**1. 强制提供 Git 修订号 (hash)**
用户必须提供一个有效的 git 修订号(如 `HEAD` 或具体的 `hash`)。
- **校验逻辑**:使用 `git rev-parse <hash>` 检查修订号是否存在。
- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:“找不到修订号 `<hash>`,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。”
- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:"找不到修订号 `<hash>`,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。"
- **路径要求**:必须在相关的项目根目录下执行 git 命令。如果当前目录不是 git 仓库或不是目标项目,请提示用户切换到正确的项目路径。
- **已 push 提交检测**:使用 `git branch -r --contains <hash>` 检查提交是否已推送到远程。如果已推送,**默认跳过 Step 5(amend)**,仅完成 Jira 绑定和文档上传,并告知用户:
> "提交 `<hash>` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。"
- **例外**:如果用户明确要求修改已 push 的提交(如通过 `--force` 或其他方式),需用户确认风险后再执行。
**2. Jira 编号 (可选)**
用户可以主动提供 Jira 编号(如 `G3SF-123`)。
- 如果提供了 Jira 编号,直接引用该编号,跳过“查找/选择 Jira 任务”的步骤。
- 如果提供了 Jira 编号,**直接使用该编号,跳过「查找/选择/新建 Jira 任务」的步骤**,进入 Step 4 上传文档和评论。
- 如果未提供,则按流程自动查找或提示用户新建。
**3. Atlassian MCP 授权**
**3. Jira 访问方式(二选一)**
- 查看 `mcps/plugin-atlassian-atlassian/STATUS.md` 确认已在 Cursor 中授权 MCP 插件。
- 如果未授权,调用 `mcp_auth`,server: `plugin-atlassian-atlassian`,参数 `{}`。
- 若 MCP 插件未安装,引导用户按以下步骤操作:
1. 打开 Cursor Settings -> MCP
2. 添加名为 `Atlassian` 的插件
3. 按照提示完成授权并启用 skill
- 确保可用工具在 `mcps/plugin-atlassian-atlassian/tools/` 目录下已列出。
| 方式 | 适用场景 |
|------|----------|
| **REST 脚本(推荐通用)** | 任意 IDE;仅需 `jira_upload.env`(与上传附件**同一套** Token)。查 Issue、新建 Task、写评论、上传附件**全部**可走脚本。 |
| **Atlassian MCP** | 仅 Cursor 等已安装并授权 Atlassian 插件的环境;可与脚本混用(附件仍必须用脚本)。 |
**4. 上传附件配置(用于将影响分析报告 MD 上传到 Jira)**
**无 MCP 时**:必须配置 `jira_upload.env`,并用 `scripts/jira_cli.py` 完成 Step 3/4 中的查询、创建与评论(见下文「无 MCP 执行要点」)。
- Atlassian MCP 插件不支持上传文件,本技能使用自带脚本 `scripts/upload_attachment.py` 将报告 MD 上传为 Issue 附件。
- 在技能目录或脚本目录放置 `jira_upload.env`(或使用 `--config` 指定路径),包含:`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`。格式见 `jira_upload.env.example`,详细说明见 `references/env_config.md`。
- 若未配置,上传附件步骤跳过,仅通过 `jira_add_comment` 写入报告;已配置则先上传 MD 再写评论。
**有 MCP 时**:可用 MCP 搜索/创建/评论;或仍用 `jira_cli.py`(行为一致,便于脚本化)。
**4. `jira_upload.env`(上传附件 + 无 MCP 时全部 Jira API)**
- 在技能目录 `skills/g3fo-commit-jira/` 放置 `jira_upload.env`(或 `--config` 指定),包含:`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`。示例见 `jira_upload.env.example`,说明见 `references/env_config.md`。
- **上传附件**:`scripts/upload_attachment.py`(MCP 不支持上传文件)。
- **无 MCP 时查 Jira / 建单 / 评论**:`scripts/jira_cli.py`,与上传使用**相同** Token,详见 `references/env_config.md` 与 `references/workflow.md`「无 MCP / jira_cli」。
- 若未配置:无法调用 Jira API;须提示用户创建 `jira_upload.env` 或在本机 Cursor 使用 MCP。
---
## 执行步骤
## 执行步骤(严格顺序)
> **提醒**:仅当上一节「执行契约」中本步的前置条件已满足时,才执行本节对应步骤。
### Step 1 - 获取提交内容
```powershell
git show <hash> # 获取 diff + 元数据
git log -1 --format="%B" <hash> # 获取原始 commit message
git branch -r --contains <hash> # 检查是否已推送到远程(有输出则已 push)
```
分析要点:
- 涉及的服务模块/包名
- 新增/修改/删除的方法与接口
- 逻辑改动的核心目的
- **是否已推送**:如果 `git branch -r --contains <hash>` 有输出,说明已推送,Step 5 将跳过 amend
### Step 2 - 判定任务类型并确定需生成的文档
根据改动内容与 `references/jira_commit_docs_policy.md` 中的**详细条件表**,先判定任务类型,再决定需要生成并上传的文档:
**前置**:Step 1 已完成。
根据改动内容与 `references/jira_commit_docs_policy.md` 中的**详细条件表**,先判定任务类型,再决定需要生成并上传的文档(须在回复中写明类型与三文档要/不要):
| 任务类型 | 开发计划 | 影响分析 | 任务摘要 |
|----------|:--------:|:--------:|:--------:|
@@ -78,29 +123,45 @@ git log -1 --format="%B" <hash> # 获取原始 commit message
### Step 2(续)- 生成所需报告内容
对上述判定为“需要”的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件):
对上述判定为"需要"的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件):
- **开发计划**:步骤、设计方法、技术决策。
- **影响分析**:改动概览、方法级分析、影响范围、风险与回滚、验证与测试(模板见 workflow.md)。
- **任务摘要**:所完成工作的简要回顾,以及任何偏离计划之处。
生成后先不按“公司附件名”落盘,等取得 Jira 编号后再以 `<JIRA-ID>_*.md` 命名保存并上传。
生成后先不按"公司附件名"落盘,等取得 Jira 编号后再以 `<JIRA-ID>_*.md` 命名保存并上传。
**文档保存路径规则**:
- 所有生成的 MD 文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
- 若该文件夹不存在,**必须先创建**再保存文件
- 示例路径:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
### Step 3 - 确定 Jira 任务
**前置**:Step 2 已完成(至少已判定文档需求;若需文档,可先草稿内容,**真实 Key 确定后再按 `<KEY>_*.md` 保存**)。
**1. 如果用户已提供 Jira 编号:**
- 直接使用该编号(如 `G3SF-123`),进入 Step 4。
- **直接使用该编号**(如 `G3SF-123`),**跳过查找、选择和新建流程**,直接进入 Step 4。
- 不执行 JQL 搜索,不展示列表,不创建新 Issue。
**2. 如果用户未提供 Jira 编号:**
- 使用 JQL 查找当前用户在 G3SF 项目下的进行中任务(limit 5):
```
project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC
```
- 展示列表供用户选择,或允许用户输入 `0` 新建。
- **有 MCP**:用 MCP 的 JQL 搜索,展示列表。
- **无 MCP**:执行(PowerShell 用 `;` 分隔):
```powershell
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" search --limit 5
```
可加 `--format json` 供解析。展示列表供用户选择,或输入 `0` 新建。
- 如果搜索结果为空,自动进入新建流程。
### Step 4 - 写入 Jira(附件 + 评论)
**前置**:已持有本任务最终 **Issue Key**(Step 3)。
**附件命名规范(公司要求)**:上传到 Jira 的 MD 文件名必须为
`<JIRA-ID>_Dev_Plan.md`、`<JIRA-ID>_Impact_Analysis.md`、`<JIRA-ID>_Task_Summary.md`。
根据 Step 2 判定结果,仅上传“需要”的文档;每个文件先按该命名写入本地再上传(脚本以本地文件名为 Jira 附件名)。
@@ -108,29 +169,59 @@ project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY
**如果选择/新建的任务 ID > 0:**
1. **保存并上传附件**(已配置 `jira_upload.env` 且本任务需要文档时):
将 Step 2 生成的各文档内容,按公司规范命名写入当前项目目录(或 `docs/` 等),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。
将 Step 2 生成的各文档内容,按公司规范命名写入项目 `doc/{日期}` 目录(日期格式 `YYYY-MM-DD`,若不存在则先创建),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。
```powershell
# 创建日期目录(若不存在)
$dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')"
if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force }
# 示例:需要三份文档时,先写入 G3SF-123_Dev_Plan.md、G3SF-123_Impact_Analysis.md、G3SF-123_Task_Summary.md,再:
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "G3SF-123_Impact_Analysis.md" --file "G3SF-123_Dev_Plan.md" --file "G3SF-123_Task_Summary.md"
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "$dateFolder/G3SF-123_Impact_Analysis.md" --file "$dateFolder/G3SF-123_Dev_Plan.md" --file "$dateFolder/G3SF-123_Task_Summary.md"
```
若本任务仅需影响分析,则只写入并上传 `G3SF-123_Impact_Analysis.md`。未配置 `jira_upload.env` 时跳过上传,仅写评论。
2. **写评论**:若有影响分析内容,调用 `jira_add_comment` 将影响分析报告正文作为评论写入 issue。
若本任务仅需影响分析,则只写入并上传 `G3SF-123_Impact_Analysis.md`。未配置 `jira_upload.env` 时跳过上传;有 MCP 时可仅写评论,无 MCP 则必须配置 env 才能完成评论。
2. **写评论**(有影响分析正文时):
- **MCP**:`jira_add_comment`,正文为影响分析报告(插件侧多为 Markdown 渲染)。
- **无 MCP**:`jira_cli.py comment/create` 会将常见 **Markdown**(`#`~`######` 标题、`-`/`1.` 列表、`**粗体**`、`` `代码` ``、代码块、`[链](url)` 等)转为 **Jira ADF**,在网页上与富文本一致;仍建议长文用 `--body-file`。将报告写入临时文件后:
```powershell
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" comment --issue "G3SF-123" --body-file "path\to\impact_body.md"
```
或使用 `--body "..."`(长文建议 `--body-file`)。
3. 记录该任务的 issue key。
**如果用户选择 0 或搜索无结果:**
- 调用 `jira_create_issue` 创建新任务,详情见 `references/workflow.md`
- 设置 project key: `G3SF`, issue type: `Task`
- **Summary**:必须在标题前添加 `[G3SF]` 前缀,格式为 `[G3SF] 提炼后的改动摘要`
- **Description**:完整影响分析报告(Markdown 格式);若本任务需开发计划/任务摘要,可在创建后通过附件上传
- **Assignee**:必须设置为当前登录账号(由 MCP 传入当前用户或等效参数)
- 获取返回的新 issue key(如 `G3SF-123`)
- 若已配置 `jira_upload.env` 且本任务需要文档,创建成功后按公司命名写入并上传对应 MD 文件到该新 Issue
- **MCP**:调用 `jira_create_issue`(见 `references/workflow.md`)。
- **无 MCP**:使用 `jira_cli.py create`(经办人默认为 Token 对应用户):
```powershell
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" create --summary "[G3SF] 提炼后的改动摘要" --description-file "path\to\impact.md"
```
脚本会打印新 issue key(如 `G3SF-123`);可加 `--format json` 解析 `key` 字段。
**新建 Issue 字段约定**(两种途径均需遵守):
- project: `G3SF`,issuetype: `Task`
- **Summary**:`[G3SF]` 前缀 + 一句话摘要
- **Description**:完整影响分析(Markdown 可先写入文件再用 `--description-file`)
- **Assignee**:当前用户(MCP 传 accountId;`jira_cli` 默认 `assignee` = API Token 对应账号)
- 若需要文档,创建成功后按公司命名上传对应 MD 到该 Issue
### Step 5 - 更新 Git 提交信息
**前置**:Step 4 已按政策完成(无需文档的绑定类任务可跳过上传/评论,但须已有 Key)。
**检查提交是否已推送**:
```powershell
git branch -r --contains <hash>
```
- 如果有输出,说明提交已推送到远程分支,**跳过 amend**,告知用户:
> "提交 `<hash>` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。"
- 如果无输出,说明提交未推送,继续执行 amend。
**执行 amend(仅当未推送时)**:
使用获取到的 Jira 编号,通过 `git commit --amend` 更新 commit message。**严禁 push**。
**注意**:在 Windows PowerShell 环境下,需确保 UTF8 编码以防止中文乱码。
@@ -149,16 +240,27 @@ git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim
完成后告知用户:
**未推送时**:
```
绑定成功:
Jira: G3SF-123 (https://your-jira/browse/G3SF-123)
Commit: <git log --oneline -1 的结果>
已追加 Jira 编号到 commit message
```
**已推送时**:
```
绑定完成(提交已推送,未修改 commit message):
Jira: G3SF-123 (https://your-jira/browse/G3SF-123)
Commit: <git log --oneline -1 的结果>
提示:提交已推送到远程,如需修改 commit message 请手动处理
```
---
## 相关参考
- **AI 逐步检查清单(防走偏)**:`references/agent_execution_checklist.md`
- **公司 Jira 附件文档政策**(何时附加哪些文件):`references/jira_commit_docs_policy.md`
- **报告模板、三文档说明与错误处理**:`references/workflow.md`
- **上传附件配置**:`references/env_config.md`;脚本 `scripts/upload_attachment.py`,依赖见 `scripts/requirements.txt`
+32
View File
@@ -0,0 +1,32 @@
# 任务总结 — g3fo-commit-jira 增强
## 任务信息
- **任务**:无 Atlassian MCP 时仍可通过同一 Jira Token 查 Jira、建单、评论。
- **范围**:`skills/g3fo-commit-jira`。
## 改动说明
- 新增 `scripts/jira_cli.py`(search / issue / create / comment / myself)。
- 新增 `scripts/jira_env.py`,`upload_attachment.py` 复用配置加载;支持在技能目录放置 `jira_upload.env`。
- 更新 `SKILL.md`、`references/workflow.md`、`references/env_config.md`。
## 影响与风险
- 无破坏性变更;风险低。详见 `IMPACT_ANALYSIS.md`。
## 测试
- 本地语法与 CLI `--help` 已验证;连通性依赖用户 `jira_upload.env`。
## 后续
- 若需同步到全局技能目录,可使用项目内 `skill-sync` 流程。
---
## 任务总结 — AI 流程防走偏(文档增强)
- **目标**:减少 Agent 跳步、误上传、未等用户选 Issue 等偏离。
- **改动**:`SKILL.md` 增加「AI 执行契约」与各 Step 前置条件;新增 `references/agent_execution_checklist.md`;`workflow.md` 增加执行顺序提醒。
- **测试**:文档审阅;逻辑无代码变更。
@@ -0,0 +1,80 @@
# AI 执行检查清单(g3fo-commit-jira)
执行本技能时建议 **边做边勾**( mentally 或写在回复里),避免跳步、顺序颠倒或误用工具。
---
## 开始前(Step 0)
- [ ] 用户已提供 **git 修订号**(如 `HEAD` 或完整/短 hash)
- [ ] 在 **目标业务仓库根目录** 执行 git(`git rev-parse --is-inside-work-tree` 为真)
- [ ] 已执行 `git rev-parse <hash>`,修订号存在
- [ ] 若要对提交做 amend:已确认 `git rev-parse HEAD` **等于** `git rev-parse <hash>`(否则停止,提示用户)
- [ ] 已检查 `git branch -r --contains <hash>`,确认提交是否已推送(已推送则跳过 amend)
---
## Step 1 — 获取提交内容
- [ ] 已运行 `git show <hash>`(或等效)并理解改动范围
- [ ] 已记录原始 commit message(供 Step 5)
**未完成 Step 0~1 前:禁止** 写 Jira、生成报告、amend。
---
## Step 2 — 任务类型与文档
- [ ] 已阅读并对照 `references/jira_commit_docs_policy.md`(或 SKILL 中的条件表)
- [ ] 已明确写出:**任务类型** + **开发计划 / 影响分析 / 任务摘要** 各是否需要(✅/❌)
- [ ] 若需要文档:已按 `workflow.md` 模板准备内容(可先草稿,**真实 Jira Key 出来后再按名落盘**)
**禁止**:未判定类型就上传三份或一份都不写却写长评论(应与政策一致)。
---
## Step 3 — Jira Key
- [ ] 已有 **真实 Issue Key**(如 `G3SF-123`):来自用户直给、用户从列表选择、或 `create` 成功返回
- [ ] 若用户已提供 Jira 编号:**跳过搜索、展示列表和新建流程**,直接使用该编号
- [ ] 若走列表:已 **等用户选 1~N 或 0**,未擅自替用户绑定
**未取得真实 Key 前:禁止** 使用 `<JIRA-ID>_*.md` 落盘上传(禁止占位符 Key)。
**用户已提供 Jira 时:禁止** 执行搜索、展示列表或创建新 Issue。
---
## Step 4 — 写入 Jira
- [ ] 需要附件时:文件名为 `<KEY>_Dev_Plan.md` / `_Impact_Analysis.md` / `_Task_Summary.md`(仅实际上传需要的)
- [ ] 文档保存路径:`doc/{日期}`(日期格式 `YYYY-MM-DD`),若目录不存在则先创建
- [ ] 附件 **仅** 通过 `scripts/upload_attachment.py` + `jira_upload.env`(不用 MCP 冒充上传)
- [ ] 需要影响分析进评论时:MCP `jira_add_comment` 或 `jira_cli.py comment`(与政策一致)
- [ ] 新建 Issue 时:Summary 带 `[G3SF]`,类型 Task,描述/评论与内容一致
---
## Step 5 — Git amend
- [ ] 已检查 `git branch -r --contains <hash>`:若已推送,**跳过 amend**,告知用户
- [ ] 若未推送:仅 `git commit --amend`,**未** `git push`
- [ ] Windows 已按 SKILL 处理 UTF-8 / `--cleanup=verbatim`(如适用)
- [ ] 若 message 已含 `**** Jira`,未重复追加
---
## 走偏速查
| 现象 | 纠正 |
|------|------|
| 先写 Jira 再去看 diff | 回到 Step 1 |
| 没有 Key 就上传 | 先完成 Step 3 |
| 用 MCP 上传 md | 改用 `upload_attachment.py` |
| 简单拼写改却写三份文档 | 重读政策表,按类型裁剪 |
| 执行了 push | 违反技能范围,后续勿再 push |
| amend 已推送的提交 | 先检查 `git branch -r --contains`,已推送则跳过 amend |
| 用户已给 Jira 却还搜索/新建 | 直接使用用户提供的编号,跳过 Step 3 的搜索流程 |
---
**原则**:顺序 = 1 → 2 → 3 → 4 → 5;每步通过后再进入下一步;不确定时重读 `SKILL.md` 本节与 `workflow.md` 错误处理表。
@@ -49,6 +49,35 @@ python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3f
---
## 无 MCP:`jira_cli.py`(查 Jira / 建单 / 评论)
在 **未安装 Atlassian MCP** 的环境(其它 IDE、终端)中,使用与上传附件**相同**的 `jira_upload.env`,通过 `scripts/jira_cli.py` 调用 Jira REST API v3:
| 子命令 | 作用 |
|--------|------|
| `myself` | 当前用户 accountId / 邮箱 |
| `search` | JQL 搜索(默认:G3SF 进行中且指派给当前用户) |
| `issue <KEY>` | 查看单条 Issue |
| `create` | 创建 Issue(默认 G3SF + Task,经办人为 Token 用户) |
| `comment` | 添加评论 |
依赖:`pip install -r scripts/requirements.txt`(仅需 `requests`)。
```powershell
# 搜索(与技能 Step 3 默认 JQL 一致)
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" search --limit 5
# 新建(描述来自文件)
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" create --summary "[G3SF] 简述" --description-file "D:\tmp\impact.md"
# 评论
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" comment --issue G3SF-123 --body-file "D:\tmp\comment.md"
```
配置文件查找顺序:当前工作目录、`scripts/` 下、`skills/g3fo-commit-jira/` 下的 `jira_upload.env`。详细参数见 `references/workflow.md`「无 MCP:jira_cli 速查」。
---
## 环境变量覆盖
若同时存在配置文件和环境变量,**环境变量优先**。
@@ -46,3 +46,9 @@
示例:工单 `G3SF-123` 的三个附件命名为
`G3SF-123_Dev_Plan.md`、`G3SF-123_Impact_Analysis.md`、`G3SF-123_Task_Summary.md`。
**本地保存路径**:
- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
- 若该文件夹不存在,**必须先创建**再保存文件
- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
+86 -6
View File
@@ -1,6 +1,7 @@
# G3FO Commit Jira — 详细工作流参考
## 目录
0. [AI 执行顺序提醒](#ai-执行顺序提醒)
1. [公司 Jira 附件文档政策](#公司-jira-附件文档政策)
2. [Step 1: 获取并解析提交内容](#step-1-获取并解析提交内容)
3. [Step 2: 任务类型判定与三文档模板](#step-2-任务类型判定与三文档模板)
@@ -12,6 +13,13 @@
---
## AI 执行顺序提醒
与 **`SKILL.md` 中「AI 执行契约」** 一致:必须 **Step 1 → 2 → 3 → 4 → 5**,不可跳步。附件仅通过 **`upload_attachment.py`**;**禁止 push**。
逐步打勾请用 **`references/agent_execution_checklist.md`**。
---
## 公司 Jira 附件文档政策
自 **2026年3月9日** 起,何时附加「开发计划」「影响分析」「任务摘要」以公司强制性指南为准。
@@ -34,6 +42,10 @@ $head = git rev-parse HEAD
$provided = git rev-parse <hash>
if ($head -ne $provided) { Write-Error "提交不是 HEAD,无法 amend" }
# 检查提交是否已推送到远程
git branch -r --contains <hash>
# 如果有输出,说明已推送,Step 5 将跳过 amend
# 获取完整 diff 和元数据
git show <hash>
@@ -55,6 +67,7 @@ git diff-tree --no-commit-id -r --name-status <hash>
| `+` 行 | 新增代码(方法签名、接口、配置) |
| `-` 行 | 删除/替换代码 |
| 文件路径中的包名 | 推断所属服务(`g3fo-trade-service`, `g3fo-margin-service` 等) |
| `git branch -r --contains` 输出 | 是否已推送(有输出 = 已推送,跳过 amend) |
G3FO 服务包路径规律:
- `com.afe.g3fo.<service-short-name>.*` → 对应服务
@@ -163,6 +176,17 @@ G3FO 服务包路径规律:
## Step 3: Jira 任务查找与确认
### 用户已提供 Jira 编号
如果用户已提供 Jira 编号(如 `G3SF-123`):
- **直接使用该编号**,跳过 JQL 搜索、列表展示和新建流程
- **禁止**执行搜索、展示列表或创建新 Issue
- 直接进入 Step 4 上传文档和评论
### 用户未提供 Jira 编号
执行 JQL 查询:
### JQL 查询
```
@@ -223,12 +247,23 @@ Summary 生成规则:
- `<JIRA-ID>_Dev_Plan.md`
- `<JIRA-ID>_Impact_Analysis.md`
- `<JIRA-ID>_Task_Summary.md`
仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。保存位置:当前 Git 项目根目录或项目约定目录(如 `docs/`)。
仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。
**保存路径规则**:
- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
- 若该文件夹不存在,**必须先创建**再保存文件
- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
2. **上传到 Jira 附件**:Atlassian MCP 不支持上传文件,使用本技能自带脚本上传上述 MD 到当前 Issue:
- 配置文件:在技能目录下配置 `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`),详见 `references/env_config.md`。
- 命令示例(PowerShell 用 `;` 连接,勿用 `&&`),按需上传多文件:
```powershell
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "G3SF-101_Impact_Analysis.md" --file "G3SF-101_Dev_Plan.md" --file "G3SF-101_Task_Summary.md"
# 创建日期目录(若不存在)
$dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')"
if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force }
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "$dateFolder/G3SF-101_Impact_Analysis.md" --file "$dateFolder/G3SF-101_Dev_Plan.md" --file "$dateFolder/G3SF-101_Task_Summary.md"
```
- 未配置 `jira_upload.env` 时跳过上传,仅写评论。
@@ -251,6 +286,28 @@ Summary 生成规则:
## Step 5: Git Amend 命令详解
### 检查提交是否已推送
在执行 amend 前,必须检查提交是否已推送到远程:
```powershell
# 检查提交是否已推送
$pushedBranches = git branch -r --contains <hash>
if ($pushedBranches) {
Write-Host "提交已推送到远程,跳过 amend"
Write-Host "已完成 Jira 绑定和文档上传"
Write-Host "如需修改 commit message,请手动处理或提供特别说明"
# 跳过 amend,直接结束
return
}
```
**已推送的处理**:
- 如果 `git branch -r --contains <hash>` 有输出,说明提交已推送到远程分支
- **跳过 amend**,仅完成 Jira 绑定和文档上传
- 告知用户提交已推送,无法 amend
- 例外:用户明确要求修改已推送的提交时,需确认风险后再执行
### PowerShell(Windows 环境)
在 Windows PowerShell 中,直接使用 `$origMsg` 可能会导致编码问题(乱码)。必须显式指定输出编码为 UTF8。
@@ -297,20 +354,43 @@ git log --oneline -1 # 查看 commit 摘要
---
## 无 MCP:`jira_cli.py` 速查
与 `upload_attachment.py` **共用** `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`)。从仓库根执行时建议 `--config` 指向技能目录下的配置文件。
| 操作 | 命令示例 |
|------|----------|
| 当前用户(accountId) | `python .../jira_cli.py --config ".../jira_upload.env" myself` |
| 默认 JQL 搜索(G3SF 进行中、指派给我) | `python .../jira_cli.py --config "..." search --limit 5` |
| 自定义 JQL | `python .../jira_cli.py --config "..." search --jql "project = G3SF AND key = G3SF-1" --limit 10` |
| 机器可读 | 上述命令加 `--format json` |
| 单条 Issue | `python .../jira_cli.py --config "..." issue G3SF-123` |
| 新建 Task | `python .../jira_cli.py --config "..." create --summary "[G3SF] 标题" --description-file report.md` |
| 新建(内联描述) | `create --summary "..." --description "多段用\n\n分隔"` |
| 添加评论 | `python .../jira_cli.py --config "..." comment --issue G3SF-123 --body-file impact.md` |
| 不上传经办人 | `create ... --no-assign-self`(若站点禁止创建时指定经办人) |
**说明**:`create` 默认 `project=G3SF`、`issuetype=Task`;描述与评论由脚本将 **Markdown** 转为 Jira Cloud **ADF**(`#` 标题、列表、`**粗体**`、代码块等会按富文本展示,不再出现 `###` 原文)。上传 MD 附件仍用 `upload_attachment.py`。
---
## 错误处理表
| 错误场景 | 检测方式 | 处理方式 |
|---------|---------|---------|
| 修订号不存在 | `git rev-parse <hash>` 报错 | **停止**,提示用户提供正确的修订号。 |
| 提交 hash 不是 HEAD | `git rev-parse HEAD` ≠ `git rev-parse <hash>` | 停止,提示用户确认 hash(只有 HEAD 才能 amend)。 |
| 提交已推送到远程 | `git branch -r --contains <hash>` 有输出 | **跳过 amend**,仅完成 Jira 绑定和文档上传,告知用户提交已推送。 |
| 项目路径不正确 | `git rev-parse --is-inside-work-tree` 失败 | 提示用户在正确的 git 项目目录下操作。 |
| Atlassian MCP 未鉴权 | `STATUS.md` 有提示 / 工具调用返回 401 | 调用 `mcp_auth`,等待用户完成授权。 |
| Atlassian 插件未安装 | `tools/` 目录为空或 MCP 调用失败 | 引导用户在 Cursor Settings → MCP 安装 Atlassian 插件 |
| Jira 项目 G3SF 不存在 | `jira_create_issue` 返回 404/400 | 提示用户确认 Jira 实例 URL 和项目 key |
| 无 MCP 且未配置 jira_upload.env | 无法执行 jira_cli / 上传 | **必须**配置 `jira_upload.env` 或改用 Cursor + MCP。 |
| Atlassian MCP 未鉴权 | `STATUS.md` 有提示 / 工具调用返回 401 | 调用 `mcp_auth`,或改用 `jira_cli.py`。 |
| Atlassian 插件未安装 | `tools/` 目录为空或 MCP 调用失败 | 使用 `jira_cli.py` + `jira_upload.env`,或在 Cursor 安装 Atlassian 插件 |
| Jira 项目 G3SF 不存在 | 创建/搜索返回 404/400 | 提示用户确认 `JIRA_BASE_URL`、项目 key;`create` 可用 `--project` |
| 工作区有未提交变更 | `git status --porcelain` 有输出 | 警告:amend 会将未暂存变更排除在外,建议先 `git add` 或 stash |
| commit message 中已有 Jira 行 | message 中包含 `**** Jira` | 跳过追加,提示用户该提交已绑定 |
| 用户已提供 Jira 编号 | 用户输入包含 Jira key | **跳过搜索和新建**,直接使用用户提供的编号 |
| Jira 搜索无结果 | 工具返回空列表 | 直接进入新建流程,无需用户确认 |
| 未配置 jira_upload.env | 上传前检查配置不存在 | 跳过上传附件,仅通过 jira_add_comment 写入报告;提示用户可配置后使用脚本上传 |
| 未配置 jira_upload.env | 上传前检查配置不存在 | **有 MCP**:跳过上传,仅评论。**无 MCP**:无法完成 Jira 操作,须配置 env 或使用 MCP。 |
| 任务类型判定为「无需文档」 | 按政策表属简单漏洞/琐碎改动 | 不生成报告、不写评论、不上传附件;仍执行 Jira 绑定与 commit --amend,将 Jira 编号追加到 commit message |
---
+673
View File
@@ -0,0 +1,673 @@
#!/usr/bin/env python3
"""
Jira REST API CLI — same credentials as upload_attachment.py (jira_upload.env).
Use when Atlassian MCP is unavailable (other IDEs).
Commands:
myself Current user (accountId for assignee)
search JQL search
issue Get one issue by key
create Create Task (G3SF by default), assign to self
comment Add comment (ADF) to issue
Examples:
python jira_cli.py --config ../jira_upload.env search --limit 5
python jira_cli.py --config ../jira_upload.env issue G3SF-123
python jira_cli.py --config ../jira_upload.env create --summary "[G3SF] fix foo" --description-file report.md
python jira_cli.py --config ../jira_upload.env comment --issue G3SF-123 --body-file impact.md
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
try:
import requests
except ImportError:
print("ERROR: pip install requests", file=sys.stderr)
sys.exit(1)
from jira_env import CONFIG_FILENAME, load_config, require_credentials, session_headers
DEFAULT_JQL = (
"project = G3SF AND assignee = currentUser() AND statusCategory != Done "
"ORDER BY updated DESC"
)
def _empty_doc() -> Dict[str, Any]:
return {
"type": "doc",
"version": 1,
"content": [{"type": "paragraph", "content": [{"type": "text", "text": " "}]}],
}
def _text_nodes(s: str) -> List[Dict[str, Any]]:
"""Single plain text node (non-empty)."""
if not s:
return []
return [{"type": "text", "text": s}]
def parse_inline_adf(s: str) -> List[Dict[str, Any]]:
"""
Parse inline **bold**, ~~strikethrough~~, `code`, *italic*, [text](url), ***bold italic***
into ADF text nodes with marks.
"""
if not s:
return []
# Order: links, bold+italic, bold, strikethrough, italic, code (non-greedy).
pattern = re.compile(
r"\[([^\]]+)\]\(([^)]+)\)" # [label](url)
r"|\*\*\*(.+?)\*\*\*" # ***bold italic***
r"|\*\*(.+?)\*\*" # **bold**
r"|~~(.+?)~~" # ~~strikethrough~~
r"|(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)" # *italic* (avoid matching **)
r"|(?<!`)`([^`\n]+)`(?!`)" # `code`
)
nodes: List[Dict[str, Any]] = []
last = 0
for m in pattern.finditer(s):
if m.start() > last:
chunk = s[last : m.start()]
if chunk:
nodes.append({"type": "text", "text": chunk})
if m.group(1) is not None and m.group(2) is not None:
nodes.append(
{
"type": "text",
"text": m.group(1),
"marks": [{"type": "link", "attrs": {"href": m.group(2).strip()}}],
}
)
elif m.group(3) is not None:
nodes.append(
{
"type": "text",
"text": m.group(3),
"marks": [{"type": "strong"}, {"type": "em"}],
}
)
elif m.group(4) is not None:
nodes.append(
{"type": "text", "text": m.group(4), "marks": [{"type": "strong"}]}
)
elif m.group(5) is not None:
nodes.append(
{"type": "text", "text": m.group(5), "marks": [{"type": "strike"}]}
)
elif m.group(6) is not None:
nodes.append(
{"type": "text", "text": m.group(6), "marks": [{"type": "em"}]}
)
elif m.group(7) is not None:
nodes.append(
{"type": "text", "text": m.group(7), "marks": [{"type": "code"}]}
)
last = m.end()
if last < len(s):
tail = s[last:]
if tail:
nodes.append({"type": "text", "text": tail})
return nodes
def _split_table_row(line: str) -> List[str]:
"""Split a pipe-delimited table row into cells, stripping whitespace."""
parts = line.split("|")
cells = []
for p in parts:
c = p.strip()
if c:
cells.append(c)
return cells
def _is_separator_row(cells: List[str]) -> bool:
"""Check if a row is a GFM table separator (e.g., |---|---|)."""
return bool(cells) and all(re.fullmatch(r"-{3,}", c.strip()) for c in cells)
def _is_table_row(line: str) -> bool:
"""Detect if a line looks like a GFM table row."""
stripped = line.strip()
if not stripped.startswith("|"):
return False
cells = _split_table_row(stripped)
return len(cells) >= 1
def _parse_table(lines: List[str], start: int, n: int):
"""
Parse a GFM table starting at index start.
Returns (adf_table_dict, next_index).
"""
header_cells = _split_table_row(lines[start].strip())
num_cols = len(header_cells)
i = start + 1
# Skip separator row if present
if i < n and _is_separator_row(_split_table_row(lines[i].strip())):
i += 1
data_rows: List[List[str]] = []
while i < n:
s = lines[i].strip()
if not s or not _is_table_row(s):
break
row_cells = _split_table_row(s)
# Pad or trim to match header column count
while len(row_cells) < num_cols:
row_cells.append("")
data_rows.append(row_cells[:num_cols])
i += 1
# Build ADF table
def _make_cell(text: str, is_header: bool = False) -> Dict[str, Any]:
cell_type = "tableHeader" if is_header else "tableCell"
inline_nodes = parse_inline_adf(text) or _text_nodes(text or " ")
return {
"type": cell_type,
"content": [{"type": "paragraph", "content": inline_nodes}],
}
def _make_row(cell_texts: List[str], is_header: bool = False) -> Dict[str, Any]:
return {
"type": "tableRow",
"content": [_make_cell(c, is_header=is_header) for c in cell_texts],
}
table_content: List[Dict[str, Any]] = [_make_row(header_cells, is_header=True)]
for dr in data_rows:
table_content.append(_make_row(dr))
return {"type": "table", "content": table_content}, i
def _paragraph_from_buffer(lines: List[str]) -> Optional[Dict[str, Any]]:
if not lines:
return None
inner: List[Dict[str, Any]] = []
for i, line in enumerate(lines):
if i > 0:
inner.append({"type": "hardBreak"})
inner.extend(parse_inline_adf(line) or _text_nodes(line))
if not inner:
return None
return {"type": "paragraph", "content": inner}
def markdown_to_adf(text: str) -> Dict[str, Any]:
"""
Convert common Markdown to Atlassian Document Format (headings, lists, bold, code, links).
Jira REST API stores comments/descriptions as ADF; plain paragraphs showed ### literally.
"""
text = text or ""
if not text.strip():
return _empty_doc()
lines = text.split("\n")
content: List[Dict[str, Any]] = []
n = len(lines)
i = 0
para_buf: List[str] = []
def flush_paragraph() -> None:
nonlocal para_buf
if not para_buf:
return
p = _paragraph_from_buffer(para_buf)
para_buf = []
if p:
content.append(p)
while i < n:
raw = lines[i]
stripped = raw.strip()
if not stripped:
flush_paragraph()
i += 1
continue
# ATX heading # .. ######
hm = re.match(r"^(#{1,6})\s+(.+)$", stripped)
if hm and len(hm.group(1)) <= 6:
flush_paragraph()
level = len(hm.group(1))
title = hm.group(2).strip()
title_nodes = parse_inline_adf(title) or _text_nodes(title)
content.append(
{"type": "heading", "attrs": {"level": level}, "content": title_nodes}
)
i += 1
continue
# Horizontal rule
if re.fullmatch(r"[-*_]{3,}", stripped):
flush_paragraph()
content.append({"type": "rule"})
i += 1
continue
# GFM Table: | col1 | col2 |
if _is_table_row(stripped):
flush_paragraph()
table_adf, i = _parse_table(lines, i, n)
content.append(table_adf)
continue
# Fenced code block
if stripped.startswith("```"):
flush_paragraph()
lang = stripped[3:].strip() or "plaintext"
code_lines: List[str] = []
i += 1
while i < n:
if lines[i].strip().startswith("```"):
i += 1
break
code_lines.append(lines[i])
i += 1
code_text = "\n".join(code_lines)
content.append(
{
"type": "codeBlock",
"attrs": {"language": lang},
"content": [{"type": "text", "text": code_text or " "}],
}
)
continue
# Bullet list (consecutive - or *)
if re.match(r"^[-*]\s+", stripped):
flush_paragraph()
items: List[str] = []
while i < n:
s = lines[i].strip()
if not s:
break
bm = re.match(r"^[-*]\s+(.*)$", s)
if not bm:
break
items.append(bm.group(1))
i += 1
if items:
content.append(
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": parse_inline_adf(it)
or _text_nodes(it),
}
],
}
for it in items
],
}
)
continue
# Ordered list
om = re.match(r"^(\d+)\.\s+(.*)$", stripped)
if om:
flush_paragraph()
start_order = int(om.group(1))
items = [om.group(2)]
i += 1
while i < n:
s = lines[i].strip()
if not s:
break
m = re.match(r"^\d+\.\s+(.*)$", s)
if not m:
break
items.append(m.group(1))
i += 1
content.append(
{
"type": "orderedList",
"attrs": {"order": start_order},
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": parse_inline_adf(it)
or _text_nodes(it),
}
],
}
for it in items
],
}
)
continue
# Blockquote: single line > text
if stripped.startswith("> "):
flush_paragraph()
quote_lines: List[str] = [stripped[2:].strip()]
i += 1
while i < n and lines[i].strip().startswith("> "):
quote_lines.append(lines[i].strip()[2:].strip())
i += 1
q_inner: List[Dict[str, Any]] = []
for j, ql in enumerate(quote_lines):
if j > 0:
q_inner.append({"type": "hardBreak"})
q_inner.extend(parse_inline_adf(ql) or _text_nodes(ql))
content.append(
{
"type": "blockquote",
"content": [{"type": "paragraph", "content": q_inner}],
}
)
continue
para_buf.append(raw)
i += 1
flush_paragraph()
if not content:
return _empty_doc()
return {"type": "doc", "version": 1, "content": content}
def plain_to_adf(text: str) -> Dict[str, Any]:
"""Backward-compatible name: Markdown-aware conversion for Jira ADF."""
return markdown_to_adf(text)
def unescape_text(text: str) -> str:
"""
Unescape common escape sequences that may appear as literal strings
in CLI arguments or generated content.
Converts \\n -> newline, \\t -> tab, \\\\ -> backslash, etc.
"""
if not text:
return text
result = text.replace("\\\\", "\x00ESCAPED_BACKSLASH\x00")
result = result.replace("\\n", "\n")
result = result.replace("\\t", "\t")
result = result.replace("\\r", "\r")
result = result.replace("\x00ESCAPED_BACKSLASH\x00", "\\")
return result
def _out(data: Any, fmt: str) -> None:
if fmt == "json":
print(json.dumps(data, ensure_ascii=False, indent=2))
else:
print(data)
def cmd_myself(base: str, email: str, token: str, fmt: str) -> int:
url = f"{base}/rest/api/3/myself"
r = requests.get(url, headers=session_headers(email, token, json_body=False), timeout=60)
if r.status_code != 200:
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
print(r.text[:800], file=sys.stderr)
return 1
j = r.json()
if fmt == "json":
_out(j, "json")
else:
print(f"accountId: {j.get('accountId')}")
print(f"displayName: {j.get('displayName')}")
print(f"email: {j.get('emailAddress')}")
return 0
def cmd_search(base: str, email: str, token: str, jql: str, limit: int, fmt: str) -> int:
url = f"{base}/rest/api/3/search/jql"
payload = {
"jql": jql,
"maxResults": limit,
"fields": ["key", "summary", "status", "assignee", "updated"],
}
r = requests.post(
url,
headers=session_headers(email, token),
json=payload,
timeout=60,
)
if r.status_code != 200:
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
print(r.text[:800], file=sys.stderr)
return 1
data = r.json()
issues = data.get("issues") or []
if fmt == "json":
_out(data, "json")
return 0
if not issues:
print("(no issues)")
return 0
for i, iss in enumerate(issues, 1):
f = iss.get("fields") or {}
st = (f.get("status") or {}).get("name") or "?"
summ = (f.get("summary") or "")[:80]
print(f"{i}. {iss.get('key')} [{summ}] status: {st}")
return 0
def cmd_issue(base: str, email: str, token: str, key: str, fmt: str) -> int:
url = f"{base}/rest/api/3/issue/{key}"
params = {"fields": "summary,status,assignee,project,description,created,updated"}
r = requests.get(
url,
headers=session_headers(email, token, json_body=False),
params=params,
timeout=60,
)
if r.status_code != 200:
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
print(r.text[:800], file=sys.stderr)
return 1
j = r.json()
if fmt == "json":
_out(j, "json")
return 0
f = j.get("fields") or {}
print(f"key: {j.get('key')}")
print(f"summary: {f.get('summary')}")
print(f"status: {(f.get('status') or {}).get('name')}")
proj = f.get("project") or {}
print(f"project: {proj.get('key')} {proj.get('name')}")
asn = f.get("assignee")
print(f"assignee: {(asn or {}).get('displayName') or asn}")
return 0
def cmd_create(
base: str,
email: str,
token: str,
project: str,
summary: str,
description: str,
issuetype: str,
assign_self: bool,
fmt: str,
) -> int:
fields: Dict[str, Any] = {
"project": {"key": project},
"summary": summary.strip(),
"description": plain_to_adf(description),
"issuetype": {"name": issuetype},
}
if assign_self:
mr = requests.get(
f"{base}/rest/api/3/myself",
headers=session_headers(email, token, json_body=False),
timeout=30,
)
if mr.status_code == 200:
aid = mr.json().get("accountId")
if aid:
fields["assignee"] = {"accountId": aid}
payload = {"fields": fields}
url = f"{base}/rest/api/3/issue"
r = requests.post(url, headers=session_headers(email, token), json=payload, timeout=60)
if r.status_code not in (200, 201):
# Retry without assignee if permission error
if r.status_code == 400 and assign_self and "assignee" in fields:
del fields["assignee"]
r2 = requests.post(
url, headers=session_headers(email, token), json={"fields": fields}, timeout=60
)
if r2.status_code in (200, 201):
key = r2.json().get("key")
print(f"WARN: created without assignee (API rejected assignee). key={key}", file=sys.stderr)
if fmt == "json":
_out(r2.json(), "json")
else:
print(key)
return 0
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
print(r.text[:1200], file=sys.stderr)
return 1
j = r.json()
if fmt == "json":
_out(j, "json")
else:
print(j.get("key", j))
return 0
def cmd_comment(base: str, email: str, token: str, issue: str, body: str, fmt: str) -> int:
url = f"{base}/rest/api/3/issue/{issue.strip()}/comment"
payload = {"body": plain_to_adf(body)}
r = requests.post(url, headers=session_headers(email, token), json=payload, timeout=60)
if r.status_code not in (200, 201):
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
print(r.text[:800], file=sys.stderr)
return 1
j = r.json()
if fmt == "json":
_out(j, "json")
else:
print(f"OK comment id={j.get('id')}")
return 0
def read_body(description: Optional[str], path: Optional[str]) -> str:
if path:
p = Path(path)
if not p.is_file():
raise FileNotFoundError(f"File not found: {p}")
return unescape_text(p.read_text(encoding="utf-8"))
return unescape_text(description or "")
def main() -> int:
parser = argparse.ArgumentParser(description="Jira REST CLI (same token as upload_attachment)")
parser.add_argument(
"--config",
default=None,
help=f"path to {CONFIG_FILENAME} (default: cwd / script dir / skill dir)",
)
parser.add_argument("--format", choices=("text", "json"), default="text", dest="fmt")
sub = parser.add_subparsers(dest="cmd", required=True)
p_my = sub.add_parser("myself", help="GET /myself (accountId, email)")
p_my.set_defaults(func="myself")
p_se = sub.add_parser("search", help="JQL search")
p_se.add_argument("--jql", default=DEFAULT_JQL, help="JQL (default: G3SF in-progress for current user)")
p_se.add_argument("--limit", type=int, default=5)
p_se.set_defaults(func="search")
p_is = sub.add_parser("issue", help="Get issue by key")
p_is.add_argument("key", help="e.g. G3SF-123")
p_is.set_defaults(func="issue")
p_cr = sub.add_parser("create", help="Create issue (Task)")
p_cr.add_argument("--project", default="G3SF")
p_cr.add_argument("--summary", required=True)
p_cr.add_argument(
"--description",
default="",
help="Markdown → ADF (headings, lists, bold, code, links)",
)
p_cr.add_argument("--description-file", dest="description_file", default=None)
p_cr.add_argument("--issuetype", default="Task", dest="issuetype")
p_cr.add_argument(
"--no-assign-self",
action="store_true",
help="do not set assignee to API token user",
)
p_cr.set_defaults(func="create")
p_co = sub.add_parser("comment", help="Add comment to issue")
p_co.add_argument("--issue", required=True)
p_co.add_argument("--body", default="")
p_co.add_argument("--body-file", dest="body_file", default=None)
p_co.set_defaults(func="comment")
args = parser.parse_args()
script_dir = Path(__file__).resolve().parent
try:
cfg = load_config(args.config, script_dir)
base, email, token = require_credentials(cfg)
except ValueError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
except FileNotFoundError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
fmt = args.fmt
if args.func == "myself":
return cmd_myself(base, email, token, fmt)
if args.func == "search":
return cmd_search(base, email, token, args.jql, args.limit, fmt)
if args.func == "issue":
return cmd_issue(base, email, token, args.key.strip(), fmt)
if args.func == "create":
try:
desc = read_body(args.description, args.description_file)
except FileNotFoundError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
return cmd_create(
base,
email,
token,
args.project,
args.summary,
desc,
args.issuetype,
assign_self=not args.no_assign_self,
fmt=fmt,
)
if args.func == "comment":
try:
body = read_body(args.body, args.body_file)
except FileNotFoundError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
if not body.strip():
print("ERROR: --body or --body-file required", file=sys.stderr)
return 1
return cmd_comment(base, email, token, args.issue, body, fmt)
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Shared Jira config loading for g3fo-commit-jira scripts.
Uses the same jira_upload.env / env vars as upload_attachment.py.
"""
import base64
import os
from pathlib import Path
from typing import Dict, Optional
CONFIG_FILENAME = "jira_upload.env"
def find_config_file(explicit_path: Optional[str], script_dir: Path) -> Optional[Path]:
if explicit_path:
p = Path(explicit_path)
return p if p.exists() and p.is_file() else None
cwd_file = Path.cwd() / CONFIG_FILENAME
if cwd_file.exists():
return cwd_file
script_dir_file = script_dir / CONFIG_FILENAME
if script_dir_file.exists():
return script_dir_file
parent_skill = script_dir.parent / CONFIG_FILENAME
if parent_skill.exists():
return parent_skill
return None
def load_config(config_path: Optional[str] = None, script_dir: Optional[Path] = None) -> Dict[str, str]:
"""Load KEY=VALUE from jira_upload.env. Env vars override file values."""
if script_dir is None:
script_dir = Path(__file__).resolve().parent
path = find_config_file(config_path, script_dir)
out: Dict[str, str] = {}
if path:
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, _, v = line.partition("=")
out[k.strip()] = v.strip().strip('"').strip("'")
for key in ("JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"):
if key in os.environ:
out[key] = os.environ[key]
return out
def require_credentials(cfg: Dict[str, str]) -> tuple:
base_url = (cfg.get("JIRA_BASE_URL") or "https://n2nafe.atlassian.net").rstrip("/")
email = cfg.get("JIRA_EMAIL")
token = cfg.get("JIRA_API_TOKEN")
if not email or not token:
raise ValueError(
"Missing JIRA_EMAIL or JIRA_API_TOKEN. "
f"Please configure {CONFIG_FILENAME} file first. "
f"You can apply for API token at: https://id.atlassian.com/manage-profile/security/api-tokens"
)
return base_url, email, token
def auth_header(email: str, token: str) -> str:
return base64.b64encode(f"{email}:{token}".encode()).decode()
def session_headers(email: str, token: str, json_body: bool = True) -> Dict[str, str]:
h = {
"Authorization": f"Basic {auth_header(email, token)}",
"Accept": "application/json",
}
if json_body:
h["Content-Type"] = "application/json"
return h
@@ -2,18 +2,15 @@
"""
Upload one or more files as attachments to a Jira Cloud issue via REST API v3.
Config: from jira_upload.env (current dir or script dir) or env vars.
Config: from jira_upload.env (current dir, script dir, or skill parent dir) or env vars.
Usage:
python upload_attachment.py --issue ISSUE_KEY --file path1 [--file path2 ...]
python upload_attachment.py --config /path/to/jira_upload.env --issue KEY --file path1
"""
import argparse
import base64
import os
import sys
from pathlib import Path
from typing import Dict, Optional
try:
import requests
@@ -21,57 +18,34 @@ except ImportError:
print("ERROR: 'requests' is required. Run: pip install requests", file=sys.stderr)
sys.exit(1)
CONFIG_FILENAME = "jira_upload.env"
from jira_env import CONFIG_FILENAME, auth_header, load_config, require_credentials
def _find_config_file(explicit_path: Optional[str], script_dir: Path) -> Optional[Path]:
if explicit_path:
p = Path(explicit_path)
return p if p.exists() and p.is_file() else None
cwd_file = Path.cwd() / CONFIG_FILENAME
if cwd_file.exists():
return cwd_file
script_dir_file = script_dir / CONFIG_FILENAME
if script_dir_file.exists():
return script_dir_file
return None
def load_config(config_path: Optional[str]) -> Dict[str, str]:
"""Load KEY=VALUE from jira_upload.env. Env vars override file values."""
script_dir = Path(__file__).resolve().parent
path = _find_config_file(config_path, script_dir)
out = {}
if path:
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, _, v = line.partition("=")
out[k.strip()] = v.strip().strip('"').strip("'")
for key in ("JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"):
if key in os.environ:
out[key] = os.environ[key]
return out
SCRIPT_DIR = Path(__file__).resolve().parent
def main() -> int:
parser = argparse.ArgumentParser(description="Upload files as attachments to a Jira issue")
parser.add_argument("--issue", required=True, help="Jira issue key (e.g. G3SF-123)")
parser.add_argument("--file", action="append", required=True, dest="files", help="Path to file to upload (can be repeated)")
parser.add_argument("--config", default=None, help=f"Path to config file (default: {CONFIG_FILENAME} in cwd or script dir)")
parser.add_argument(
"--file",
action="append",
required=True,
dest="files",
help="Path to file to upload (can be repeated)",
)
parser.add_argument(
"--config",
default=None,
help=f"Path to config file (default: {CONFIG_FILENAME} in cwd, script dir, or skill dir)",
)
args = parser.parse_args()
cfg = load_config(args.config)
base_url = (cfg.get("JIRA_BASE_URL") or "").rstrip("/")
email = cfg.get("JIRA_EMAIL")
token = cfg.get("JIRA_API_TOKEN")
if not base_url or not email or not token:
print("ERROR: Set JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN in config file or environment.", file=sys.stderr)
print(f" Config file: {CONFIG_FILENAME} (in current dir or script dir), or use --config PATH", file=sys.stderr)
try:
cfg = load_config(args.config, SCRIPT_DIR)
base_url, email, token = require_credentials(cfg)
except ValueError as e:
print(f"ERROR: {e}", file=sys.stderr)
print(f" Use --config PATH or create {CONFIG_FILENAME}", file=sys.stderr)
return 1
issue_key = args.issue.strip()
@@ -87,9 +61,8 @@ def main() -> int:
files_to_upload.append(path)
url = f"{base_url}/rest/api/3/issue/{issue_key}/attachments"
auth_str = base64.b64encode(f"{email}:{token}".encode()).decode()
headers = {
"Authorization": f"Basic {auth_str}",
"Authorization": f"Basic {auth_header(email, token)}",
"X-Atlassian-Token": "no-check",
}
+11
View File
@@ -14,6 +14,7 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
- **分析测试流程**:参考 `references/business_flows/_TEST_FLOW_TEMPLATE.md`。
- **查标准运维流程**:去 `references/middleware/` 或 `references/system-init/`。
- **查中间件总览/高可用架构**:查阅 `references/middleware/middleware-comprehensive-guide.md`。
- **查中间件与标准组件版本**:查阅 `references/middleware/middleware-versions.md`(含 `g3fo-*-service` 应用版本、中间件与 JDK;**Web Server** 仅 Nginx / Vue / Flutter,其余为 **AP Server**;用户要「所有版本」时须分两类列出,见 §2.D)。
- **查具体环境/客户资产**:
- 内部环境(Dev/UAT):查阅 `references/inventory/internal.md`。
- 外部客户(客户A、B等):查阅 `references/inventory/clients/[客户名].md`。
@@ -61,6 +62,13 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
4. **表格生成**:汇总信息,输出包含“步骤”、“涉及服务”、“关键接口/代码逻辑”、“测试内容/预期结果”的表格。
- **格式参考**:`references/business_flows/_TEST_FLOW_TEMPLATE.md`。
### D. 版本清单问答
1. **文档来源**:以 `references/middleware/middleware-versions.md` 为唯一标准版本表(除非用户指定环境资产清单覆盖)。
2. **角色区分**:
- **Web Server**:仅 **Nginx、Vue、Flutter**。
- **AP Server**:全部 **`g3fo-*-service`**、**全部中间件**(MySQL、Redis、RocketMQ、Nacos、PowerJob、Beszel、Dozzle、dufs 等)、**OpenJDK**。
3. **用户问「所有版本 / 完整版本 / 各组件版本号」等**:必须分两块输出,且标题或小节名明确为 **Web Server** 与 **AP Server**(可先 Web 后 AP,或先 AP 后 Web,但两类不可混为一张无标签表)。
## 3. 示例 Prompt (用户可参考)
- **业务咨询**:
- “我想测试下单业务流程,请分析代码并列出详细的测试步骤表格。”
@@ -71,6 +79,8 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
- “请根据 `references/middleware/middleware-comprehensive-guide.md` 和 `references/middleware/nacos/deploy.md`,结合客户资产清单生成 Nacos 部署配置。”
- **故障排查**:
- “我的 MySQL 出现了复制冲突,报错 `Duplicate entry`,请根据 `references/middleware/mysql/fault-analysis.md` 提供排查脚本 and 修复建议。”
- **版本清单**:
- “请根据 `middleware-versions.md` 列出当前约定的全部组件版本,并按 Web Server / AP Server 分开。”
## 4. 资源地图
- **业务与服务**:
@@ -79,6 +89,7 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
- **架构概览**: `architecture/domain_overview.md`
- **中间件标准**(部署/故障分析等详见各子目录):
- **总览**:`middleware/middleware-comprehensive-guide.md`
- **版本清单**:`middleware/middleware-versions.md`(Web:Nginx/Vue/Flutter;AP:`g3fo-*-service` + 中间件 + JDK)
- **Keepalived**: `middleware/keepalived/`
- **MySQL**: `middleware/mysql/`
- **Nacos**: `middleware/nacos/`
@@ -0,0 +1,74 @@
# 中间件、应用服务与标准组件版本清单
本文档列出 g3fo 相关环境中**约定使用的镜像、发行或应用版本**,便于部署对齐、升级评估与问题排查。若与某客户现场或分支实际镜像不一致,以该环境资产清单或运行中的 `docker images` / 构建配置为准。
## 角色划分:AP Server 与 Web Server
| 角色 | 范围 | 说明 |
| ---- | ---- | ---- |
| **AP Server** | 全部 `g3fo-*-service` 业务应用、**全部中间件**(MySQL、Redis、RocketMQ、Nacos、PowerJob、观测与辅助组件等)、Java 运行时(OpenJDK) | 后端与基础设施侧;**不**含 Nginx / Vue / Flutter。 |
| **Web Server** | 仅 **Nginx**、**Vue**、**Flutter** | 反向代理 / 静态与前端构建链;与 AP 侧分开罗列。 |
**AI / 读者输出约定**:当用户询问「所有版本」「完整版本清单」「环境上各组件版本」等时,须**分两节**回答:**先列 Web Server**,**再列 AP Server**(或按用户要求的顺序,但必须明确标注两类,不可混成一张不分角色的总表)。
---
## Web Server(仅下列三项)
| 组件 | 版本 / 标识 | 说明 |
| ---- | ------------- | ---- |
| Nginx | `1.27.3` | 反向代理 / 网关前置 |
| Vue | `3.5.17` | 前端框架(与构建依赖对齐) |
| Flutter | `3.38.4` | 移动端 / 跨端构建工具链 |
---
## AP Server
### 业务应用(`g3fo-*-service`)
以下应用版本统一为 **`1.5.1.22`**。
| 服务 | 版本 |
| ---- | ---- |
| g3fo-trade-service | `1.5.1.22` |
| g3fo-product-service | `1.5.1.22` |
| g3fo-base-service | `1.5.1.22` |
| g3fo-admin-service | `1.5.1.22` |
| g3fo-monitor-service | `1.5.1.22` |
| g3fo-dx-service | `1.5.1.22` |
| g3fo-user-service | `1.5.1.22` |
| g3fo-margin-service | `1.5.1.22` |
| g3fo-notification-service | `1.5.1.22` |
| g3fo-exchange-fix-engine-service | `1.5.1.22` |
| g3fo-gateway-service | `1.5.1.22` |
| g3fo-push-service | `1.5.1.22` |
| g3fo-utility-service | `1.5.1.22` |
### 中间件与运行时(同属 AP Server 侧)
| 组件 | 版本 / 镜像标识 | 说明 |
| ---- | ---------------- | ---- |
| MySQL | `8.4.7` | 关系型数据库 |
| Redis | `8.4.0` | 缓存与会话等 |
| RocketMQ | `5.3.2` | 消息队列 |
| RocketMQ Dashboard | `2.1` | RocketMQ 管理控制台 |
| Nacos Server | `v3.1.1` | 注册与配置中心 |
| PowerJob Server | `v5.1.2` | 分布式任务调度 |
| Beszel | `v0.18.4` | 轻量监控相关(容器化部署时按此标签) |
| Dozzle | `v9.0.3` | 容器日志查看 |
| dufs | `v0.43.0` | 文件服务(静态/上传等场景) |
| OpenJDK | `amazoncorretto:17-al2023` | Java 运行时(Amazon Corretto 17,AL2023 基础镜像) |
---
## 使用说明
- **中间件**:部署、Compose 或 K8s 清单中的镜像 tag 建议与本表一致,避免隐式 `latest`。
- **OpenJDK**:表内为 Docker 镜像习惯写法;本地非容器环境需安装同主版本 JDK 17 并与团队规范一致。
- **Vue / Flutter**:仅归入 **Web Server**;升级时需同步 CI、锁文件与团队文档。
## 相关文档
- 中间件高可用与部署总览:`middleware-comprehensive-guide.md`
- 各组件细则:同目录下 `mysql/`、`redis/`、`rocketmq/`、`nacos/`、`powerjob/` 等子目录