Selaa lähdekoodia

feat(document-skills): integrate ProcessOn diagram generator skill

Add processon-diagram-generator from processonai/processon-skills and publish it through document-skills.

Co-authored-by: Cursor <cursoragent@cursor.com>
wandl-6A72h 3 kuukautta sitten
vanhempi
sitoutus
359fd79401

+ 2 - 1
.claude-plugin/marketplace.json

@@ -311,7 +311,7 @@
     },
     {
       "name": "document-skills",
-      "description": "文档与图表:docx、pptx、pdf、xlsx、文档协作、API 文档、全栈产品文档标准(full-stack-doc)、ProcessOn 思维导图、Mermaid、PlantUML(10个技能,与 skill-group-mapping 一致)。",
+      "description": "文档与图表:docx、pptx、pdf、xlsx、文档协作、API 文档、全栈产品文档标准(full-stack-doc)、ProcessOn 思维导图、ProcessOn 图表生成、Mermaid、PlantUML(11个技能,与 skill-group-mapping 一致)。",
       "source": "./",
       "strict": false,
       "skills": [
@@ -323,6 +323,7 @@
         "./skills/document-skills/api-doc-generator",
         "./skills/document-skills/full-stack-doc",
         "./skills/document-skills/processon-mindmap",
+        "./skills/document-skills/processon-diagram-generator",
         "./skills/document-skills/mermaid",
         "./skills/document-skills/plantuml"
       ]

+ 1 - 0
PLANNING_SKILL_CATEGORIES.md

@@ -210,6 +210,7 @@ Marketplace (full-stack-skills)
 - `mermaid` - Mermaid 图表绘制(已存在)
 - `drawio-flowchart` - Draw.io 流程图绘制指南
 - `processon-mindmap` - ProcessOn 思维导图指南
+- `processon-diagram-generator` - ProcessOn 智能图表生成(流程图、架构图、时序图等)
 
 ---
 

+ 9 - 9
README.md

@@ -6,7 +6,7 @@
 
 ![Version](https://img.shields.io/badge/Version-0.0.1-red)
 ![License](https://img.shields.io/badge/License-Apache%202.0-green)
-![Skill Docs](https://img.shields.io/badge/SKILL.md-421-orange)
+![Skill Docs](https://img.shields.io/badge/SKILL.md-422-orange)
 ![Skill Groups](https://img.shields.io/badge/Groups-42-blue)
 ![Marketplace Plugins](https://img.shields.io/badge/Plugins-40-brightgreen)
 
@@ -18,7 +18,7 @@
 
 > **⚠️ 注意:** 当前文档口径以仓库内 `skills/` 实际目录和 `.claude-plugin/marketplace.json` 为准。
 
-> **说明:** 当前仓库内共有 **42 个技能组目录**、**421 个 `SKILL.md` 文件**;其中 Marketplace 当前发布 **40 个插件**、共 **410 条技能路径**。未进入 Marketplace 的仓库内技能组为 `threejs-skills` 与 `vscode-skills`。
+> **说明:** 当前仓库内共有 **42 个技能组目录**、**422 个 `SKILL.md` 文件**;其中 Marketplace 当前发布 **40 个插件**、共 **411 条技能路径**。未进入 Marketplace 的仓库内技能组为 `threejs-skills` 与 `vscode-skills`。
 
 ## ⚡ 5 分钟安装与转换示例
 
@@ -127,9 +127,9 @@ Skills 是由说明、脚本和资源组成的文件夹,Claude 会按需动态
 | 指标 | 当前值 |
 |---|---:|
 | `skills/` 下技能组目录 | 42 |
-| 仓库内 `SKILL.md` 文件 | 421 |
+| 仓库内 `SKILL.md` 文件 | 422 |
 | Marketplace 插件数 | 40 |
-| Marketplace 技能路径数 | 410 |
+| Marketplace 技能路径数 | 411 |
 | 仓库内未发布技能组 | 2 |
 
 ### ✨ 核心特性
@@ -137,7 +137,7 @@ Skills 是由说明、脚本和资源组成的文件夹,Claude 会按需动态
 #### 1. 分组式组织
 - **42 个技能组目录**:按领域、框架和工作流拆分,便于维护与扩展
 - **40 个 Marketplace 插件**:按技能组发布,便于按需安装与组合使用
-- **421 个 `SKILL.md` 文件**:沉淀可复用的 Agent Skills 资产
+- **422 个 `SKILL.md` 文件**:沉淀可复用的 Agent Skills 资产
 
 #### 2. 全链路覆盖
 - **前端与 UI**:Vue、React、Angular、Svelte、各类 UI 组件库与构建工具
@@ -176,7 +176,7 @@ Skills 是由说明、脚本和资源组成的文件夹,Claude 会按需动态
 
 - `threejs-skills` 当前在仓库中包含 18 个技能,但未进入 Marketplace 发布面。
 - `vscode-skills` 当前在仓库中包含 4 个技能,但未进入 Marketplace 发布面。
-- `document-skills` 当前目录中有 6 个技能(含 `full-stack-doc`),而 Marketplace 仍引用 10 个条目,其中 `docx`、`pptx`、`pdf`、`xlsx` 为缺失目录引用。
+- `document-skills` 当前目录中有 7 个技能(含 `full-stack-doc`、`processon-diagram-generator`),而 Marketplace 仍引用 11 个条目,其中 `docx`、`pptx`、`pdf`、`xlsx` 为缺失目录引用。
 - `tauri-skills` 当前目录中有 52 个技能,而 Marketplace 当前发布 51 个条目;`tauri-app-updater` 目前仅存在于仓库目录中。
 
 ### 阅读路径建议
@@ -262,9 +262,9 @@ full-stack-skills/
 | 项目 | 当前值 |
 |---|---:|
 | 技能组目录 | 42 |
-| `SKILL.md` 文件 | 421 |
+| `SKILL.md` 文件 | 422 |
 | Marketplace 插件 | 40 |
-| Marketplace 技能路径 | 410 |
+| Marketplace 技能路径 | 411 |
 | 仓库内未发布技能组 | 2 |
 
 ### 免责声明
@@ -478,7 +478,7 @@ fskill install
 | `dev-utils-skills` | 12 | 是 | 12 | 已与当前发布面对应;`full-stack-doc` 已迁至 `document-skills` |
 | `devops-skills` | 6 | 是 | 6 | 已与当前发布面对应 |
 | `docker-skills` | 2 | 是 | 2 | 已与当前发布面对应 |
-| `document-skills` | 6 | 是 | 10 | 目录技能数与 Marketplace 引用数不一致(含缺失的 docx/pptx/pdf/xlsx),详见 repository-map |
+| `document-skills` | 7 | 是 | 11 | 目录技能数与 Marketplace 引用数不一致(含缺失的 docx/pptx/pdf/xlsx),详见 repository-map |
 | `drawio-skills` | 2 | 是 | 2 | 已与当前发布面对应 |
 | `electron-skills` | 3 | 是 | 3 | 已与当前发布面对应 |
 | `flutter-skills` | 2 | 是 | 2 | 已与当前发布面对应 |

+ 4 - 4
README_EN.md

@@ -4,7 +4,7 @@ A grouped Agent Skills repository for Claude Code, Claude.ai, and compatible Age
 
 ![Version](https://img.shields.io/badge/Version-0.0.1-red)
 ![License](https://img.shields.io/badge/License-Apache%202.0-green)
-![Skill%20Docs](https://img.shields.io/badge/SKILL.md-421-orange)
+![Skill%20Docs](https://img.shields.io/badge/SKILL.md-422-orange)
 ![Skill%20Groups](https://img.shields.io/badge/Groups-42-blue)
 ![Marketplace%20Plugins](https://img.shields.io/badge/Plugins-40-brightgreen)
 
@@ -99,7 +99,7 @@ Skills are folders of instructions, scripts, and resources that Claude loads dyn
 
 ## About This Repository
 
-This repository is PartMeAI’s grouped Agent Skills catalog. The current repository snapshot contains **42** skill-group directories and **421** `SKILL.md` files. `.claude-plugin/marketplace.json` currently publishes **40** plugins with **410** skill paths.
+This repository is PartMeAI’s grouped Agent Skills catalog. The current repository snapshot contains **42** skill-group directories and **422** `SKILL.md` files. `.claude-plugin/marketplace.json` currently publishes **40** plugins with **411** skill paths.
 
 The main repo now covers frontend, backend, mobile, cross-platform development, testing, DevOps, documentation, OCR, Tauri, Stitch, Pencil, T2UI, OpenSpec, and Spec Kit workflows.
 
@@ -108,9 +108,9 @@ The main repo now covers frontend, backend, mobile, cross-platform development,
 | Metric | Current Value |
 |---|---:|
 | Skill-group directories under `skills/` | 42 |
-| `SKILL.md` files on disk | 421 |
+| `SKILL.md` files on disk | 422 |
 | Plugins in `.claude-plugin/marketplace.json` | 40 |
-| Published skill refs in Marketplace | 410 |
+| Published skill refs in Marketplace | 411 |
 | Repo-only groups not published | 2 |
 
 ### Coverage Map

+ 4 - 4
docs/repository-map.md

@@ -5,8 +5,8 @@
 ## 一、当前仓库概况
 
 - `skills/` 下共有 **42** 个技能组目录。
-- 仓库内共检出 **421** 个 `SKILL.md` 文件。
-- Marketplace 当前配置了 **40** 个插件,合计 **410** 个技能路径。
+- 仓库内共检出 **422** 个 `SKILL.md` 文件。
+- Marketplace 当前配置了 **40** 个插件,合计 **411** 个技能路径。
 - 已发布插件组:angular-skills、antd-skills、ascii-skills、avue-skills、build-skills、chart-skills、cocos-skills、database-skills、ddd-skills、design-skills、dev-utils-skills、devops-skills、docker-skills、document-skills、drawio-skills、electron-skills、flutter-skills、go-skills、mobile-native-skills、nodejs-skills、nvm-skills、ocrmypdf-skills、openspec-skills、pencil-skills、python-skills、react-skills、social-skills、speckit-skills、spring-skills、stitch-skills、svelte-skills、t2ui-skills、tauri-skills、teaching-skills、testing-skills、uniapp-skills、utility-skills、uview-skills、vue-skills、vue-ui-skills。
 - 文档入口建议优先查看:`README.md`、`docs/skill-group-mapping.md`、`docs/pipeline-stage-to-skills.md`、本文件。
 
@@ -47,7 +47,7 @@ full-stack-skills/
 | `dev-utils-skills` | 12 | 12 | 已发布 | `full-stack-doc` 已迁至 `document-skills` |
 | `devops-skills` | 6 | 6 | 已发布 | — |
 | `docker-skills` | 2 | 2 | 已发布 | — |
-| `document-skills` | 6 | 10 | 已发布 | 含 `full-stack-doc`;仍缺目录:docx、pptx、pdf、xlsx |
+| `document-skills` | 7 | 11 | 已发布 | 含 `full-stack-doc`、`processon-diagram-generator`;仍缺目录:docx、pptx、pdf、xlsx |
 | `drawio-skills` | 2 | 2 | 已发布 | — |
 | `electron-skills` | 3 | 3 | 已发布 | — |
 | `flutter-skills` | 2 | 2 | 已发布 | — |
@@ -85,7 +85,7 @@ full-stack-skills/
 
 ## 五、当前一致性说明
 
-- Marketplace 当前声明 410 个技能路径,其中 406 个可在仓库中直接解析。
+- Marketplace 当前声明 411 个技能路径,其中 407 个可在仓库中直接解析。
 - `document-skills` 当前存在未落地目录引用:`docx`、`pptx`、`pdf`、`xlsx`。
 - `tauri-skills` 目录下已有技能尚未纳入 Marketplace:`tauri-app-updater`。
 

+ 1 - 1
docs/skill-group-mapping.md

@@ -32,7 +32,7 @@
 | **chart-skills** | lime-echart, ucharts | 2 | 图表组件 |
 | **dev-utils-skills** | code-generator, test-writer, java-code-comments, mybatis-plus-generator, documentation-builder, mcp-builder, webapp-testing, frontend-design, web-artifacts-builder, theme-factory, agent-browser, maven-search | 12 | 开发工具链(与 development-skills-utils 对应) |
 | **design-skills** | algorithmic-art, brand-guidelines, canvas-design, figma, figma-ai, sketch, adobe-xd, dalle, uizard, galileo-ai, runway-ml, framer | 12 | 设计与 AI 绘图(midjourney/stable-diffusion/axure/modao 待补目录则入本组) |
-| **document-skills** | docx, pptx, pdf, xlsx, doc-coauthoring, api-doc-generator, full-stack-doc, processon-mindmap, mermaid, plantuml | 10 | 文档与图表(与 document 插件对应) |
+| **document-skills** | docx, pptx, pdf, xlsx, doc-coauthoring, api-doc-generator, full-stack-doc, processon-mindmap, processon-diagram-generator, mermaid, plantuml | 11 | 文档与图表(与 document 插件对应) |
 | **testing-skills** | jest, vitest, pytest, junit, cypress, playwright, selenium, appium, detox | 9 | 测试框架与 E2E/移动端测试 |
 | **devops-skills** | gitlab-ci, github-actions, kubernetes, terraform, ansible, cloudformation | 6 | CI/CD 与 IaC(jenkins 待补目录则入本组) |
 | **database-skills** | postgresql, oracle, redis, elasticsearch, dbeaver | 5 | 数据库与工具(mysql/mongodb/navicat 待补目录则入本组) |

+ 1 - 0
skills/document-skills/README.md

@@ -12,5 +12,6 @@
 | [mermaid](mermaid/) | Mermaid 图表 |
 | [plantuml](plantuml/) | PlantUML |
 | [processon-mindmap](processon-mindmap/) | ProcessOn 思维导图 |
+| [processon-diagram-generator](processon-diagram-generator/) | ProcessOn 智能图表生成(流程图、架构图、时序图等) |
 
 安装:`/plugin install document-skills@full-stack-skills`

+ 109 - 0
skills/document-skills/processon-diagram-generator/README.md

@@ -0,0 +1,109 @@
+# processon-diagram-generator
+
+用 ProcessOn 生成专业、可继续编辑的图形,支持自然语言、代码上下文。
+
+## 安装与首要配置
+
+### 1. 安装技能
+
+**通过 full-stack-skills(推荐):**
+
+```bash
+/plugin install document-skills@full-stack-skills
+```
+
+或单独安装本技能:
+
+```bash
+npx skills add https://github.com/partme-ai/full-stack-skills.git --skill processon-diagram-generator
+```
+
+**通过 ProcessOn 官方仓库:**
+
+```bash
+npx skills add https://github.com/processonai/processon-skills.git --skill processon-diagram-generator
+```
+#### 更新技能
+```bash
+npx skills add https://github.com/processonai/processon-skills.git --skill processon-diagram-generator --force -g -y
+```
+
+### 2. 获取 API Key (核心步骤)
+本技能依赖 ProcessOn 智能绘图接口。请务必访问以下地址获取你的 `PROCESSON_API_KEY`:
+
+**👉 [获取 API Key/Token](https://smart.processon.com/user)**
+
+### 3. 设置环境变量
+将获取到的 Key 设置为系统环境变量:
+
+> **重要提示**:请务必在**当前运行 Agent 的终端**中设置环境变量。如果你使用的是 IDE 内置终端,设置后可能需要重启 IDE 或重启 Agent 宿主进程以使配置生效。
+
+**macOS / Linux:**
+```bash
+export PROCESSON_API_KEY="你的-API-Key"
+```
+
+**Windows (PowerShell):**
+```powershell
+$env:PROCESSON_API_KEY="你的-API-Key"
+```
+
+**Windows (CMD):**
+```cmd
+set PROCESSON_API_KEY=你的-API-Key
+```
+
+## 支持的图形类型
+
+- 流程图
+- 业务流程图
+- 泳道图
+- 流程地图
+- 标准流程图
+- 时序图
+- 软件架构图
+- 系统架构图
+- 云架构图
+- ER 图
+- 组织结构图
+- 时间轴
+- 信息图
+- 金字塔图
+
+
+## 提示词示例
+
+```text
+生成一个登录注册流程图,要求布局清晰,适合产品和研发沟通
+```
+
+```text
+生成一个订单创建、支付、库存扣减的时序图
+```
+
+```text
+分析这个项目的模块关系并生成系统架构图,不要画目录树
+```
+
+```text
+把这张手绘草图重绘成流程图
+```
+
+## 输出
+
+- 图片链接。
+- 图形的dsl
+
+dsl编辑链接:
+
+```text
+https://smart.processon.com/editor
+```
+
+## 本地开发
+
+如果你想在本地迭代这个 skill,而不是从 GitHub 安装,可以把 skill 目录放到本地 skills 目录,例如:
+
+```text
+~/.agents/skills/processon-diagram-generator
+```

+ 156 - 0
skills/document-skills/processon-diagram-generator/SKILL.md

@@ -0,0 +1,156 @@
+---
+name: processon-diagram-generator
+description: |
+  A fast diagramming tool developed by ProcessOn that allows you to quickly create beautiful, editable visual diagrams such as flowcharts, swimlane diagrams, sequence diagrams, architecture diagrams, ER diagrams, organizational charts, timelines, and infographics. It supports direct rendering of Mermaid data.
+  ProcessOn官方研发的一键生成精美可编辑的流程图、泳道图、时序图 、架构图、ER图、组织结构图、时间轴、信息图等可视化图表的快速绘制图表工具,支持mermaid数据直接绘制。
+  Generate, edit, and visualize professional ProcessOn diagrams from natural language descriptions. Use this tool when users request to "create a diagram", "make a flowchart", "visualize a process", or "draw a system architecture". It comprehensively supports generating process flowcharts (including swimlane and process maps for business workflows), sequence diagrams (for system interactions and API call orders), software architecture diagrams (for cloud architecture and system composition), ER diagrams (for database modeling), org charts (for team structures), timelines, and infographics. This AI diagram generator and flowchart maker is designed for developers, managers, and designers to achieve publication-ready visual outputs efficiently.
+  一键将自然语言转化为精美且可编辑的 ProcessOn 专业可视化图表。当你需要“自动画图”、“在线制作流程图”、“可视化业务流程”或“生成系统架构”时即可完美触发。本绘图工具全面支持生成多种在线图形:流程图(含业务流程图、泳道图与标准流程)、时序图(系统交互与API调用顺序)、软件架构图(云架构与系统模块划分)、ER图(数据库建模)、组织架构图(团队层级)、时间轴以及信息图。这款 AI 智能图表生成器与流程图制作软件,专为开发者、产品经理和设计师打造,助你高效产出专业级出图。
+
+author: ProcessOn
+version: 2.4.0
+dependencies:
+  bins:
+    - python3
+---
+
+# processon-diagram-generator
+
+将用户意图、代码关系或草图转换为专业图形。默认跟随用户当前语言输出提示、澄清问题、优化 Prompt 和最终结果。
+
+## ⚠️ 每次使用前必须完成(缺一不可)
+
+- [ ] **检查云端版本**:立即调用 `run_shell_command` 执行极速预检(限时 5 秒):`curl -s --connect-timeout 3 -m 5 https://raw.githubusercontent.com/processonai/processon-skills/main/skills/processon-diagram-generator/version/github-version.json`。
+- [ ] **容错机制**:如果 `curl` 报错、超时或返回非 JSON 内容,**严禁提示网络错误**,应直接视为“暂无更新”,并立即继续后续绘图流程。
+- [ ] **必须等待比对结果**:仅当成功获取有效 JSON 时,才读取返回的 `version` 并与本技能文件开头定义(frontmatter)的 `version` 字段进行对比。
+- [ ] **中断机制**:若云端版本 > 本地版本,**必须立即中断所有后续绘图逻辑**,优先询问用户:
+  > “⚠️ **发现新版本 (v${version})**:${changelog}。建议立即更新以获得更好的生成效果。你可以选择:
+  > - **一键更新**:输入‘更新’来执行:`npx skills add https://github.com/processonai/processon-skills.git --skill processon-diagram-generator --force -g -y`
+  > - **手动下载**:前往 https://github.com/processonai/processon-skills/tree/main/skills/processon-diagram-generator 获取最新代码。”
+
+## 何时触发
+
+- 支持:流程图、业务流程图、泳道图、流程地图、标准流程图、时序图、软件架构图、系统架构图、云架构图、ER 图、组织结构图、时间轴、信息图、金字塔图、草图重绘。
+- 英文表达同样触发,例如:`create a diagram`、`draw a diagram`、`generate a flowchart`、`make a flowchart`、`visualize a process`、`create a sequence diagram`、`draw a system architecture`、`software architecture diagram`、`cloud architecture diagram`、`ER diagram`、`org chart`、`timeline`、`infographic`、`redraw this sketch`、`ProcessOn`。
+- 如果用户只说“画个图”之类的模糊请求,先确认图形类型。
+- 如果用户上传图片并要求“重绘”或“转成图”,先识别图片中的节点、文字和连接关系,再生成结构化输入。
+
+## 工作方式
+
+### 0. 安装后首次引导与状态检查
+当用户询问如何使用本技能、技能是否安装成功,或直接询问“怎么配置”时,你应该:
+1. **主动检查环境**:告知用户你已准备好提供 ProcessOn 智能绘图服务,但前提是需要配置 `PROCESSON_API_KEY`。
+2. **提供指引**:明确告知用户前往 `https://smart.processon.com/user` 获取 Key,并根据其操作系统提供 `export PROCESSON_API_KEY="..."` 的设置命令。
+3. **特别提醒**:务必提醒用户该环境变量必须在**当前运行 Agent 的终端**中设置,否则 Agent 无法读取到配置。
+
+### 1. 先补关键信息
+
+不要在关系不清、流程断层或结构缺失时直接生成。
+
+信息不足时按这个顺序处理:
+1. 指出缺少什么。
+2. 给出合理默认方案或行业常见做法供用户确认。
+3. 用户确认后再继续。
+
+### 2. 优化 Prompt,但不要改写用户语言
+
+在用户原始需求上补充专业约束:
+
+- 通用:专业风格、布局清晰、颜色协调、避免线条交叉。
+- 流程图:明确开始/结束节点,决策点用标准菱形。
+- 时序图:参与者清晰,调用消息明确。
+- 架构图:按层次组织,标注关键通信关系。
+- ER 图:标注主外键关系和字段类型。
+
+优化后的 Prompt 默认保持与用户一致的语言。
+
+### 3. 架构分析画关系,不画目录树
+
+当用户要求分析项目架构时,重点提取模块边界、依赖关系、调用链路和数据流向。优先阅读入口文件、路由、核心配置和关键模块,不要把结果退化成文件夹树。
+
+## 执行顺序
+
+1. 先在聊天里说明正在使用 `processon-diagram-generator` 技能处理当前请求。
+2. 识别图形类型,提取关键实体、动作、判断条件,构建优化后的 Prompt。
+3. 生成依赖 `PROCESSON_API_KEY`。如果缺失,明确告诉用户如何配置,并附上获取地址 `https://smart.processon.com/user`。**必须强调在当前运行 Agent 的终端设置环境变量。**
+4. **单脚本全流程自动执行逻辑:**
+   - 调用 `scripts/processon_api_client.py` 启动生成任务。
+   - 脚本会自动流式生成 DSL,并在完成后立即自动触发图片渲染(除非指定 `--no-render`)。
+   - **立即展示中间结果:** 第一阶段 DSL 生成完成后,**必须立即**在回复中完整展示生成的 DSL 和编辑链接 `https://smart.processon.com/editor`。
+   - **自动输出渲染结果:** 脚本渲染完成后,会自动输出图片原始链接。你必须确保这些内容都呈现在最终回复中。
+   - **硬性闸门:** 只有在 DSL、编辑链接、“可复制粘贴渲染和二次编辑”提示语、以及(如果成功)图片链接全部输出完毕后,才允许结束当前任务。
+5. **禁止使用富文本语法:** 在任何阶段,严禁输出 `<img>` 等 HTML 标签,也**严禁使用 Markdown 图片语法 `![]()` 或链接语法 `[]()`**。所有图片链接和编辑链接必须以**原始 URL 纯文本**形式直接展示。
+6. **结果呈现:**
+   - 如果图片生成成功,最终回复中**必须同时保留**完整 DSL、编辑链接和图片原始链接。
+   - 如果图片生成失败,告知用户原因,并引导用户使用已展示的 DSL 去编辑链接手动渲染。
+
+## 结果呈现
+
+关键结果必须在 assistant 正文里以纯文本形式可见。
+
+- **DSL 展示规范:** 优先使用代码块展示 DSL,并紧跟原始编辑链接。
+- **链接展示规范:** 严禁使用任何 Markdown 或 HTML 包装。必须直接输出 `https://...` 这种可识别的原始 URL。并在链接旁明确说明:**“你可以复制上方 DSL 数据到此链接进行渲染和二次编辑”**。
+- **全自动流程:** 默认情况下,脚本会一次性完成从 DSL 生成到图片渲染的所有步骤,无需手动干预。
+- **最终回复保留规则:** 即使图片已经成功生成,最后一条回复也必须再次包含 DSL 代码块、编辑链接和图片链接,防止中间态在收尾时丢失。
+- **失败处理:** 如果渲染失败,不要删除或隐藏已经生成的 DSL。
+
+## 输出前自检
+
+在发送任何最终回复前,必须逐项自检,四项全部满足才允许发送:
+
+1. assistant 正文里已经完整贴出 DSL,且不是摘要或省略版。
+2. assistant 正文里已经贴出原始编辑链接 `https://smart.processon.com/editor`。
+3. assistant 正文里已经明确写出“你可以复制上方 DSL 数据到此链接进行渲染和二次编辑”这一层含义。
+4. 如果图片已生成,assistant 正文里同时包含图片原始链接;如果图片未生成,明确说明失败原因。
+
+只要以上任一项不满足,就不能结束当前回复。
+
+### 最终回复格式示例
+
+**第一阶段输出(中间态):**
+> 语义分析:用户需要一个...流程图。
+> 
+> **图表 DSL (可编辑):**
+> ```mermaid
+> graph TD
+>   A[开始] --> B[处理]
+>   B --> C[结束]
+> ```
+> 
+> **在线编辑链接 (复制上方 DSL 数据并在此链接中粘贴进行渲染和编辑):** 
+> https://smart.processon.com/editor
+> (提示:如果下方图片渲染失败,可手动将上述代码粘贴至此链接)
+
+**第二阶段输出(最终态):**
+> **图片预览链接:**
+> https://ai-smart.ks3-cn-beijing.ksyuncs.com/gallery/...png
+
+
+## 配置提示
+
+### API Key 配置
+
+> **重要提示**:请务必在**当前运行 Agent 的终端**中设置环境变量。
+
+```bash
+export PROCESSON_API_KEY="<your-processon-api-key>"
+```
+
+获取地址:[ProcessOn 开发者中心](https://smart.processon.com/user)
+
+### 命令行调用参考
+
+```bash
+# 全自动生成:DSL + 图片渲染
+python3 scripts/processon_api_client.py "请生成一张专业流程图"
+
+# 仅生成 DSL (不渲染图片)
+python3 scripts/processon_api_client.py --no-render "请生成一张专业流程图"
+```
+
+## 示例优化 Prompt
+
+> **用户意图**:帮我画一个登录流程。
+> **优化后**:请生成一张专业的流程图,描述用户登录注册流程。包含:前端校验、后端鉴权、数据库查询、Token 发放。要求:布局清晰,使用标准流程图符号,明确开始和结束节点,配色协调。
+
+> **User intent**: Draw a user login flow.
+> **Optimized prompt**: Please generate a professional flowchart for the user login and registration flow. Include frontend validation, backend authentication, database lookup, and token issuance. Use a clean layout, standard flowchart symbols, clear start and end nodes, and a polished color palette.

+ 818 - 0
skills/document-skills/processon-diagram-generator/scripts/processon_api_client.py

@@ -0,0 +1,818 @@
+import argparse
+import base64
+import json
+import os
+import re
+import sys
+import urllib.error
+import urllib.request
+from datetime import datetime
+
+DSL_STREAM_API_URL = "https://smart.processon.com/v1/chat/completion"
+DSL_STREAM_MODEL = "deepseek-v3-2-251201"
+DSL_STREAM_UID = "567890"
+DSL_EDIT_URL = "https://smart.processon.com/editor"
+IMAGE_RENDER_API_URL = "https://smart.processon.com/v1/api/generate/img"
+
+
+def normalize_title(title):
+    if not title:
+        return "processon-diagram"
+    normalized = title.strip(",。;:、 ,.-_")
+    if not normalized:
+        return "processon-diagram"
+    return normalized[:20]
+
+
+def slugify_filename(title):
+    if not title:
+        return "processon-diagram"
+    slug = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9_-]+", "-", title).strip("-_")
+    if not slug:
+        slug = "processon-diagram"
+    return slug[:40]
+
+
+def save_image_content(title, content_items, output_dir=None):
+    if not output_dir:
+        output_dir = os.path.join(os.getcwd(), "outputs", "processon")
+
+    saved_paths = []
+    os.makedirs(output_dir, exist_ok=True)
+
+    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
+    image_index = 1
+    title = normalize_title(title)
+    filename_slug = slugify_filename(title)
+
+    for item in content_items:
+        if not isinstance(item, dict):
+            continue
+        if item.get("type") != "image":
+            continue
+        if item.get("mimeType") != "image/png":
+            continue
+
+        image_data = item.get("data", "")
+        if not image_data:
+            continue
+
+        if image_index == 1:
+            filename = f"{filename_slug}-{timestamp}.png"
+        else:
+            filename = f"{filename_slug}-{timestamp}-{image_index}.png"
+        file_path = os.path.abspath(os.path.join(output_dir, filename))
+        with open(file_path, "wb") as f:
+            f.write(base64.b64decode(image_data))
+        saved_paths.append(file_path)
+        image_index += 1
+
+    return {
+        "title": title,
+        "filename_slug": filename_slug,
+        "saved_paths": saved_paths,
+    }
+
+
+def normalize_bearer(api_key):
+    if not api_key:
+        return None
+    api_key = api_key.strip()
+    if not api_key:
+        return None
+    if api_key.lower().startswith("bearer "):
+        return api_key
+    return f"Bearer {api_key}"
+
+
+def build_headers(api_key):
+    headers = {
+        "Content-Type": "application/json; charset=utf-8",
+        "Accept": "*/*",
+        "User-Agent": "ProcessOn-Architect-Skill/3.0",
+    }
+    bearer = normalize_bearer(api_key)
+    if bearer:
+        headers["Authorization"] = bearer
+    return headers
+
+
+def build_stream_payload(prompt):
+    return {
+        "model": DSL_STREAM_MODEL,
+        "prompt": prompt,
+        "uid": DSL_STREAM_UID,
+        "source": "skill",
+        "cancelPursueNode": True,
+    }
+
+
+def build_image_payload(dsl, diagram_type):
+    payload = {
+        "prompt": dsl,
+        "source": "skill",
+    }
+    if diagram_type:
+        payload["diagramType"] = diagram_type
+    return payload
+
+
+def normalize_content_items(content_items):
+    normalized = []
+    for item in content_items:
+        if not isinstance(item, dict):
+            continue
+        normalized_item = dict(item)
+        if "data" in normalized_item and "text" not in normalized_item and normalized_item.get("type") == "text":
+            normalized_item["text"] = normalized_item["data"]
+        normalized.append(normalized_item)
+    return normalized
+
+
+def extract_content_items(result):
+    if isinstance(result, list):
+        return result
+    if not isinstance(result, dict):
+        return None
+    if isinstance(result.get("content"), list):
+        return result["content"]
+    if isinstance(result.get("data"), dict) and isinstance(result["data"].get("content"), list):
+        return result["data"]["content"]
+    return None
+
+
+def extract_remote_image_urls(content_items):
+    urls = []
+    for item in content_items:
+        if not isinstance(item, dict):
+            continue
+        if item.get("type") != "image_url":
+            continue
+        url = item.get("url")
+        if isinstance(url, str) and url.strip():
+            urls.append(url.strip())
+    return urls
+
+
+def build_final_image_payload(result, diagram_title):
+    content = extract_content_items(result)
+    if content is None:
+        raise ValueError("Invalid image response: missing 'content' array")
+
+    normalized_content = normalize_content_items(content)
+    remote_image_urls = extract_remote_image_urls(normalized_content)
+    save_result = save_image_content(diagram_title, normalized_content)
+    saved_paths = save_result["saved_paths"]
+
+    output_content = []
+    for item in normalized_content:
+        if not isinstance(item, dict):
+            continue
+        if item.get("type") in ("image", "image_url"):
+            output_content.append(item)
+    if remote_image_urls:
+        output_content.append({
+            "type": "text",
+            "text": "\n".join(["图片链接:"] + remote_image_urls),
+        })
+    if saved_paths:
+        output_content.append({
+            "type": "text",
+            "text": "\n".join(["图片已保存:"] + saved_paths),
+        })
+
+    output_payload = {"content": output_content}
+    if isinstance(result, dict) and isinstance(result.get("data"), dict):
+        output_payload["data"] = dict(result["data"])
+    if remote_image_urls:
+        output_payload.setdefault("data", {})
+        output_payload["data"]["remoteImageUrls"] = remote_image_urls
+    if saved_paths:
+        output_payload.setdefault("data", {})
+        output_payload["data"].update({
+            "imageTitle": save_result["title"],
+            "savedImagePaths": saved_paths,
+            "primarySavedImagePath": saved_paths[0],
+            "preferredDisplay": "inline",
+            "showInlineIfPossible": True,
+        })
+    return output_payload
+
+
+def build_image_failure_payload(message):
+    return {
+        "content": [
+            {
+                "type": "text",
+                "text": "\n".join([
+                    "DSL 已生成,但图片生成失败。",
+                    f"你可以打开编辑链接,粘贴 DSL 继续渲染和导出图片:{DSL_EDIT_URL}",
+                    f"失败原因:{message}",
+                ]),
+            }
+        ],
+        "data": {"errorCode": "IMAGE_RENDER_FAILED"},
+    }
+
+
+def generate_diagram(prompt, title=None, stream_style=None, output_mode=None, auto_render=True):
+    output_mode = (output_mode or os.environ.get("PROCESSON_OUTPUT_MODE", "text")).strip().lower()
+    stream_style = (stream_style or os.environ.get("PROCESSON_STREAM_STYLE", "host")).strip().lower()
+    dsl_event_buffer = ""
+
+    def is_json_output():
+        return output_mode == "json"
+
+    def is_event_stream_output():
+        return output_mode in ("eventstream", "event-stream", "events", "jsonl", "ndjson")
+
+    if hasattr(sys.stdout, "reconfigure"):
+        try:
+            sys.stdout.reconfigure(write_through=True)
+        except Exception:
+            pass
+
+    def write_stdout(text):
+        if text is None:
+            return
+        if not isinstance(text, str):
+            text = str(text)
+        sys.stdout.write(text)
+        sys.stdout.flush()
+
+    def mcp_print(payload):
+        if is_json_output():
+            print(json.dumps(payload, ensure_ascii=False), flush=True)
+            return
+        if is_event_stream_output():
+            for event in payload_to_events(payload):
+                print(json.dumps(event, ensure_ascii=False), flush=True)
+            return
+        text = payload_to_text(payload)
+        if text:
+            write_stdout(text)
+            if not text.endswith("\n"):
+                write_stdout("\n")
+
+    def mcp_print_text(text, data=None):
+        if is_json_output() or is_event_stream_output():
+            payload = {"event": "commentary", "text": text}
+            if data:
+                payload.update(data)
+            print(json.dumps(payload, ensure_ascii=False), flush=True)
+            return
+        write_stdout(text)
+
+    def payload_to_text(payload):
+        if not isinstance(payload, dict):
+            return json.dumps(payload, ensure_ascii=False)
+
+        content = payload.get("content")
+        parts = []
+        if isinstance(content, list):
+            for item in content:
+                if not isinstance(item, dict):
+                    continue
+                item_type = item.get("type")
+                if item_type == "text":
+                    text = item.get("text")
+                    if text is None:
+                        text = item.get("data")
+                    if isinstance(text, str) and text:
+                        parts.append(text)
+                elif item_type == "image_url":
+                    url = item.get("url")
+                    if isinstance(url, str) and url.strip():
+                        parts.append(url.strip())
+
+        if parts:
+            return "\n\n".join(parts)
+
+        data = payload.get("data")
+        if isinstance(data, dict):
+            extra_parts = []
+            remote_urls = data.get("remoteImageUrls") or []
+            saved_paths = data.get("savedImagePaths") or []
+            if remote_urls:
+                extra_parts.append("\n".join(["图片链接:"] + remote_urls))
+            if saved_paths:
+                extra_parts.append("\n".join(["图片已保存:"] + saved_paths))
+            if extra_parts:
+                return "\n\n".join(extra_parts)
+
+        return json.dumps(payload, ensure_ascii=False)
+
+    def payload_to_events(payload):
+        events = []
+        if not isinstance(payload, dict):
+            return [{"event": "message", "text": json.dumps(payload, ensure_ascii=False)}]
+
+        content = payload.get("content")
+        if isinstance(content, list):
+            for item in content:
+                if not isinstance(item, dict):
+                    continue
+                item_type = item.get("type")
+                if item_type == "text":
+                    text = item.get("text")
+                    if text is None:
+                        text = item.get("data")
+                    if isinstance(text, str) and text:
+                        events.append({"event": "message", "text": text, "data": {"contentType": "text"}})
+                elif item_type == "image_url":
+                    url = item.get("url")
+                    if isinstance(url, str) and url.strip():
+                        events.append({"event": "image_url", "text": url.strip(), "data": {"contentType": "image_url"}})
+                elif item_type == "image":
+                    events.append({"event": "image", "data": item})
+
+        data = payload.get("data")
+        if isinstance(data, dict):
+            for url in data.get("remoteImageUrls") or []:
+                if isinstance(url, str) and url.strip():
+                    events.append({"event": "image_url", "text": url.strip(), "data": {"contentType": "image_url"}})
+            for path in data.get("savedImagePaths") or []:
+                if isinstance(path, str) and path.strip():
+                    events.append({"event": "saved_image_path", "text": path.strip(), "data": {"contentType": "saved_image_path"}})
+
+        if not events:
+            events.append({"event": "message", "text": json.dumps(payload, ensure_ascii=False)})
+        return events
+
+    def emit_event(event_type, text=None, data=None):
+        payload = {"event": event_type}
+        if text is not None:
+            payload["text"] = text
+        if data is not None:
+            payload["data"] = data
+        print(json.dumps(payload, ensure_ascii=False), flush=True)
+
+    def flush_dsl_event_buffer(force=False):
+        nonlocal dsl_event_buffer
+        if not is_event_stream_output():
+            return
+        if not dsl_event_buffer:
+            return
+        if force or len(dsl_event_buffer) > 20 or "\n" in dsl_event_buffer:
+            emit_event("dsl_line", text=dsl_event_buffer)
+            dsl_event_buffer = ""
+
+    def build_credential_metadata():
+        macos_command = 'export PROCESSON_API_KEY="<your-processon-api-key>"'
+        windows_powershell_command = '$env:PROCESSON_API_KEY="<your-processon-api-key>"'
+        windows_cmd_command = 'set PROCESSON_API_KEY=<your-processon-api-key>'
+        return {
+            "credential": {
+                "name": "PROCESSON_API_KEY",
+                "label": "ProcessOn API Key",
+                "kind": "secret",
+                "required": True,
+                "envVar": "PROCESSON_API_KEY",
+                "placeholder": "<your-processon-api-key>",
+                "description": "用于 ProcessOn API 的鉴权密钥。",
+            },
+            "actions": [
+                {
+                    "type": "request_credential",
+                    "credential": "PROCESSON_API_KEY",
+                    "label": "配置 ProcessOn API Key",
+                    "mode": "secret",
+                },
+                {
+                    "type": "show_config_example",
+                    "target": "processon-api",
+                    "label": "查看配置示例",
+                },
+                {
+                    "type": "copy_command",
+                    "label": "复制 macOS/Linux 配置命令",
+                    "command": macos_command,
+                    "platform": ["macos", "linux"],
+                },
+                {
+                    "type": "copy_command",
+                    "label": "复制 Windows PowerShell 配置命令",
+                    "command": windows_powershell_command,
+                    "platform": ["windows"],
+                    "shell": "powershell",
+                },
+                {
+                    "type": "copy_command",
+                    "label": "复制 Windows CMD 配置命令",
+                    "command": windows_cmd_command,
+                    "platform": ["windows"],
+                    "shell": "cmd",
+                },
+                {
+                    "type": "retry",
+                    "label": "配置完成后重试",
+                },
+            ],
+            "suggestedCommands": {
+                "macos_linux": macos_command,
+                "windows_powershell": windows_powershell_command,
+                "windows_cmd": windows_cmd_command,
+                "verify": "echo $PROCESSON_API_KEY",
+                "retryPrompt": "继续生成流程图",
+            },
+            "interactive": {
+                "canRequestCredential": True,
+                "preferredAction": "request_credential",
+            },
+        }
+
+    def build_missing_api_key_payload():
+        hint = "\n".join([
+            "当前还没有检测到可用的 ProcessOn API Key,所以暂时无法直接生成流程图。",
+            "API Key/Token 获取地址:https://smart.processon.com/user",
+            "macOS/Linux:",
+            '  export PROCESSON_API_KEY="<your-processon-api-key>"',
+        ])
+        payload = {
+            "content": [{"type": "text", "text": hint}],
+            "data": {"errorCode": "MISSING_API_KEY"},
+        }
+        payload["data"].update(build_credential_metadata())
+        return payload
+
+    def build_invalid_api_key_payload(http_message):
+        hint = "\n".join([
+            "检测到 PROCESSON_API_KEY,但鉴权失败。当前配置的 API Key 可能无效、已过期,或不适用于当前接口。",
+            "如需重新获取 Token,请访问:https://smart.processon.com/user",
+            "",
+            f"失败原因:{http_message}",
+            "",
+            "请检查:",
+            "1. PROCESSON_API_KEY 是否填写正确",
+            "2. 该 Key 是否具备 smart.processon.com 接口访问权限",
+            "3. 是否误填了其他系统的 token",
+        ])
+        payload = {
+            "content": [{"type": "text", "text": hint}],
+            "data": {"errorCode": "INVALID_API_KEY"},
+        }
+        payload["data"].update(build_credential_metadata())
+        return payload
+
+    def extract_complete_json(buffer):
+        depth = 0
+        in_string = False
+        escape = False
+        start_index = -1
+
+        for index, char in enumerate(buffer):
+            if in_string:
+                if escape:
+                    escape = False
+                elif char == "\\":
+                    escape = True
+                elif char == '"':
+                    in_string = False
+                continue
+
+            if char == '"':
+                in_string = True
+                continue
+
+            if char == "{":
+                if depth == 0:
+                    start_index = index
+                depth += 1
+            elif char == "}":
+                depth -= 1
+                if depth == 0 and start_index != -1:
+                    return buffer[start_index:index + 1], buffer[index + 1:]
+
+        return None
+
+    def parse_json_response(response):
+        status_code = getattr(response, "status", "unknown")
+        content_type = response.headers.get("Content-Type", "unknown")
+        response_data = response.read().decode("utf-8")
+        if not response_data.strip():
+            raise ValueError(
+                f"Empty response body from ProcessOn API "
+                f"(status={status_code}, content_type={content_type})"
+            )
+        try:
+            return json.loads(response_data)
+        except json.JSONDecodeError as exc:
+            snippet = response_data[:500]
+            raise ValueError(
+                f"Invalid JSON response from ProcessOn API "
+                f"(status={status_code}, content_type={content_type}, body_prefix={snippet!r})"
+            ) from exc
+
+    def open_json_request(url, headers, payload, timeout=180):
+        json_payload = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+        request = urllib.request.Request(url, data=json_payload, headers=headers, method="POST")
+        return urllib.request.urlopen(request, timeout=timeout)
+
+    def use_markdown_stream():
+        return stream_style in ("markdown", "md", "codefence", "code_fence", "fenced")
+
+    def guess_dsl_code_fence(dsl_text):
+        if not isinstance(dsl_text, str):
+            return ""
+        stripped = dsl_text.strip()
+        if not stripped:
+            return ""
+        first_line = ""
+        for line in stripped.splitlines():
+            normalized = line.strip()
+            if normalized:
+                first_line = normalized.lower()
+                break
+        if first_line.startswith((
+            "graph ",
+            "flowchart ",
+            "sequencediagram",
+            "classdiagram",
+            "statediagram",
+            "statediagram-v2",
+            "erdiagram",
+            "journey",
+            "gantt",
+            "mindmap",
+            "timeline",
+            "gitgraph",
+            "pie",
+            "quadrantchart",
+            "requirementdiagram",
+            "xychart",
+            "block-beta",
+        )):
+            return "mermaid"
+        if stripped.startswith("{") or stripped.startswith("["):
+            return "json"
+        return "text"
+
+    def build_first_stage_display_text(dsl_text):
+        fence = guess_dsl_code_fence(dsl_text)
+        lines = [
+            "第一阶段已完成,DSL 已生成。",
+            "",
+            "编辑链接:",
+            DSL_EDIT_URL,
+            "",
+            "DSL 原文如下(你可以复制上方 DSL 数据到此链接进行渲染和二次编辑):",
+            f"```{fence}",
+            dsl_text,
+            "```",
+        ]
+        return "\n".join(lines)
+
+    def emit_dsl_stream_prefix():
+        if use_markdown_stream():
+            mcp_print_text(
+                "创建图结果:\n```text\n",
+                data={"event": "dsl_start", "streamStyle": "markdown"},
+            )
+            return
+        mcp_print_text(
+            "创建图结果(实时输出):\n",
+            data={"event": "dsl_start", "streamStyle": "host"},
+        )
+
+    def emit_dsl_stream_suffix():
+        flush_dsl_event_buffer(force=True)
+        if use_markdown_stream():
+            mcp_print_text("\n```\n", data={"event": "dsl_complete", "streamStyle": "markdown"})
+            return
+        mcp_print_text("\n\nDSL 输出结束。\n", data={"event": "dsl_complete", "streamStyle": "host"})
+
+    def emit_edit_link():
+        mcp_print_text(
+            "\n".join([
+                "编辑链接:",
+                DSL_EDIT_URL,
+                "",
+                "你可以复制上方 DSL 数据到此链接进行渲染和二次编辑。",
+            ]) + "\n",
+            data={"event": "edit_link", "url": DSL_EDIT_URL},
+        )
+
+    def stream_dsl_from_chat_completion(prompt_text, headers):
+        payload = build_stream_payload(prompt_text)
+        ai_content_arr = ["", "", ""]
+        step_number = 0
+        diagram_type = ""
+        dsl_started = False
+        event_name = None
+        event_data_lines = []
+
+        with open_json_request(DSL_STREAM_API_URL, headers, payload, timeout=300) as response:
+            for raw_line in response:
+                line = raw_line.decode("utf-8", errors="replace")
+                stripped = line.strip("\r\n")
+
+                if not stripped:
+                    if event_data_lines:
+                        event_data = "\n".join(event_data_lines)
+                        event_name, step_number, diagram_type, dsl_started = handle_stream_message(
+                            event_name,
+                            event_data,
+                            ai_content_arr,
+                            step_number,
+                            diagram_type,
+                            dsl_started,
+                        )
+                        if event_name == "DONE":
+                            break
+                    event_name = None
+                    event_data_lines = []
+                    continue
+
+                if stripped.startswith(":"):
+                    continue
+                if stripped.startswith("event:"):
+                    event_name = stripped.split(":", 1)[1].strip()
+                if stripped.startswith("data:"):
+                    event_data_lines.append(stripped.split(":", 1)[1].lstrip())
+
+            if event_data_lines:
+                event_name, step_number, diagram_type, dsl_started = handle_stream_message(
+                    event_name,
+                    "\n".join(event_data_lines),
+                    ai_content_arr,
+                    step_number,
+                    diagram_type,
+                    dsl_started,
+                )
+
+        dsl = ai_content_arr[2].rstrip()
+        if not dsl:
+            raise ValueError("DSL generation failed: empty DSL result from stream")
+        return dsl, diagram_type
+
+    def handle_stream_message(current_event_name, raw_data, ai_content_arr, step_number, diagram_type, dsl_started):
+        nonlocal dsl_event_buffer
+        try:
+            parsed = json.loads(raw_data)
+            data = parsed["a"]
+        except Exception:
+            return current_event_name, step_number, diagram_type, dsl_started
+
+        if data == "语义分析结果:":
+            step_number = 1
+            ai_content_arr[1] = ""
+            mcp_print_text("语义分析结果:\n", data={"event": "analysis_start"})
+            return current_event_name, step_number, diagram_type, dsl_started
+        if data == "创建图结果:":
+            step_number = 2
+            ai_content_arr[2] = ""
+            dsl_started = True
+            emit_dsl_stream_prefix()
+            return current_event_name, step_number, diagram_type, dsl_started
+        if data == "追问结果:":
+            if dsl_started:
+                emit_dsl_stream_suffix()
+                emit_edit_link()
+            return current_event_name, 3, diagram_type, False
+        if data == "[DONE]":
+            if dsl_started:
+                emit_dsl_stream_suffix()
+                emit_edit_link()
+            return "DONE", 3, diagram_type, False
+
+        if step_number == 1:
+            ai_content_arr[1] += data
+            extracted = extract_complete_json(ai_content_arr[1])
+            while extracted:
+                json_str, rest = extracted
+                ai_content_arr[1] = rest
+                parsed_json = json.loads(json_str)
+                if parsed_json.get("type") == "analysis" and parsed_json.get("content"):
+                    analysis_text = parsed_json["content"]
+                    if output_mode != "json" and not analysis_text.endswith("\n"):
+                        analysis_text += "\n"
+                    mcp_print_text(analysis_text, data={"event": "analysis_chunk"})
+                    if is_event_stream_output():
+                        emit_event("analysis_complete", text=analysis_text.strip())
+                if parsed_json.get("type") == "route":
+                    diagram_type = parsed_json.get("diagramType") or diagram_type
+                    if is_event_stream_output() and diagram_type:
+                        emit_event("route", text=diagram_type, data={"diagramType": diagram_type})
+                extracted = extract_complete_json(ai_content_arr[1])
+        elif step_number == 2:
+            ai_content_arr[2] += data
+            mcp_print_text(data, data={"event": "dsl_chunk"})
+            if is_event_stream_output():
+                dsl_event_buffer += data
+                flush_dsl_event_buffer()
+
+        return current_event_name, step_number, diagram_type, dsl_started
+
+    api_key = os.environ.get("PROCESSON_API_KEY", "")
+
+    try:
+        if not api_key.strip():
+            mcp_print(build_missing_api_key_payload())
+            sys.exit(1)
+
+        headers = build_headers(api_key)
+        dsl, diagram_type = stream_dsl_from_chat_completion(prompt, headers)
+        
+        # End of first stage
+        display_text = build_first_stage_display_text(dsl)
+        result_data = {
+            "dsl": dsl,
+            "diagramType": diagram_type,
+            "editUrl": DSL_EDIT_URL,
+            "title": title,
+            "displayText": display_text,
+            "displayImmediately": True,
+        }
+        
+        if is_event_stream_output():
+            emit_event("dsl_ready", text=display_text, data=result_data)
+        elif is_json_output():
+            mcp_print({
+                "content": [{"type": "text", "text": display_text}],
+                "data": result_data,
+            })
+        else:
+            # For plain text mode, we already printed fragments.
+            # But we might want to ensure the final block is clear.
+            pass
+
+        if auto_render:
+            if is_event_stream_output():
+                emit_event("image_render_start", text="正在渲染图片")
+            
+            try:
+                render_payload = build_image_payload(dsl, diagram_type)
+                with open_json_request(IMAGE_RENDER_API_URL, headers, render_payload, timeout=180) as response:
+                    image_result = parse_json_response(response)
+                    final_payload = build_final_image_payload(image_result, title)
+                    if is_event_stream_output():
+                        emit_event("image_render_succeeded", text="图片渲染成功")
+                    mcp_print(final_payload)
+            except Exception as e:
+                msg = str(e)
+                if is_event_stream_output():
+                    emit_event("image_render_failed", text=msg)
+                mcp_print(build_image_failure_payload(msg))
+
+        return dsl, diagram_type
+
+    except urllib.error.HTTPError as exc:
+        try:
+            body = exc.read().decode("utf-8")
+            msg = f"HTTP {exc.code} {exc.reason}: {body}"
+        except Exception:
+            msg = f"HTTP {exc.code} {exc.reason}"
+        current_api_key = os.environ.get("PROCESSON_API_KEY", "").strip()
+        if exc.code in (401, 403) and not current_api_key:
+            missing_payload = build_missing_api_key_payload()
+            missing_payload["content"][0]["text"] = f"{msg}\n\n{missing_payload['content'][0]['text']}"
+            mcp_print(missing_payload)
+            sys.exit(1)
+        if exc.code in (401, 403) and current_api_key:
+            mcp_print(build_invalid_api_key_payload(msg))
+            sys.exit(1)
+        mcp_print_text(msg)
+        sys.exit(1)
+    except urllib.error.URLError as exc:
+        mcp_print_text(f"Connection Error: {exc.reason}")
+        sys.exit(1)
+    except Exception as exc:
+        mcp_print_text(str(exc))
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description="ProcessOn AI Diagram Generator")
+    parser.add_argument("prompt", type=str, help="The optimized prompt for the diagram")
+    parser.add_argument("--title", type=str, default="processon-diagram", help="Short title for the saved image filename")
+    parser.add_argument(
+        "--stream-style",
+        type=str,
+        choices=["host", "markdown"],
+        default=None,
+        help="Streaming presentation style.",
+    )
+    parser.add_argument(
+        "--output-mode",
+        type=str,
+        choices=["text", "json", "eventstream", "jsonl", "ndjson"],
+        default=None,
+        help="Output mode.",
+    )
+    parser.add_argument(
+        "--no-render",
+        action="store_false",
+        dest="auto_render",
+        help="Disable automatic image rendering.",
+    )
+    parser.set_defaults(auto_render=True)
+
+    args = parser.parse_args()
+
+    generate_diagram(
+        args.prompt,
+        args.title,
+        stream_style=args.stream_style,
+        output_mode=args.output_mode,
+        auto_render=args.auto_render,
+    )

+ 4 - 0
skills/document-skills/processon-diagram-generator/version/coze-version.json

@@ -0,0 +1,4 @@
+{
+    "version": "3.5.0",
+    "changelog": "优化了生成效果"
+}

+ 4 - 0
skills/document-skills/processon-diagram-generator/version/github-version.json

@@ -0,0 +1,4 @@
+{
+    "version": "2.4.0",
+    "changelog": "优化了生成速度和效果"
+}