@microi.net/cli 5.9.4 → 5.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codebuddy-plugin/marketplace.json +2 -2
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/.workbuddy-plugin/marketplace.json +2 -2
- package/.workbuddy-plugin/plugin.json +1 -1
- package/LICENSE +21 -21
- package/README.md +71 -71
- package/assets/build-meta.json +7 -7
- package/assets/feature-matrix.json +138 -138
- package/assets/logo.svg +4 -4
- package/cordis.patch.yml +1 -1
- package/package.json +1 -1
- package/scripts/codex-marketplace.json +20 -20
- package/scripts/mcp-codex-stdio-adapter.js +189 -189
- package/scripts/mcp-server.js +1 -0
- package/scripts/mcp-trae-windows-launcher.cmd +21 -21
- package/scripts/microi-cli-mcp.js +7 -7
- package/scripts/microi-cli.js +44 -49
- package/scripts/microi-codex-broker.js +466 -466
- package/scripts/microi-codex-router.js +635 -634
- package/scripts/microi-skills.meta.json +248 -248
- package/skills/.microi-skills-version.json +2 -2
- package/skills/.progressive-disclosure-manifest.json +55 -46
- package/skills/ai-engine/agents/openai.yaml +4 -4
- package/skills/app-store/agents/openai.yaml +4 -4
- package/skills/datasource-engine/agents/openai.yaml +4 -4
- package/skills/job-engine/agents/openai.yaml +4 -4
- package/skills/message-notification/agents/openai.yaml +5 -5
- package/skills/microi-ai-app-auth.js +652 -652
- package/skills/microi-ai-application/agents/openai.yaml +4 -4
- package/skills/microi-codex/SKILL.md +11 -6
- package/skills/microi-codex-installer/SKILL.md +1 -1
- package/skills/microi-codex-installer/agents/openai.yaml +7 -7
- package/skills/microi-db-schema/agents/openai.yaml +4 -4
- package/skills/microi-deployment/SKILL.md +6 -4
- package/skills/microi-docs-coverage/references/capability-map.md +1 -1
- package/skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +894 -894
- package/skills/microi-microservice/SKILL.md +5 -0
- package/skills/microi-solution-quotation/agents/openai.yaml +4 -4
- package/skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -296
- package/skills/microi-system-delivery/SKILL.md +18 -6
- package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +39 -1
- package/skills/microi-system-delivery/references/progressive-02-/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/345/277/205/351/241/273/350/246/206/347/233/226/347/232/204/345/235/221.md +5 -3
- package/skills/microi-uniapp-frontend/SKILL.md +3 -1
- package/skills/microi.v8.js +1921 -1921
- package/skills/module-engine/SKILL.md +26 -0
- package/skills/ocr-engine/agents/openai.yaml +4 -4
- package/skills/page-engine/SKILL.md +21 -0
- package/skills/page-engine/examples/compact-dashboard.json +1444 -1444
- package/skills/playwright-e2e/SKILL.md +5 -4
- package/skills/playwright-e2e/references/progressive-03-microi-helper-/346/250/241/346/235/277.md +3 -3
- package/skills/report-engine/SKILL.md +12 -0
- package/skills/report-engine/agents/openai.yaml +4 -4
- package/skills/scripts/optimize-progressive-disclosure.mjs +204 -204
- package/skills/scripts/refresh-progressive-disclosure.mjs +64 -64
- package/skills/scripts/sync-embedded-skills.mjs +46 -46
- package/skills/scripts/validate-progressive-disclosure.mjs +57 -57
- package/skills/search-engine/agents/openai.yaml +4 -4
- package/skills/translate-engine/agents/openai.yaml +4 -4
- package/skills/ui-design/assets/pattern-showcase/app.js +54 -54
- package/skills/ui-design/assets/pattern-showcase/index.html +163 -163
- package/skills/ui-design/assets/pattern-showcase/styles.css +311 -311
- package/skills/unity-integration/agents/openai.yaml +4 -4
- package/skills/v8-file-upload/SKILL.md +2 -0
- package/skills/v8-image-processing/agents/openai.yaml +4 -4
- package/skills/v8-mongodb/SKILL.md +2 -1
- package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +203 -203
- package/skills/v8-security/SKILL.md +6 -0
- package/skills/v8-tcp-integration/agents/openai.yaml +4 -4
- package/skills/vision-engine/agents/openai.yaml +4 -4
- package/skills/workspace-conventions/SKILL.md +5 -4
- package/skills/workspace-conventions/references/progressive-02-microi-net-api-/346/234/254/345/234/260/345/220/257/345/212/250/347/272/246/345/256/232.md +13 -0
|
@@ -231,6 +231,11 @@ AI 生成菜单微服务时,应优先封装一个 `callMicroiHost(action, data
|
|
|
231
231
|
|
|
232
232
|
### 3D 数字孪生动态资产与许可门禁
|
|
233
233
|
|
|
234
|
+
- 通用 3D 引擎导入 Tekla `.lis` 时,先确认它是型材/截面库而非完整工程模型;缺少构件长度、位置与装配关系,不能宣称自动还原楼宇或钢结构。保留 GLB/GLTF 原加载流程,LIS 使用独立的文本解析与参数化几何流程。
|
|
235
|
+
- 型材库先列出规格、搜索/分页和可预览状态,不自动选择或一次性实例化全部规格;用户手动选择后输入毫米长度,再明确添加到场景。尺寸统一按 `1000 mm = 1 m` 转换,长度不能伪装成文件提供的工程数据。保存/导出场景携带已校验的截面参数和长度,重新打开时重建几何,沿用原有场景保存、渲染、材质和变换流程,不依赖原电脑文件路径或新增一套持久化。
|
|
236
|
+
- 当前 LIS 适配的 `PROFILE_TYPE/PROFILE_SUB_TYPE` 精确组合为 H/I `1/1001`、L `2/2001`、圆管 `7/7001`、矩形空心 `8/8002`、C `9/9001`、T `10/10001`。BP 类型 `20`、H 特殊子类型 `1002`、其它组合、实心圆钢、变截面及非零截面坐标偏移必须展示具体原因并拒绝生成,不能只按规格名称猜形状。H/I/L/矩形空心/C/T 的圆角或斜翼缘采用近似轮廓时,界面和交付文档明确显示“近似预览”;圆管必须保留内孔。预览不作为加工、结构计算或施工精度依据。
|
|
237
|
+
- LIS 文件内容是数据,不执行脚本、宏、表达式或外部资源;先校验文件大小、记录数量、字段类型、有限数值、壁厚与内孔,再创建有界几何。浏览器本地预览无需上传原文件,保存服务器场景继续使用已有租户和权限边界。验收至少覆盖非法文本、超限与非有限尺寸、不支持类型、内孔、毫米换算、长度校验、保存恢复、旧 GLB/GLTF、页面卸载释放及真实 WebGL 画面。
|
|
238
|
+
- 官方引擎必须从版本管理的应用源码契约定位唯一源码根,测试归应用自身 `tests/`;不能从旧主前端同名目录或其它同步副本择新构建。交付分别回读私有源码、公有运行清单、包含对应运行版本的官方平台包、目标租户安装与页面实际操作,Docker 更新不能代替目标应用安装。
|
|
234
239
|
- 通用 3D 设计器/渲染器与某一楼宇的定制数字孪生应使用不同 AppKey、源码目录、路由清单、运行资产与商城安装包。官方 `Platform` 包只拥有通用引擎和平台菜单;项目专属模型/贴图、业务表与字段、IoT 接口、演示数据和业务菜单属于独立业务应用(本项目采用 `MicroService`)。批量“安装/更新全部平台应用”只覆盖 `ApplicationType=Platform`,不得借平台包向所有租户散发某个客户的项目。拆分时要检查旧商城包的 `SelectMenu/SelectTable/SelectApiEngine/SelectData` 回退选择,清空旧选择后重新制作安装包,并分别回读两个包的资源快照与真实浏览器入口。
|
|
235
240
|
- 道路车辆、人物、设备动画不能只靠静态截图验收。浏览器测试应同时断言对象数量、时间推进后的坐标/骨骼变化、路径末端回绕、切换楼层后的恢复及 `prefers-reduced-motion` 设置下的实际行为;测试探针只暴露非敏感数量和位置,不暴露 Token、人员或设备隐私。若持续运动本身是用户明确要求的核心功能,不得把系统的“减少动态效果”误用为整体动画总开关;只降级装饰性过渡,并提供可见、可键盘操作的暂停/恢复控制。页面隐藏或暂停期间冻结业务动画时钟,恢复时扣除停顿时间,避免物体瞬移。
|
|
236
241
|
- 外部 GLB/GLTF 进入官方运行包前,逐项保存作者、原始地址、许可证、是否允许商用和再分发、署名方式、三角面数、纹理大小及优化记录。“免费下载”、搜索摘要或仓库内没有许可证都不构成可再分发授权;`NC`、`ND` 或用途不明的资产默认不进入商业应用包。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Microi吾码解决方案与报价"
|
|
3
|
-
short_description: "生成紧凑专业的吾码解决方案、周期与总价报价 Word"
|
|
4
|
-
default_prompt: "使用 $microi-solution-quotation 生成一份紧凑的一页 Microi吾码系统解决方案与报价 Word。"
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Microi吾码解决方案与报价"
|
|
3
|
+
short_description: "生成紧凑专业的吾码解决方案、周期与总价报价 Word"
|
|
4
|
+
default_prompt: "使用 $microi-solution-quotation 生成一份紧凑的一页 Microi吾码系统解决方案与报价 Word。"
|
|
@@ -1,296 +1,296 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import json
|
|
3
|
-
from datetime import date
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from docx import Document
|
|
7
|
-
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
|
8
|
-
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
9
|
-
from docx.oxml import OxmlElement
|
|
10
|
-
from docx.oxml.ns import qn
|
|
11
|
-
from docx.shared import Inches, Pt, RGBColor
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
NAVY = "17365D"
|
|
15
|
-
BLUE = "2F75B5"
|
|
16
|
-
PALE_BLUE = "EAF3F9"
|
|
17
|
-
PALE_GRAY = "F4F6F8"
|
|
18
|
-
PALE_GOLD = "FFF3D6"
|
|
19
|
-
GOLD = "B45309"
|
|
20
|
-
INK = "1F2937"
|
|
21
|
-
MUTED = "64748B"
|
|
22
|
-
WHITE = "FFFFFF"
|
|
23
|
-
BORDER = "CBD5E1"
|
|
24
|
-
FONT = "Microsoft YaHei"
|
|
25
|
-
PAGE_WIDTH_DXA = 11909
|
|
26
|
-
MARGIN_DXA = 720
|
|
27
|
-
TABLE_INDENT_DXA = 120
|
|
28
|
-
TABLE_WIDTH_DXA = PAGE_WIDTH_DXA - MARGIN_DXA * 2 - TABLE_INDENT_DXA
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def set_run_font(run, size=9.2, bold=False, color=INK):
|
|
32
|
-
run.font.name = FONT
|
|
33
|
-
rfonts = run._element.get_or_add_rPr().get_or_add_rFonts()
|
|
34
|
-
for key in ("w:eastAsia", "w:ascii", "w:hAnsi"):
|
|
35
|
-
rfonts.set(qn(key), FONT)
|
|
36
|
-
run.font.size = Pt(size)
|
|
37
|
-
run.font.bold = bold
|
|
38
|
-
run.font.color.rgb = RGBColor.from_string(color)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def format_paragraph(paragraph, before=0, after=0, line=1.05, align=None):
|
|
42
|
-
fmt = paragraph.paragraph_format
|
|
43
|
-
fmt.space_before = Pt(before)
|
|
44
|
-
fmt.space_after = Pt(after)
|
|
45
|
-
fmt.line_spacing = line
|
|
46
|
-
if align is not None:
|
|
47
|
-
paragraph.alignment = align
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def add_paragraph(doc, text, size=9.2, bold=False, color=INK, before=0, after=2, line=1.05, align=None):
|
|
51
|
-
paragraph = doc.add_paragraph()
|
|
52
|
-
format_paragraph(paragraph, before, after, line, align)
|
|
53
|
-
set_run_font(paragraph.add_run(text), size, bold, color)
|
|
54
|
-
return paragraph
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def set_cell_shading(cell, fill):
|
|
58
|
-
tc_pr = cell._tc.get_or_add_tcPr()
|
|
59
|
-
shd = tc_pr.find(qn("w:shd"))
|
|
60
|
-
if shd is None:
|
|
61
|
-
shd = OxmlElement("w:shd")
|
|
62
|
-
tc_pr.append(shd)
|
|
63
|
-
shd.set(qn("w:fill"), fill)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def set_cell_margins(cell, top=70, start=110, bottom=70, end=110):
|
|
67
|
-
tc_pr = cell._tc.get_or_add_tcPr()
|
|
68
|
-
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
|
69
|
-
if tc_mar is None:
|
|
70
|
-
tc_mar = OxmlElement("w:tcMar")
|
|
71
|
-
tc_pr.append(tc_mar)
|
|
72
|
-
for name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
|
73
|
-
element = tc_mar.find(qn(f"w:{name}"))
|
|
74
|
-
if element is None:
|
|
75
|
-
element = OxmlElement(f"w:{name}")
|
|
76
|
-
tc_mar.append(element)
|
|
77
|
-
element.set(qn("w:w"), str(value))
|
|
78
|
-
element.set(qn("w:type"), "dxa")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def set_cell_width(cell, width):
|
|
82
|
-
tc_pr = cell._tc.get_or_add_tcPr()
|
|
83
|
-
tc_w = tc_pr.find(qn("w:tcW"))
|
|
84
|
-
if tc_w is None:
|
|
85
|
-
tc_w = OxmlElement("w:tcW")
|
|
86
|
-
tc_pr.append(tc_w)
|
|
87
|
-
tc_w.set(qn("w:w"), str(width))
|
|
88
|
-
tc_w.set(qn("w:type"), "dxa")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def set_table_geometry(table, widths, indent=TABLE_INDENT_DXA):
|
|
92
|
-
total = sum(widths)
|
|
93
|
-
table.autofit = False
|
|
94
|
-
table.alignment = WD_TABLE_ALIGNMENT.LEFT
|
|
95
|
-
tbl_pr = table._tbl.tblPr
|
|
96
|
-
tbl_w = tbl_pr.find(qn("w:tblW"))
|
|
97
|
-
if tbl_w is None:
|
|
98
|
-
tbl_w = OxmlElement("w:tblW")
|
|
99
|
-
tbl_pr.append(tbl_w)
|
|
100
|
-
tbl_w.set(qn("w:w"), str(total))
|
|
101
|
-
tbl_w.set(qn("w:type"), "dxa")
|
|
102
|
-
tbl_ind = tbl_pr.find(qn("w:tblInd"))
|
|
103
|
-
if tbl_ind is None:
|
|
104
|
-
tbl_ind = OxmlElement("w:tblInd")
|
|
105
|
-
tbl_pr.append(tbl_ind)
|
|
106
|
-
tbl_ind.set(qn("w:w"), str(indent))
|
|
107
|
-
tbl_ind.set(qn("w:type"), "dxa")
|
|
108
|
-
grid = table._tbl.tblGrid
|
|
109
|
-
for child in list(grid):
|
|
110
|
-
grid.remove(child)
|
|
111
|
-
for width in widths:
|
|
112
|
-
col = OxmlElement("w:gridCol")
|
|
113
|
-
col.set(qn("w:w"), str(width))
|
|
114
|
-
grid.append(col)
|
|
115
|
-
for row in table.rows:
|
|
116
|
-
for index, cell in enumerate(row.cells):
|
|
117
|
-
set_cell_width(cell, widths[index])
|
|
118
|
-
set_cell_margins(cell)
|
|
119
|
-
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def set_table_borders(table, color=BORDER, size="6"):
|
|
123
|
-
tbl_pr = table._tbl.tblPr
|
|
124
|
-
borders = tbl_pr.first_child_found_in("w:tblBorders")
|
|
125
|
-
if borders is None:
|
|
126
|
-
borders = OxmlElement("w:tblBorders")
|
|
127
|
-
tbl_pr.append(borders)
|
|
128
|
-
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
|
129
|
-
tag = f"w:{edge}"
|
|
130
|
-
element = borders.find(qn(tag))
|
|
131
|
-
if element is None:
|
|
132
|
-
element = OxmlElement(tag)
|
|
133
|
-
borders.append(element)
|
|
134
|
-
element.set(qn("w:val"), "single")
|
|
135
|
-
element.set(qn("w:sz"), size)
|
|
136
|
-
element.set(qn("w:space"), "0")
|
|
137
|
-
element.set(qn("w:color"), color)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def add_section_heading(doc, text):
|
|
141
|
-
return add_paragraph(doc, text, size=10.8, bold=True, color=BLUE, before=4, after=1, line=1.0)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def add_bullet(doc, label, detail):
|
|
145
|
-
paragraph = doc.add_paragraph(style="List Bullet")
|
|
146
|
-
format_paragraph(paragraph, before=0, after=1, line=1.03)
|
|
147
|
-
set_run_font(paragraph.add_run(f"{label}:"), 8.8, True, NAVY)
|
|
148
|
-
set_run_font(paragraph.add_run(detail), 8.8, False, INK)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def add_metadata_table(doc, config):
|
|
152
|
-
table = doc.add_table(rows=1, cols=4)
|
|
153
|
-
widths = [TABLE_WIDTH_DXA // 4] * 4
|
|
154
|
-
widths[-1] += TABLE_WIDTH_DXA - sum(widths)
|
|
155
|
-
values = [
|
|
156
|
-
("版本", config["version"]),
|
|
157
|
-
("日期", config["date"]),
|
|
158
|
-
("开发周期", config["development_cycle"]),
|
|
159
|
-
("报价有效期", f'{config["validity_days"]} 天'),
|
|
160
|
-
]
|
|
161
|
-
for index, (label, value) in enumerate(values):
|
|
162
|
-
cell = table.cell(0, index)
|
|
163
|
-
set_cell_shading(cell, PALE_GRAY)
|
|
164
|
-
paragraph = cell.paragraphs[0]
|
|
165
|
-
format_paragraph(paragraph, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
166
|
-
set_run_font(paragraph.add_run(f"{label}:"), 8.0, True, MUTED)
|
|
167
|
-
set_run_font(paragraph.add_run(str(value)), 8.2, True, NAVY)
|
|
168
|
-
set_table_geometry(table, widths)
|
|
169
|
-
set_table_borders(table, "E2E8F0", "4")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def add_callout(doc, label, detail, fill=PALE_BLUE):
|
|
173
|
-
table = doc.add_table(rows=1, cols=1)
|
|
174
|
-
set_table_geometry(table, [TABLE_WIDTH_DXA])
|
|
175
|
-
set_table_borders(table, "9CC2E5", "7")
|
|
176
|
-
cell = table.cell(0, 0)
|
|
177
|
-
set_cell_shading(cell, fill)
|
|
178
|
-
paragraph = cell.paragraphs[0]
|
|
179
|
-
format_paragraph(paragraph, after=0, line=1.03)
|
|
180
|
-
set_run_font(paragraph.add_run(f"{label}:"), 8.8, True, NAVY)
|
|
181
|
-
set_run_font(paragraph.add_run(detail), 8.8, False, INK)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def add_quote_table(doc, config):
|
|
185
|
-
table = doc.add_table(rows=2, cols=3)
|
|
186
|
-
widths = [TABLE_WIDTH_DXA // 3] * 3
|
|
187
|
-
widths[-1] += TABLE_WIDTH_DXA - sum(widths)
|
|
188
|
-
headers = ["市场预估价(总价)", "吾码优惠报价(总价)", "开发周期"]
|
|
189
|
-
values = [config["market_price"], config["preferential_price"], config["development_cycle"]]
|
|
190
|
-
for index, header in enumerate(headers):
|
|
191
|
-
cell = table.cell(0, index)
|
|
192
|
-
set_cell_shading(cell, NAVY)
|
|
193
|
-
paragraph = cell.paragraphs[0]
|
|
194
|
-
format_paragraph(paragraph, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
195
|
-
set_run_font(paragraph.add_run(header), 8.7, True, WHITE)
|
|
196
|
-
for index, value in enumerate(values):
|
|
197
|
-
cell = table.cell(1, index)
|
|
198
|
-
set_cell_shading(cell, PALE_GOLD if index == 1 else PALE_GRAY)
|
|
199
|
-
paragraph = cell.paragraphs[0]
|
|
200
|
-
format_paragraph(paragraph, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
201
|
-
set_run_font(paragraph.add_run(str(value)), 12.0 if index == 1 else 10.5, True, GOLD if index == 1 else NAVY)
|
|
202
|
-
set_table_geometry(table, widths)
|
|
203
|
-
set_table_borders(table)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def normalize_config(raw):
|
|
207
|
-
required = [
|
|
208
|
-
"system_name", "project_goal", "solution_items", "technical_route",
|
|
209
|
-
"deliverables", "market_price", "preferential_price",
|
|
210
|
-
"development_cycle", "boundary_note",
|
|
211
|
-
]
|
|
212
|
-
missing = [name for name in required if not raw.get(name)]
|
|
213
|
-
if missing:
|
|
214
|
-
raise ValueError("Missing required fields: " + ", ".join(missing))
|
|
215
|
-
if not isinstance(raw["solution_items"], list) or not raw["solution_items"]:
|
|
216
|
-
raise ValueError("solution_items must be a non-empty list")
|
|
217
|
-
raw.setdefault("version", "v1.0.0")
|
|
218
|
-
raw.setdefault("date", date.today().isoformat())
|
|
219
|
-
raw.setdefault("validity_days", 15)
|
|
220
|
-
raw.setdefault("subtitle", "AI 深度融合 V8引擎 的低代码平台,官网:https://microi.net")
|
|
221
|
-
raw.setdefault("project_scope", "")
|
|
222
|
-
return raw
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def build_document(config, output):
|
|
226
|
-
doc = Document()
|
|
227
|
-
section = doc.sections[0]
|
|
228
|
-
section.page_width = Inches(8.27)
|
|
229
|
-
section.page_height = Inches(11.69)
|
|
230
|
-
section.top_margin = Inches(0.5)
|
|
231
|
-
section.bottom_margin = Inches(0.5)
|
|
232
|
-
section.left_margin = Inches(0.5)
|
|
233
|
-
section.right_margin = Inches(0.5)
|
|
234
|
-
section.header_distance = Inches(0.2)
|
|
235
|
-
section.footer_distance = Inches(0.2)
|
|
236
|
-
|
|
237
|
-
normal = doc.styles["Normal"]
|
|
238
|
-
normal.font.name = FONT
|
|
239
|
-
normal._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)
|
|
240
|
-
normal.font.size = Pt(9.2)
|
|
241
|
-
normal.font.color.rgb = RGBColor.from_string(INK)
|
|
242
|
-
|
|
243
|
-
title = f'Microi吾码 {config["system_name"]} 解决方案与报价'
|
|
244
|
-
add_paragraph(doc, title, size=20.0, bold=True, color=NAVY, after=1, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
245
|
-
add_paragraph(doc, config["subtitle"], size=9.3, color=MUTED, after=4, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
246
|
-
add_metadata_table(doc, config)
|
|
247
|
-
|
|
248
|
-
add_section_heading(doc, "一、项目目标")
|
|
249
|
-
add_callout(doc, "目标", config["project_goal"])
|
|
250
|
-
|
|
251
|
-
add_section_heading(doc, "二、解决方案")
|
|
252
|
-
for item in config["solution_items"]:
|
|
253
|
-
if isinstance(item, str):
|
|
254
|
-
add_bullet(doc, "方案", item)
|
|
255
|
-
else:
|
|
256
|
-
add_bullet(doc, item.get("label", "方案"), item.get("detail", ""))
|
|
257
|
-
|
|
258
|
-
add_section_heading(doc, "三、核心技术路线")
|
|
259
|
-
add_callout(doc, "链路", config["technical_route"], fill=PALE_GRAY)
|
|
260
|
-
|
|
261
|
-
add_section_heading(doc, "四、交付范围")
|
|
262
|
-
add_paragraph(doc, config["deliverables"], size=8.7, after=1, line=1.03)
|
|
263
|
-
if config["project_scope"]:
|
|
264
|
-
add_paragraph(doc, config["project_scope"], size=8.3, color=MUTED, after=1, line=1.0)
|
|
265
|
-
|
|
266
|
-
add_section_heading(doc, "五、项目总价与周期")
|
|
267
|
-
add_quote_table(doc, config)
|
|
268
|
-
|
|
269
|
-
add_section_heading(doc, "六、说明与边界")
|
|
270
|
-
add_paragraph(doc, config["boundary_note"], size=8.1, color=MUTED, after=0, line=1.0)
|
|
271
|
-
|
|
272
|
-
footer = section.footer.paragraphs[0]
|
|
273
|
-
format_paragraph(footer, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.RIGHT)
|
|
274
|
-
set_run_font(footer.add_run(f'Microi吾码 | {config["version"]} | {config["date"]} | https://microi.net'), 7.2, False, MUTED)
|
|
275
|
-
|
|
276
|
-
doc.core_properties.title = title
|
|
277
|
-
doc.core_properties.subject = "Microi吾码系统解决方案与报价"
|
|
278
|
-
doc.core_properties.author = "Microi吾码"
|
|
279
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
-
doc.save(output)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def main():
|
|
284
|
-
parser = argparse.ArgumentParser(description="Build a compact Microi solution quotation DOCX")
|
|
285
|
-
parser.add_argument("--input", required=True, help="UTF-8 JSON configuration file")
|
|
286
|
-
parser.add_argument("--output", required=True, help="Output DOCX path")
|
|
287
|
-
args = parser.parse_args()
|
|
288
|
-
with open(args.input, "r", encoding="utf-8") as handle:
|
|
289
|
-
config = normalize_config(json.load(handle))
|
|
290
|
-
output = Path(args.output).resolve()
|
|
291
|
-
build_document(config, output)
|
|
292
|
-
print(output)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if __name__ == "__main__":
|
|
296
|
-
main()
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from docx import Document
|
|
7
|
+
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
|
8
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
9
|
+
from docx.oxml import OxmlElement
|
|
10
|
+
from docx.oxml.ns import qn
|
|
11
|
+
from docx.shared import Inches, Pt, RGBColor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
NAVY = "17365D"
|
|
15
|
+
BLUE = "2F75B5"
|
|
16
|
+
PALE_BLUE = "EAF3F9"
|
|
17
|
+
PALE_GRAY = "F4F6F8"
|
|
18
|
+
PALE_GOLD = "FFF3D6"
|
|
19
|
+
GOLD = "B45309"
|
|
20
|
+
INK = "1F2937"
|
|
21
|
+
MUTED = "64748B"
|
|
22
|
+
WHITE = "FFFFFF"
|
|
23
|
+
BORDER = "CBD5E1"
|
|
24
|
+
FONT = "Microsoft YaHei"
|
|
25
|
+
PAGE_WIDTH_DXA = 11909
|
|
26
|
+
MARGIN_DXA = 720
|
|
27
|
+
TABLE_INDENT_DXA = 120
|
|
28
|
+
TABLE_WIDTH_DXA = PAGE_WIDTH_DXA - MARGIN_DXA * 2 - TABLE_INDENT_DXA
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def set_run_font(run, size=9.2, bold=False, color=INK):
|
|
32
|
+
run.font.name = FONT
|
|
33
|
+
rfonts = run._element.get_or_add_rPr().get_or_add_rFonts()
|
|
34
|
+
for key in ("w:eastAsia", "w:ascii", "w:hAnsi"):
|
|
35
|
+
rfonts.set(qn(key), FONT)
|
|
36
|
+
run.font.size = Pt(size)
|
|
37
|
+
run.font.bold = bold
|
|
38
|
+
run.font.color.rgb = RGBColor.from_string(color)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_paragraph(paragraph, before=0, after=0, line=1.05, align=None):
|
|
42
|
+
fmt = paragraph.paragraph_format
|
|
43
|
+
fmt.space_before = Pt(before)
|
|
44
|
+
fmt.space_after = Pt(after)
|
|
45
|
+
fmt.line_spacing = line
|
|
46
|
+
if align is not None:
|
|
47
|
+
paragraph.alignment = align
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_paragraph(doc, text, size=9.2, bold=False, color=INK, before=0, after=2, line=1.05, align=None):
|
|
51
|
+
paragraph = doc.add_paragraph()
|
|
52
|
+
format_paragraph(paragraph, before, after, line, align)
|
|
53
|
+
set_run_font(paragraph.add_run(text), size, bold, color)
|
|
54
|
+
return paragraph
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_cell_shading(cell, fill):
|
|
58
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
59
|
+
shd = tc_pr.find(qn("w:shd"))
|
|
60
|
+
if shd is None:
|
|
61
|
+
shd = OxmlElement("w:shd")
|
|
62
|
+
tc_pr.append(shd)
|
|
63
|
+
shd.set(qn("w:fill"), fill)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set_cell_margins(cell, top=70, start=110, bottom=70, end=110):
|
|
67
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
68
|
+
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
|
69
|
+
if tc_mar is None:
|
|
70
|
+
tc_mar = OxmlElement("w:tcMar")
|
|
71
|
+
tc_pr.append(tc_mar)
|
|
72
|
+
for name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
|
73
|
+
element = tc_mar.find(qn(f"w:{name}"))
|
|
74
|
+
if element is None:
|
|
75
|
+
element = OxmlElement(f"w:{name}")
|
|
76
|
+
tc_mar.append(element)
|
|
77
|
+
element.set(qn("w:w"), str(value))
|
|
78
|
+
element.set(qn("w:type"), "dxa")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def set_cell_width(cell, width):
|
|
82
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
83
|
+
tc_w = tc_pr.find(qn("w:tcW"))
|
|
84
|
+
if tc_w is None:
|
|
85
|
+
tc_w = OxmlElement("w:tcW")
|
|
86
|
+
tc_pr.append(tc_w)
|
|
87
|
+
tc_w.set(qn("w:w"), str(width))
|
|
88
|
+
tc_w.set(qn("w:type"), "dxa")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def set_table_geometry(table, widths, indent=TABLE_INDENT_DXA):
|
|
92
|
+
total = sum(widths)
|
|
93
|
+
table.autofit = False
|
|
94
|
+
table.alignment = WD_TABLE_ALIGNMENT.LEFT
|
|
95
|
+
tbl_pr = table._tbl.tblPr
|
|
96
|
+
tbl_w = tbl_pr.find(qn("w:tblW"))
|
|
97
|
+
if tbl_w is None:
|
|
98
|
+
tbl_w = OxmlElement("w:tblW")
|
|
99
|
+
tbl_pr.append(tbl_w)
|
|
100
|
+
tbl_w.set(qn("w:w"), str(total))
|
|
101
|
+
tbl_w.set(qn("w:type"), "dxa")
|
|
102
|
+
tbl_ind = tbl_pr.find(qn("w:tblInd"))
|
|
103
|
+
if tbl_ind is None:
|
|
104
|
+
tbl_ind = OxmlElement("w:tblInd")
|
|
105
|
+
tbl_pr.append(tbl_ind)
|
|
106
|
+
tbl_ind.set(qn("w:w"), str(indent))
|
|
107
|
+
tbl_ind.set(qn("w:type"), "dxa")
|
|
108
|
+
grid = table._tbl.tblGrid
|
|
109
|
+
for child in list(grid):
|
|
110
|
+
grid.remove(child)
|
|
111
|
+
for width in widths:
|
|
112
|
+
col = OxmlElement("w:gridCol")
|
|
113
|
+
col.set(qn("w:w"), str(width))
|
|
114
|
+
grid.append(col)
|
|
115
|
+
for row in table.rows:
|
|
116
|
+
for index, cell in enumerate(row.cells):
|
|
117
|
+
set_cell_width(cell, widths[index])
|
|
118
|
+
set_cell_margins(cell)
|
|
119
|
+
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def set_table_borders(table, color=BORDER, size="6"):
|
|
123
|
+
tbl_pr = table._tbl.tblPr
|
|
124
|
+
borders = tbl_pr.first_child_found_in("w:tblBorders")
|
|
125
|
+
if borders is None:
|
|
126
|
+
borders = OxmlElement("w:tblBorders")
|
|
127
|
+
tbl_pr.append(borders)
|
|
128
|
+
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
|
129
|
+
tag = f"w:{edge}"
|
|
130
|
+
element = borders.find(qn(tag))
|
|
131
|
+
if element is None:
|
|
132
|
+
element = OxmlElement(tag)
|
|
133
|
+
borders.append(element)
|
|
134
|
+
element.set(qn("w:val"), "single")
|
|
135
|
+
element.set(qn("w:sz"), size)
|
|
136
|
+
element.set(qn("w:space"), "0")
|
|
137
|
+
element.set(qn("w:color"), color)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def add_section_heading(doc, text):
|
|
141
|
+
return add_paragraph(doc, text, size=10.8, bold=True, color=BLUE, before=4, after=1, line=1.0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def add_bullet(doc, label, detail):
|
|
145
|
+
paragraph = doc.add_paragraph(style="List Bullet")
|
|
146
|
+
format_paragraph(paragraph, before=0, after=1, line=1.03)
|
|
147
|
+
set_run_font(paragraph.add_run(f"{label}:"), 8.8, True, NAVY)
|
|
148
|
+
set_run_font(paragraph.add_run(detail), 8.8, False, INK)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add_metadata_table(doc, config):
|
|
152
|
+
table = doc.add_table(rows=1, cols=4)
|
|
153
|
+
widths = [TABLE_WIDTH_DXA // 4] * 4
|
|
154
|
+
widths[-1] += TABLE_WIDTH_DXA - sum(widths)
|
|
155
|
+
values = [
|
|
156
|
+
("版本", config["version"]),
|
|
157
|
+
("日期", config["date"]),
|
|
158
|
+
("开发周期", config["development_cycle"]),
|
|
159
|
+
("报价有效期", f'{config["validity_days"]} 天'),
|
|
160
|
+
]
|
|
161
|
+
for index, (label, value) in enumerate(values):
|
|
162
|
+
cell = table.cell(0, index)
|
|
163
|
+
set_cell_shading(cell, PALE_GRAY)
|
|
164
|
+
paragraph = cell.paragraphs[0]
|
|
165
|
+
format_paragraph(paragraph, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
166
|
+
set_run_font(paragraph.add_run(f"{label}:"), 8.0, True, MUTED)
|
|
167
|
+
set_run_font(paragraph.add_run(str(value)), 8.2, True, NAVY)
|
|
168
|
+
set_table_geometry(table, widths)
|
|
169
|
+
set_table_borders(table, "E2E8F0", "4")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def add_callout(doc, label, detail, fill=PALE_BLUE):
|
|
173
|
+
table = doc.add_table(rows=1, cols=1)
|
|
174
|
+
set_table_geometry(table, [TABLE_WIDTH_DXA])
|
|
175
|
+
set_table_borders(table, "9CC2E5", "7")
|
|
176
|
+
cell = table.cell(0, 0)
|
|
177
|
+
set_cell_shading(cell, fill)
|
|
178
|
+
paragraph = cell.paragraphs[0]
|
|
179
|
+
format_paragraph(paragraph, after=0, line=1.03)
|
|
180
|
+
set_run_font(paragraph.add_run(f"{label}:"), 8.8, True, NAVY)
|
|
181
|
+
set_run_font(paragraph.add_run(detail), 8.8, False, INK)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def add_quote_table(doc, config):
|
|
185
|
+
table = doc.add_table(rows=2, cols=3)
|
|
186
|
+
widths = [TABLE_WIDTH_DXA // 3] * 3
|
|
187
|
+
widths[-1] += TABLE_WIDTH_DXA - sum(widths)
|
|
188
|
+
headers = ["市场预估价(总价)", "吾码优惠报价(总价)", "开发周期"]
|
|
189
|
+
values = [config["market_price"], config["preferential_price"], config["development_cycle"]]
|
|
190
|
+
for index, header in enumerate(headers):
|
|
191
|
+
cell = table.cell(0, index)
|
|
192
|
+
set_cell_shading(cell, NAVY)
|
|
193
|
+
paragraph = cell.paragraphs[0]
|
|
194
|
+
format_paragraph(paragraph, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
195
|
+
set_run_font(paragraph.add_run(header), 8.7, True, WHITE)
|
|
196
|
+
for index, value in enumerate(values):
|
|
197
|
+
cell = table.cell(1, index)
|
|
198
|
+
set_cell_shading(cell, PALE_GOLD if index == 1 else PALE_GRAY)
|
|
199
|
+
paragraph = cell.paragraphs[0]
|
|
200
|
+
format_paragraph(paragraph, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
201
|
+
set_run_font(paragraph.add_run(str(value)), 12.0 if index == 1 else 10.5, True, GOLD if index == 1 else NAVY)
|
|
202
|
+
set_table_geometry(table, widths)
|
|
203
|
+
set_table_borders(table)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def normalize_config(raw):
|
|
207
|
+
required = [
|
|
208
|
+
"system_name", "project_goal", "solution_items", "technical_route",
|
|
209
|
+
"deliverables", "market_price", "preferential_price",
|
|
210
|
+
"development_cycle", "boundary_note",
|
|
211
|
+
]
|
|
212
|
+
missing = [name for name in required if not raw.get(name)]
|
|
213
|
+
if missing:
|
|
214
|
+
raise ValueError("Missing required fields: " + ", ".join(missing))
|
|
215
|
+
if not isinstance(raw["solution_items"], list) or not raw["solution_items"]:
|
|
216
|
+
raise ValueError("solution_items must be a non-empty list")
|
|
217
|
+
raw.setdefault("version", "v1.0.0")
|
|
218
|
+
raw.setdefault("date", date.today().isoformat())
|
|
219
|
+
raw.setdefault("validity_days", 15)
|
|
220
|
+
raw.setdefault("subtitle", "AI 深度融合 V8引擎 的低代码平台,官网:https://microi.net")
|
|
221
|
+
raw.setdefault("project_scope", "")
|
|
222
|
+
return raw
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def build_document(config, output):
|
|
226
|
+
doc = Document()
|
|
227
|
+
section = doc.sections[0]
|
|
228
|
+
section.page_width = Inches(8.27)
|
|
229
|
+
section.page_height = Inches(11.69)
|
|
230
|
+
section.top_margin = Inches(0.5)
|
|
231
|
+
section.bottom_margin = Inches(0.5)
|
|
232
|
+
section.left_margin = Inches(0.5)
|
|
233
|
+
section.right_margin = Inches(0.5)
|
|
234
|
+
section.header_distance = Inches(0.2)
|
|
235
|
+
section.footer_distance = Inches(0.2)
|
|
236
|
+
|
|
237
|
+
normal = doc.styles["Normal"]
|
|
238
|
+
normal.font.name = FONT
|
|
239
|
+
normal._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)
|
|
240
|
+
normal.font.size = Pt(9.2)
|
|
241
|
+
normal.font.color.rgb = RGBColor.from_string(INK)
|
|
242
|
+
|
|
243
|
+
title = f'Microi吾码 {config["system_name"]} 解决方案与报价'
|
|
244
|
+
add_paragraph(doc, title, size=20.0, bold=True, color=NAVY, after=1, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
245
|
+
add_paragraph(doc, config["subtitle"], size=9.3, color=MUTED, after=4, line=1.0, align=WD_ALIGN_PARAGRAPH.CENTER)
|
|
246
|
+
add_metadata_table(doc, config)
|
|
247
|
+
|
|
248
|
+
add_section_heading(doc, "一、项目目标")
|
|
249
|
+
add_callout(doc, "目标", config["project_goal"])
|
|
250
|
+
|
|
251
|
+
add_section_heading(doc, "二、解决方案")
|
|
252
|
+
for item in config["solution_items"]:
|
|
253
|
+
if isinstance(item, str):
|
|
254
|
+
add_bullet(doc, "方案", item)
|
|
255
|
+
else:
|
|
256
|
+
add_bullet(doc, item.get("label", "方案"), item.get("detail", ""))
|
|
257
|
+
|
|
258
|
+
add_section_heading(doc, "三、核心技术路线")
|
|
259
|
+
add_callout(doc, "链路", config["technical_route"], fill=PALE_GRAY)
|
|
260
|
+
|
|
261
|
+
add_section_heading(doc, "四、交付范围")
|
|
262
|
+
add_paragraph(doc, config["deliverables"], size=8.7, after=1, line=1.03)
|
|
263
|
+
if config["project_scope"]:
|
|
264
|
+
add_paragraph(doc, config["project_scope"], size=8.3, color=MUTED, after=1, line=1.0)
|
|
265
|
+
|
|
266
|
+
add_section_heading(doc, "五、项目总价与周期")
|
|
267
|
+
add_quote_table(doc, config)
|
|
268
|
+
|
|
269
|
+
add_section_heading(doc, "六、说明与边界")
|
|
270
|
+
add_paragraph(doc, config["boundary_note"], size=8.1, color=MUTED, after=0, line=1.0)
|
|
271
|
+
|
|
272
|
+
footer = section.footer.paragraphs[0]
|
|
273
|
+
format_paragraph(footer, after=0, line=1.0, align=WD_ALIGN_PARAGRAPH.RIGHT)
|
|
274
|
+
set_run_font(footer.add_run(f'Microi吾码 | {config["version"]} | {config["date"]} | https://microi.net'), 7.2, False, MUTED)
|
|
275
|
+
|
|
276
|
+
doc.core_properties.title = title
|
|
277
|
+
doc.core_properties.subject = "Microi吾码系统解决方案与报价"
|
|
278
|
+
doc.core_properties.author = "Microi吾码"
|
|
279
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
doc.save(output)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main():
|
|
284
|
+
parser = argparse.ArgumentParser(description="Build a compact Microi solution quotation DOCX")
|
|
285
|
+
parser.add_argument("--input", required=True, help="UTF-8 JSON configuration file")
|
|
286
|
+
parser.add_argument("--output", required=True, help="Output DOCX path")
|
|
287
|
+
args = parser.parse_args()
|
|
288
|
+
with open(args.input, "r", encoding="utf-8") as handle:
|
|
289
|
+
config = normalize_config(json.load(handle))
|
|
290
|
+
output = Path(args.output).resolve()
|
|
291
|
+
build_document(config, output)
|
|
292
|
+
print(output)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|