@hupan56/wlkj 2.7.12 → 3.0.0

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.
Files changed (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
3
9
  """
4
10
  QODER EVA - PRD/原型 自动评估 (快且准的"准"侧自动指标)
5
11
 
@@ -14,7 +20,7 @@ Usage:
14
20
  A3 模板完整 (30分): PRD 包含团队模板必需章节的比例
15
21
  -> 保证团队章程的模板结构不丢
16
22
 
17
- 结果追加到 .qoder/learning/eval-history.jsonl (本机), 供 /wl-status 趋势展示。
23
+ 结果追加到 data/learning/eval-history.jsonl (本机), 供 /wl-status 趋势展示。
18
24
  Exit: 0 = 总分>=80, 1 = 不达标 (发布前应修复)
19
25
  """
20
26
 
@@ -36,7 +42,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
36
42
  BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
37
43
  INDEX_DIR = os.path.join(BASE, 'data', 'index')
38
44
  TPL_DIR = os.path.join(BASE, '.qoder', 'templates')
39
- HISTORY = os.path.join(BASE, '.qoder', 'learning', 'eval-history.jsonl')
45
+ HISTORY = os.path.join(BASE, 'data', 'learning', 'eval-history.jsonl')
40
46
 
41
47
  # 完整模板必需章节 (prd-full-template.md) / Mini 模板必需章节
42
48
  FULL_SECTIONS = ['版本信息', '变更日志', '名词解释', '需求背景', '需求来源', '产品现状',
@@ -185,10 +191,17 @@ def score_reality(prd_text):
185
191
  if checked == 0:
186
192
  return 1.0, '未引用具体字段/API (零星文案类需求正常)', []
187
193
  ratio = hits / checked
194
+ # 图谱覆盖度感知: 防止"认真写 API 引用但图谱没覆盖"比"啥都不写"分还低。
195
+ kg_api_count = len(api_paths)
196
+ if kg_api_count < 10 and ratio < 0.5 and hits == 0:
197
+ ratio = max(ratio, 0.5)
198
+ note_suffix = '(图谱API稀疏, 已软保底)'
199
+ else:
200
+ note_suffix = ''
188
201
  db_note = (',其中 %d 个由数据库层确认' % db_bonus) if db_bonus else ''
189
202
  layer_note = ('(双层: 代码+数据库)' if db_cols else '(代码层)')
190
- return ratio, '{}/{} 个字段/API 引用可在图谱中找到{}{}'.format(
191
- hits, checked, layer_note, db_note), misses[:8]
203
+ return ratio, '{}/{} 个字段/API 引用可在图谱中找到{}{}{}'.format(
204
+ hits, checked, layer_note, db_note, note_suffix), misses[:8]
192
205
 
193
206
 
194
207
  # ============================================================
@@ -257,6 +270,13 @@ def score_template(prd_text):
257
270
  # ============================================================
258
271
 
259
272
  def main():
273
+ """v3.0: 接入 Result + contract 校验 (让设计模式真落地)。
274
+
275
+ 改动:
276
+ 1. 评估前先 validate_contract (软校验, 无契约头降级不阻塞)
277
+ 2. 用 Result 表达三级结果 (PASS/WARN/FAIL), 替代裸 return 0/1
278
+ 3. graceful_error 装饰器统一输出
279
+ """
260
280
  if len(sys.argv) < 2:
261
281
  print(__doc__)
262
282
  return 1
@@ -264,9 +284,35 @@ def main():
264
284
  html_path = sys.argv[2] if len(sys.argv) > 2 else None
265
285
 
266
286
  if not os.path.isfile(prd_path):
267
- print('PRD 不存在: ' + prd_path)
287
+ print('[FAIL] PRD 不存在: ' + prd_path)
268
288
  return 1
269
289
 
290
+ # v3.0 接入: 角色边界校验 (guard.py, 软提示不阻塞)
291
+ # eval_prd 是 PRD 发布的必经质量门, 在这里检查"当前角色能否产出 prd"
292
+ # 越权 → 打印提示但不 return(降级, 让用户自己决定是否继续)
293
+ try:
294
+ from common.guard import check_produce
295
+ gr = check_produce('prd')
296
+ if gr.is_warn:
297
+ print(gr.msg)
298
+ if gr.hint:
299
+ print(' -> ' + gr.hint)
300
+ except Exception:
301
+ pass # guard 不可用不影响评估
302
+
303
+ # v3.0 接入: 契约校验 (软, 无@contract头则降级不阻塞)
304
+ try:
305
+ from common.contract import validate_contract
306
+ cr = validate_contract(prd_path)
307
+ if cr.is_warn:
308
+ print(cr.msg) # WARN: 无契约头或字段警告, 提示但不阻塞
309
+ elif cr.ok and cr.code not in ('no_contract', 'no_schema', 'ok'):
310
+ print(cr.msg)
311
+ if cr.hint:
312
+ print(' -> ' + cr.hint)
313
+ except Exception:
314
+ pass # 契约校验失败不影响评估
315
+
270
316
  # 用程序化 API 评估 (重构后 main 只是 CLI 包装)
271
317
  from common.eval_api import evaluate, format_report
272
318
  try:
@@ -278,10 +324,23 @@ def main():
278
324
  # 打印人类可读报告
279
325
  print(format_report(result))
280
326
 
281
- # 详细 misses (补充 format_report 之外的细节)
282
- if result.get('msg2') and '非真源颜色' in str(result.get('msg2', '')):
283
- # msg2 已含信息, 此处可补 unknown 列表 —— 但 evaluate 已合并进 misses
284
- pass
327
+ # v3.0 接入: 用 Result 表达三级 (PASS ≥80 / WARN 60-80 / FAIL <60)
328
+ # 现状是二值 passed, 这里加 WARN 中间档, 让"差一点的 PRD"能带警告发布
329
+ from common.result import Result, CODE
330
+ pct = result.get('pct', 0)
331
+ if result['passed']:
332
+ verdict = Result.success(msg='✓ PASS ({}%)'.format(pct), data=result)
333
+ elif pct >= 60:
334
+ verdict = Result.warning(
335
+ '⚠ WARN ({}%): 低于80但有内容, 可带警告发布'.format(pct),
336
+ code=CODE.BELOW_THRESH, data=result,
337
+ )
338
+ else:
339
+ verdict = Result.failure(
340
+ '✗ FAIL ({}%): 低于60, 需修复'.format(pct),
341
+ code=CODE.BELOW_THRESH,
342
+ hint='检查字段锚定(A1)和模板完整(A3)',
343
+ )
285
344
 
286
345
  # 记录历史 (原子追加, 防并发损坏)
287
346
  try:
@@ -295,11 +354,12 @@ def main():
295
354
  'a3_template': result['r3'],
296
355
  'score_pct': result['pct'],
297
356
  'passed': result['passed'],
357
+ 'verdict': verdict.code, # v3.0: 记录三级判定
298
358
  }, timeout=10)
