@microi.net/cli 5.9.3 → 5.9.4
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 +6 -6
- 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 +81 -81
- package/scripts/mcp-trae-windows-launcher.cmd +21 -21
- package/scripts/microi-cli-mcp.js +7 -7
- package/scripts/microi-codex-broker.js +466 -466
- package/scripts/microi-codex-router.js +634 -634
- package/scripts/microi-skills.meta.json +230 -230
- package/skills/.microi-skills-version.json +2 -2
- 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-installer/agents/openai.yaml +7 -7
- package/skills/microi-db-schema/agents/openai.yaml +4 -4
- package/skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +894 -894
- package/skills/microi-microservice/SKILL.md +6 -4
- package/skills/microi-microservice/references/runtime-delivery.md +9 -5
- 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.v8.js +1921 -1921
- package/skills/ocr-engine/agents/openai.yaml +4 -4
- package/skills/page-engine/examples/compact-dashboard.json +1444 -1444
- 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-image-processing/agents/openai.yaml +4 -4
- package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +203 -203
- package/skills/v8-tcp-integration/agents/openai.yaml +4 -4
- package/skills/vision-engine/agents/openai.yaml +4 -4
|
@@ -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()
|