299
359
  except Exception:
300
360
  pass # 历史记录失败不影响评估结果
301
361
 
302
- # 埋点: PRD 评估结果反馈给 learning 引擎 (prd_accepted / prd_rejected)
362
+ # 埋点: PRD 评估结果反馈给 learning 引擎
303
363
  try:
304
364
  from learn import record_feedback
305
365
  event = 'prd_accepted' if result['passed'] else 'prd_rejected'
@@ -311,7 +371,9 @@ def main():
311
371
  except Exception:
312
372
  pass # 埋点失败不影响评估
313
373
 
314
- return 0 if result['passed'] else 1
374
+ # exit code: PASS=0, WARN=0(可继续), FAIL=1
375
+ # (WARN 不阻塞发布, 与方案"eval_prd 三级门禁"一致)
376
+ return 0 if verdict.ok else 1
315
377
 
316
378
 
317
379
  if __name__ == '__main__':
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
3
9
  """
4
10
  export.py - 集成导出工具 (阶段 C)
5
11
 
@@ -470,6 +476,13 @@ def main():
470
476
  p_cg = sub.add_parser('callgraph', help='Java 调用图')
471
477
  p_cg.add_argument('--class', dest='class_name', help='查询指定类的依赖')
472
478
 
479
+ p_dx = sub.add_parser('docx', help='PRD导出Word(M5)')
480
+ p_dx.add_argument('prd', help='PRD md文件')
481
+ p_dx.add_argument('-o', '--output')
482
+ p_pf = sub.add_parser('pdf', help='PRD导出HTML(浏览器打印为PDF)')
483
+ p_pf.add_argument('prd', help='PRD md文件')
484
+ p_pf.add_argument('-o', '--output')
485
+
473
486
  args = parser.parse_args()
474
487
  if not args.format:
475
488
  parser.print_help()
@@ -482,6 +495,56 @@ def main():
482
495
  elif args.format == 'callgraph':
483
496
  return export_callgraph(class_name=args.class_name)
484
497
 
498
+ elif args.format == 'docx':
499
+ return export_docx(args.prd, args.output)
500
+ elif args.format == 'pdf':
501
+ return export_pdf(args.prd, args.output)
502
+
503
+
504
+ def export_docx(prd_path, output=None):
505
+ """PRD markdown -> Word docx (M5)。用 python-docx, 纯Python不依赖pandoc。"""
506
+ if not os.path.isfile(prd_path):
507
+ print('PRD 不存在: %s' % prd_path); return 1
508
+ try:
509
+ from docx import Document
510
+ except ImportError:
511
+ print('python-docx 未装。pip install python-docx'); return 1
512
+ doc = Document()
513
+ with open(prd_path, encoding='utf-8') as f:
514
+ for line in f:
515
+ ln = line.rstrip('\n'); s = ln.strip()
516
+ if s.startswith('# '): doc.add_heading(s[2:], level=1)
517
+ elif s.startswith('## '): doc.add_heading(s[3:], level=2)
518
+ elif s.startswith('### '): doc.add_heading(s[4:], level=3)
519
+ elif s.startswith('- ') or s.startswith('* '): doc.add_paragraph(s[2:], style='List Bullet')
520
+ elif s.startswith('<!--'): pass
521
+ elif s == '': pass
522
+ else: doc.add_paragraph(s)
523
+ if not output: output = os.path.splitext(prd_path)[0] + '.docx'
524
+ doc.save(output)
525
+ print('OK Word: %s' % output); return 0
526
+
527
+
528
+ def export_pdf(prd_path, output=None):
529
+ """PRD markdown -> HTML (浏览器Ctrl+P另存PDF) (M5)。避免wkhtmltopdf重依赖。"""
530
+ if not os.path.isfile(prd_path):
531
+ print('PRD 不存在: %s' % prd_path); return 1
532
+ try:
533
+ import markdown as md
534
+ except ImportError:
535
+ print('markdown 未装。pip install markdown'); return 1
536
+ with open(prd_path, encoding='utf-8') as f:
537
+ body = md.markdown(f.read(), extensions=['tables', 'fenced_code'])
538
+ html = ('<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><style>'
539
+ 'body{font-family:"Microsoft YaHei",sans-serif;max-width:800px;margin:40px auto;line-height:1.6}'
540
+ 'h1{border-bottom:2px solid #333}h2{border-bottom:1px solid #999;margin-top:24px}'
541
+ 'table{border-collapse:collapse;width:100%}th,td{border:1px solid #ccc;padding:6px}th{background:#f5f5f5}'
542
+ '@media print{body{margin:20px}}</style></head><body>' + body + '</body></html>')
543
+ if not output: output = os.path.splitext(prd_path)[0] + '.html'
544
+ with open(output, 'w', encoding='utf-8') as f:
545
+ f.write(html)
546
+ print('OK HTML: %s (浏览器打开 Ctrl+P 另存PDF)' % output); return 0
547
+
485
548
 
486
549
  if __name__ == '__main__':
487
550
  sys.exit(main())
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
3
9
  """
4
10
  QODER Fill Prototype - 用真实数据自动预填原型模板 (80%完成)
5
11