codebee 0.1.101 → 0.1.103

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 (134) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -21
  3. package/README.md +3 -6
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/acp_client.py +457 -426
  13. package/app/core/aiflavor.py +197 -197
  14. package/app/core/analytics.py +38 -38
  15. package/app/core/artifacts.py +148 -148
  16. package/app/core/assets.py +110 -110
  17. package/app/core/attachments.py +623 -623
  18. package/app/core/audit.py +30 -30
  19. package/app/core/backend_profiles.py +188 -188
  20. package/app/core/backup.py +470 -470
  21. package/app/core/beekeeper.py +183 -183
  22. package/app/core/benchstore.py +102 -102
  23. package/app/core/board.py +21 -17
  24. package/app/core/bookmeta.py +1036 -1036
  25. package/app/core/bookmeta_catalog.py +180 -180
  26. package/app/core/branching.py +137 -137
  27. package/app/core/builtin_agent.py +13 -4
  28. package/app/core/capability.py +144 -144
  29. package/app/core/catalog.py +28 -6
  30. package/app/core/chaptersafe.py +32 -32
  31. package/app/core/checkpoints.py +117 -31
  32. package/app/core/cleanup.py +303 -303
  33. package/app/core/command_auth.py +302 -302
  34. package/app/core/compaction.py +201 -201
  35. package/app/core/contracts.py +255 -255
  36. package/app/core/covergen.py +356 -356
  37. package/app/core/defectretro.py +43 -43
  38. package/app/core/diagnostics.py +114 -114
  39. package/app/core/dispatch.py +1 -1
  40. package/app/core/dispatch_log.py +128 -128
  41. package/app/core/env_scrub.py +85 -85
  42. package/app/core/errorlog.py +137 -137
  43. package/app/core/eval_matrix.py +91 -82
  44. package/app/core/evaluation.py +174 -174
  45. package/app/core/failure_notify.py +290 -290
  46. package/app/core/flow_graph.py +33 -33
  47. package/app/core/goal_service.py +167 -167
  48. package/app/core/health.py +389 -389
  49. package/app/core/history.py +38 -38
  50. package/app/core/hooks.py +162 -162
  51. package/app/core/knowledge_pipeline.py +86 -86
  52. package/app/core/manager.py +2385 -2385
  53. package/app/core/mcp_client.py +7 -2
  54. package/app/core/mocks.py +64 -64
  55. package/app/core/notify.py +201 -201
  56. package/app/core/novel_quality.py +181 -181
  57. package/app/core/openapi.py +217 -209
  58. package/app/core/operations.py +228 -228
  59. package/app/core/paihang.py +145 -145
  60. package/app/core/pipeline.py +6 -3
  61. package/app/core/policy.py +9 -0
  62. package/app/core/portguard.py +111 -111
  63. package/app/core/presence.py +77 -77
  64. package/app/core/preview.py +293 -293
  65. package/app/core/project_memory.py +206 -206
  66. package/app/core/prompt_guard.py +78 -78
  67. package/app/core/publish/__init__.py +7 -7
  68. package/app/core/publish/auto.py +659 -562
  69. package/app/core/publish/chaprows.py +138 -138
  70. package/app/core/publish/fanqie.py +16 -10
  71. package/app/core/publish/flow.py +29 -1
  72. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  73. package/app/core/publish/manager.py +1185 -1177
  74. package/app/core/publish/qimao.py +193 -193
  75. package/app/core/publish/volumes.py +108 -108
  76. package/app/core/publish/ws.py +139 -139
  77. package/app/core/quality_gate.py +201 -201
  78. package/app/core/redact.py +38 -38
  79. package/app/core/repeat_guard.py +129 -129
  80. package/app/core/retrieval.py +137 -137
  81. package/app/core/sandbox_runtime.py +95 -0
  82. package/app/core/selfupdate.py +384 -384
  83. package/app/core/session_log.py +161 -161
  84. package/app/core/sessions.py +312 -312
  85. package/app/core/settings.py +215 -215
  86. package/app/core/settings_schema.py +282 -282
  87. package/app/core/share_page.py +114 -114
  88. package/app/core/skill_scan.py +139 -139
  89. package/app/core/skillpacks/market/character-bible.md +66 -66
  90. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  91. package/app/core/skillpacks/market/git-workflow.md +57 -57
  92. package/app/core/skillpacks/market/release-notes.md +72 -72
  93. package/app/core/skillpacks/market/weekly-report.md +71 -71
  94. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  95. package/app/core/step_runner.py +132 -132
  96. package/app/core/store.py +13 -3
  97. package/app/core/story_tracking.py +275 -275
  98. package/app/core/task_compile.py +199 -199
  99. package/app/core/telemetry.py +292 -292
  100. package/app/core/tlsctx.py +64 -64
  101. package/app/core/token_meter.py +147 -147
  102. package/app/core/tool_outputs.py +107 -107
  103. package/app/core/tracing.py +207 -207
  104. package/app/core/upstream.py +41 -41
  105. package/app/core/usage.py +1038 -1038
  106. package/app/core/volumes.py +389 -389
  107. package/app/core/webhooks.py +89 -89
  108. package/app/core/wxdigest.py +865 -865
  109. package/app/core/zentao.py +2692 -2692
  110. package/app/main.py +1 -0
  111. package/app/mcp_server.py +244 -244
  112. package/app/output/_createbook_body.json +1 -0
  113. package/app/output/_probe_fq_click.py +50 -0
  114. package/app/output/_probe_fq_form.py +42 -0
  115. package/app/output/_probe_fq_modal.py +55 -0
  116. package/app/output/_probe_fq_points.py +71 -0
  117. package/app/output/_probe_fq_submit.py +34 -0
  118. package/app/output/_probe_fq_watch.py +62 -0
  119. package/app/output/workbench-clean-preview.jpg +0 -0
  120. package/app/output/workbench-reference-render.png +0 -0
  121. package/app/output/workbench-shell.png +0 -0
  122. package/app/pick_dialog.py +110 -110
  123. package/app/ui/app.js +165 -29
  124. package/app/ui/artdiff.js +74 -74
  125. package/app/ui/hive3d.js +491 -223
  126. package/app/ui/i18n.js +18 -1
  127. package/app/ui/icons/bee.svg +79 -79
  128. package/app/ui/index.html +24 -4
  129. package/app/ui/manifest.json +16 -16
  130. package/app/ui/qrcode.js +2297 -2297
  131. package/app/ui/style.css +548 -18
  132. package/bin/tutti.js +147 -147
  133. package/package.json +1 -1
  134. package/tools/wx_reader.py +261 -261
@@ -1,201 +1,201 @@
1
- # -*- coding: utf-8 -*-
2
- """Deterministic quality and release gates for generated fiction.
3
-
4
- LLM reviewers provide judgements; this module decides whether the evidence is
5
- valid enough to support a release action. It deliberately treats missing,
6
- malformed, out-of-range and non-consensus scores as unevaluated.
7
- """
8
- from __future__ import annotations
9
-
10
- import math
11
- import re
12
- from pathlib import Path
13
-
14
-
15
- MIN_REVIEWERS = 2
16
- DEFAULT_MAX_SPREAD = 2.0
17
- _CHAPTER_RE = re.compile(r"^chapter-(\d{1,4})\.(?:md|txt)$", re.I)
18
-
19
-
20
- def normalize_scores(raw, dimensions):
21
- """Return (scores, errors); incomplete scores are invalid as a set."""
22
- raw = raw if isinstance(raw, dict) else {}
23
- scores, errors = {}, {}
24
- for dim in dimensions or []:
25
- value = raw.get(dim)
26
- try:
27
- number = float(value)
28
- except (TypeError, ValueError):
29
- errors[dim] = "非数字"
30
- continue
31
- if not math.isfinite(number) or number < 1.0 or number > 10.0:
32
- errors[dim] = "必须在 1-10 之间"
33
- continue
34
- scores[dim] = round(number, 2)
35
- if errors or len(scores) != len(list(dimensions or [])):
36
- return {}, errors or {str(d): "缺失" for d in dimensions or [] if d not in scores}
37
- return scores, {}
38
-
39
-
40
- def aggregate_reviews(reviews, dimensions, threshold=7.0,
41
- min_reviewers=MIN_REVIEWERS,
42
- max_spread=DEFAULT_MAX_SPREAD):
43
- """Validate reviewers, calculate means, and require reviewer consensus."""
44
- dims = list(dimensions or [])
45
- valid = []
46
- invalid = []
47
- seen = set()
48
- for idx, review in enumerate(reviews or []):
49
- review = review if isinstance(review, dict) else {}
50
- reviewer_id = str(review.get("id") or "reviewer-%d" % idx)
51
- if reviewer_id in seen:
52
- continue
53
- seen.add(reviewer_id)
54
- scores, errors = normalize_scores(review.get("scores"), dims)
55
- if errors:
56
- invalid.append({"id": reviewer_id, "errors": errors})
57
- else:
58
- valid.append({"id": reviewer_id, "scores": scores})
59
-
60
- reasons = []
61
- eligible = len(valid) >= int(min_reviewers)
62
- if not eligible:
63
- reasons.append("至少需要 %d 名有效评审,当前 %d 名" %
64
- (int(min_reviewers), len(valid)))
65
- means = {}
66
- spreads = {}
67
- for dim in dims:
68
- values = [r["scores"][dim] for r in valid]
69
- if values:
70
- means[dim] = round(sum(values) / len(values), 1)
71
- spreads[dim] = round(max(values) - min(values), 1)
72
-
73
- consensus = bool(eligible and means and
74
- all(spreads.get(dim, float("inf")) <= float(max_spread)
75
- for dim in dims))
76
- if eligible and not consensus:
77
- bad = [dim for dim in dims if spreads.get(dim, float("inf")) > float(max_spread)]
78
- reasons.append("评审分歧超过 %.1f 分:%s" % (float(max_spread), "、".join(bad)))
79
- passed = bool(consensus and means and
80
- all(value >= float(threshold) for value in means.values()))
81
- if consensus and not passed:
82
- reasons.append("综合分存在低于 %.1f 的维度" % float(threshold))
83
- return {
84
- "eligible": eligible,
85
- "consensus": consensus,
86
- "passed": passed,
87
- "means": means,
88
- "spreads": spreads,
89
- "valid_reviewers": valid,
90
- "invalid_reviewers": invalid,
91
- "reviewer_count": len(valid),
92
- "reasons": reasons,
93
- }
94
-
95
-
96
- def local_version(workdir):
97
- """Return the local manuscript version used for a release decision."""
98
- root = Path(workdir or "")
99
- rows = []
100
- if root.is_dir():
101
- for path in root.iterdir():
102
- match = _CHAPTER_RE.match(path.name)
103
- if not match or not path.is_file():
104
- continue
105
- try:
106
- text = path.read_text(encoding="utf-8", errors="replace")
107
- except OSError:
108
- continue
109
- rows.append((int(match.group(1)), len(re.sub(r"\s", "", text))))
110
- return {
111
- "local_total": len(rows),
112
- "local_latest": max((n for n, _ in rows), default=0),
113
- "local_words": sum(words for _, words in rows),
114
- }
115
-
116
-
117
- def evaluate_release(verdict, action="publish", target_chapter=None,
118
- platform_version=None, force=False,
119
- force_confirmed=False, force_reason=""):
120
- """Decide whether a publish/signing action is allowed.
121
-
122
- ``continue`` is intentionally separate: writers may continue improving an
123
- unready manuscript, while publishing and signing remain gated.
124
- """
125
- verdict = verdict if isinstance(verdict, dict) else {}
126
- action = str(action or "publish").strip().lower()
127
- if action == "continue":
128
- return {"allowed": True, "status": "continue_only", "forced": False,
129
- "blockers": [], "warnings": []}
130
- if action == "draft":
131
- # 存草稿不是发布:内容不公开展示(读者不可见),正式提交前用户还会
132
- # 在平台上逐章过目——质量闸只在「发布/签约」这类上线动作上拦,
133
- # 拦草稿只会把「先落平台再人工把关」的通路堵死(2026-10-09 全部
134
- # 发草稿需求:质量未达标的 53 章先存草稿,发布仍受本闸约束)。
135
- return {"allowed": True, "status": "draft_only", "forced": False,
136
- "blockers": [], "warnings": []}
137
-
138
- blockers = []
139
- warnings = []
140
- if verdict.get("publishable") is not True:
141
- blockers.append("最近一次质量评审未达标")
142
- if verdict.get("global_pass") is False:
143
- blockers.append("全局一致性评审未通过")
144
- if verdict.get("global_reviewer_count", 0) < MIN_REVIEWERS:
145
- blockers.append("有效全局评审不足 %d 名" % MIN_REVIEWERS)
146
- if verdict.get("global_reviewer_consensus") is False:
147
- blockers.append("全局评审未形成共识")
148
- chapter_scores = verdict.get("chapter_scores") or []
149
- if target_chapter is not None:
150
- try:
151
- target = int(target_chapter)
152
- except (TypeError, ValueError):
153
- target = 0
154
- try:
155
- reviewed_end = int(verdict.get("end_chapter") or 0)
156
- except (TypeError, ValueError):
157
- reviewed_end = 0
158
- if target > 0 and reviewed_end < target:
159
- blockers.append("目标章节超出最近评审版本(目标第 %d 章,评审至第 %d 章)" %
160
- (target, reviewed_end))
161
- if chapter_scores and any(c.get("passed") is not True for c in chapter_scores):
162
- blockers.append("存在未达标章节")
163
- if any(c.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS or
164
- c.get("reviewer_consensus") is False for c in chapter_scores):
165
- blockers.append("存在评审人数不足或无共识章节")
166
- majors = [x for x in (verdict.get("major_issues") or [])
167
- if isinstance(x, dict) and x.get("severity") == "major"]
168
- if majors:
169
- blockers.append("存在 major 问题(共 %d 条)" % len(majors))
170
-
171
- if action == "signing":
172
- checkpoint = verdict.get("signing_checkpoint") or {}
173
- if checkpoint.get("passed") is not True:
174
- blockers.append("2 万字签约检查点未通过")
175
- if checkpoint.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS:
176
- blockers.append("签约检查点有效评审不足 %d 名" % MIN_REVIEWERS)
177
- if checkpoint.get("reviewer_consensus") is False:
178
- blockers.append("签约检查点评审未形成共识")
179
- remote = platform_version if isinstance(platform_version, dict) else {}
180
- remote_n = remote.get("remote_published")
181
- if remote_n is None:
182
- remote_n = remote.get("remote_total")
183
- end_chapter = int(target_chapter or verdict.get("end_chapter") or 0)
184
- if end_chapter and (remote_n is None or int(remote_n or 0) < end_chapter):
185
- blockers.append("平台版本与申请评审版本不一致(本地第 %d 章,平台已发布第 %d 章)" %
186
- (end_chapter, int(remote_n or 0)))
187
-
188
- forced = bool(force)
189
- if blockers and forced:
190
- if not force_confirmed:
191
- blockers.append("强制放行必须二次确认")
192
- if not str(force_reason or "").strip():
193
- blockers.append("强制放行必须填写人工复核原因")
194
- if force_confirmed and str(force_reason or "").strip() and \
195
- all("强制放行必须" not in x for x in blockers):
196
- warnings = list(blockers)
197
- blockers = []
198
- allowed = not blockers
199
- return {"allowed": allowed, "status": "allowed" if allowed else "blocked",
200
- "forced": bool(allowed and forced), "blockers": blockers,
201
- "warnings": warnings}
1
+ # -*- coding: utf-8 -*-
2
+ """Deterministic quality and release gates for generated fiction.
3
+
4
+ LLM reviewers provide judgements; this module decides whether the evidence is
5
+ valid enough to support a release action. It deliberately treats missing,
6
+ malformed, out-of-range and non-consensus scores as unevaluated.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ import re
12
+ from pathlib import Path
13
+
14
+
15
+ MIN_REVIEWERS = 2
16
+ DEFAULT_MAX_SPREAD = 2.0
17
+ _CHAPTER_RE = re.compile(r"^chapter-(\d{1,4})\.(?:md|txt)$", re.I)
18
+
19
+
20
+ def normalize_scores(raw, dimensions):
21
+ """Return (scores, errors); incomplete scores are invalid as a set."""
22
+ raw = raw if isinstance(raw, dict) else {}
23
+ scores, errors = {}, {}
24
+ for dim in dimensions or []:
25
+ value = raw.get(dim)
26
+ try:
27
+ number = float(value)
28
+ except (TypeError, ValueError):
29
+ errors[dim] = "非数字"
30
+ continue
31
+ if not math.isfinite(number) or number < 1.0 or number > 10.0:
32
+ errors[dim] = "必须在 1-10 之间"
33
+ continue
34
+ scores[dim] = round(number, 2)
35
+ if errors or len(scores) != len(list(dimensions or [])):
36
+ return {}, errors or {str(d): "缺失" for d in dimensions or [] if d not in scores}
37
+ return scores, {}
38
+
39
+
40
+ def aggregate_reviews(reviews, dimensions, threshold=7.0,
41
+ min_reviewers=MIN_REVIEWERS,
42
+ max_spread=DEFAULT_MAX_SPREAD):
43
+ """Validate reviewers, calculate means, and require reviewer consensus."""
44
+ dims = list(dimensions or [])
45
+ valid = []
46
+ invalid = []
47
+ seen = set()
48
+ for idx, review in enumerate(reviews or []):
49
+ review = review if isinstance(review, dict) else {}
50
+ reviewer_id = str(review.get("id") or "reviewer-%d" % idx)
51
+ if reviewer_id in seen:
52
+ continue
53
+ seen.add(reviewer_id)
54
+ scores, errors = normalize_scores(review.get("scores"), dims)
55
+ if errors:
56
+ invalid.append({"id": reviewer_id, "errors": errors})
57
+ else:
58
+ valid.append({"id": reviewer_id, "scores": scores})
59
+
60
+ reasons = []
61
+ eligible = len(valid) >= int(min_reviewers)
62
+ if not eligible:
63
+ reasons.append("至少需要 %d 名有效评审,当前 %d 名" %
64
+ (int(min_reviewers), len(valid)))
65
+ means = {}
66
+ spreads = {}
67
+ for dim in dims:
68
+ values = [r["scores"][dim] for r in valid]
69
+ if values:
70
+ means[dim] = round(sum(values) / len(values), 1)
71
+ spreads[dim] = round(max(values) - min(values), 1)
72
+
73
+ consensus = bool(eligible and means and
74
+ all(spreads.get(dim, float("inf")) <= float(max_spread)
75
+ for dim in dims))
76
+ if eligible and not consensus:
77
+ bad = [dim for dim in dims if spreads.get(dim, float("inf")) > float(max_spread)]
78
+ reasons.append("评审分歧超过 %.1f 分:%s" % (float(max_spread), "、".join(bad)))
79
+ passed = bool(consensus and means and
80
+ all(value >= float(threshold) for value in means.values()))
81
+ if consensus and not passed:
82
+ reasons.append("综合分存在低于 %.1f 的维度" % float(threshold))
83
+ return {
84
+ "eligible": eligible,
85
+ "consensus": consensus,
86
+ "passed": passed,
87
+ "means": means,
88
+ "spreads": spreads,
89
+ "valid_reviewers": valid,
90
+ "invalid_reviewers": invalid,
91
+ "reviewer_count": len(valid),
92
+ "reasons": reasons,
93
+ }
94
+
95
+
96
+ def local_version(workdir):
97
+ """Return the local manuscript version used for a release decision."""
98
+ root = Path(workdir or "")
99
+ rows = []
100
+ if root.is_dir():
101
+ for path in root.iterdir():
102
+ match = _CHAPTER_RE.match(path.name)
103
+ if not match or not path.is_file():
104
+ continue
105
+ try:
106
+ text = path.read_text(encoding="utf-8", errors="replace")
107
+ except OSError:
108
+ continue
109
+ rows.append((int(match.group(1)), len(re.sub(r"\s", "", text))))
110
+ return {
111
+ "local_total": len(rows),
112
+ "local_latest": max((n for n, _ in rows), default=0),
113
+ "local_words": sum(words for _, words in rows),
114
+ }
115
+
116
+
117
+ def evaluate_release(verdict, action="publish", target_chapter=None,
118
+ platform_version=None, force=False,
119
+ force_confirmed=False, force_reason=""):
120
+ """Decide whether a publish/signing action is allowed.
121
+
122
+ ``continue`` is intentionally separate: writers may continue improving an
123
+ unready manuscript, while publishing and signing remain gated.
124
+ """
125
+ verdict = verdict if isinstance(verdict, dict) else {}
126
+ action = str(action or "publish").strip().lower()
127
+ if action == "continue":
128
+ return {"allowed": True, "status": "continue_only", "forced": False,
129
+ "blockers": [], "warnings": []}
130
+ if action == "draft":
131
+ # 存草稿不是发布:内容不公开展示(读者不可见),正式提交前用户还会
132
+ # 在平台上逐章过目——质量闸只在「发布/签约」这类上线动作上拦,
133
+ # 拦草稿只会把「先落平台再人工把关」的通路堵死(2026-10-09 全部
134
+ # 发草稿需求:质量未达标的 53 章先存草稿,发布仍受本闸约束)。
135
+ return {"allowed": True, "status": "draft_only", "forced": False,
136
+ "blockers": [], "warnings": []}
137
+
138
+ blockers = []
139
+ warnings = []
140
+ if verdict.get("publishable") is not True:
141
+ blockers.append("最近一次质量评审未达标")
142
+ if verdict.get("global_pass") is False:
143
+ blockers.append("全局一致性评审未通过")
144
+ if verdict.get("global_reviewer_count", 0) < MIN_REVIEWERS:
145
+ blockers.append("有效全局评审不足 %d 名" % MIN_REVIEWERS)
146
+ if verdict.get("global_reviewer_consensus") is False:
147
+ blockers.append("全局评审未形成共识")
148
+ chapter_scores = verdict.get("chapter_scores") or []
149
+ if target_chapter is not None:
150
+ try:
151
+ target = int(target_chapter)
152
+ except (TypeError, ValueError):
153
+ target = 0
154
+ try:
155
+ reviewed_end = int(verdict.get("end_chapter") or 0)
156
+ except (TypeError, ValueError):
157
+ reviewed_end = 0
158
+ if target > 0 and reviewed_end < target:
159
+ blockers.append("目标章节超出最近评审版本(目标第 %d 章,评审至第 %d 章)" %
160
+ (target, reviewed_end))
161
+ if chapter_scores and any(c.get("passed") is not True for c in chapter_scores):
162
+ blockers.append("存在未达标章节")
163
+ if any(c.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS or
164
+ c.get("reviewer_consensus") is False for c in chapter_scores):
165
+ blockers.append("存在评审人数不足或无共识章节")
166
+ majors = [x for x in (verdict.get("major_issues") or [])
167
+ if isinstance(x, dict) and x.get("severity") == "major"]
168
+ if majors:
169
+ blockers.append("存在 major 问题(共 %d 条)" % len(majors))
170
+
171
+ if action == "signing":
172
+ checkpoint = verdict.get("signing_checkpoint") or {}
173
+ if checkpoint.get("passed") is not True:
174
+ blockers.append("2 万字签约检查点未通过")
175
+ if checkpoint.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS:
176
+ blockers.append("签约检查点有效评审不足 %d 名" % MIN_REVIEWERS)
177
+ if checkpoint.get("reviewer_consensus") is False:
178
+ blockers.append("签约检查点评审未形成共识")
179
+ remote = platform_version if isinstance(platform_version, dict) else {}
180
+ remote_n = remote.get("remote_published")
181
+ if remote_n is None:
182
+ remote_n = remote.get("remote_total")
183
+ end_chapter = int(target_chapter or verdict.get("end_chapter") or 0)
184
+ if end_chapter and (remote_n is None or int(remote_n or 0) < end_chapter):
185
+ blockers.append("平台版本与申请评审版本不一致(本地第 %d 章,平台已发布第 %d 章)" %
186
+ (end_chapter, int(remote_n or 0)))
187
+
188
+ forced = bool(force)
189
+ if blockers and forced:
190
+ if not force_confirmed:
191
+ blockers.append("强制放行必须二次确认")
192
+ if not str(force_reason or "").strip():
193
+ blockers.append("强制放行必须填写人工复核原因")
194
+ if force_confirmed and str(force_reason or "").strip() and \
195
+ all("强制放行必须" not in x for x in blockers):
196
+ warnings = list(blockers)
197
+ blockers = []
198
+ allowed = not blockers
199
+ return {"allowed": allowed, "status": "allowed" if allowed else "blocked",
200
+ "forced": bool(allowed and forced), "blockers": blockers,
201
+ "warnings": warnings}
@@ -1,38 +1,38 @@
1
- # -*- coding: utf-8 -*-
2
- """自由文本脱敏基础函数;不依赖业务模块,供各类本地台账复用。"""
3
- from __future__ import annotations
4
-
5
- import re
6
-
7
-
8
- _KEY_PATTERNS = (
9
- (re.compile(r"\bsk-[A-Za-z0-9_-]{8,}"), "[key]"),
10
- (re.compile(r"\b(?:Bearer|bearer)\s+\S+"), "Bearer [key]"),
11
- (re.compile(r"(?i)\b((?:api[_-]?|access[_-]?|secret[_-]?|auth[_-]?)(?:key|token|secret))"
12
- r"""["']?\s*[:=,,]\s*["']?[A-Za-z0-9._~+/=-]{8,}"""), r"\1[key]"),
13
- (re.compile(r"\b[0-9a-fA-F]{40,}\b"), "[token]"),
14
- )
15
-
16
- _PATH_PATTERNS = (
17
- (re.compile(r"(?i)\b[A-Z]:\\(?:[^\\/:*?\"<>|\r\n]+\\)*[^\\/:*?\"<>|\r\n]*"),
18
- lambda m: "[path]" + m.group(0).split("\\")[-1]),
19
- (re.compile(r"(?i)\b(?:\\\\[^\\\s]+\\[^\s]+)"), "[path]"),
20
- (re.compile(r"(?:/Users/|/home/|~)[^\s\"':]+"),
21
- lambda m: "[path]" + m.group(0).rsplit("/", 1)[-1]),
22
- )
23
-
24
-
25
- def scrub_text(text, limit=600):
26
- """剥离常见密钥、令牌和绝对路径,再按字符上限截断。"""
27
- if not isinstance(text, str):
28
- text = "" if text is None else str(text)
29
- out = text
30
- for pattern, replacement in _KEY_PATTERNS:
31
- out = pattern.sub(replacement, out)
32
- for pattern, replacement in _PATH_PATTERNS:
33
- try:
34
- out = pattern.sub(replacement, out)
35
- except Exception:
36
- pass
37
- out = out.strip()
38
- return out[:limit] + "…" if len(out) > limit else out
1
+ # -*- coding: utf-8 -*-
2
+ """自由文本脱敏基础函数;不依赖业务模块,供各类本地台账复用。"""
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ _KEY_PATTERNS = (
9
+ (re.compile(r"\bsk-[A-Za-z0-9_-]{8,}"), "[key]"),
10
+ (re.compile(r"\b(?:Bearer|bearer)\s+\S+"), "Bearer [key]"),
11
+ (re.compile(r"(?i)\b((?:api[_-]?|access[_-]?|secret[_-]?|auth[_-]?)(?:key|token|secret))"
12
+ r"""["']?\s*[:=,,]\s*["']?[A-Za-z0-9._~+/=-]{8,}"""), r"\1[key]"),
13
+ (re.compile(r"\b[0-9a-fA-F]{40,}\b"), "[token]"),
14
+ )
15
+
16
+ _PATH_PATTERNS = (
17
+ (re.compile(r"(?i)\b[A-Z]:\\(?:[^\\/:*?\"<>|\r\n]+\\)*[^\\/:*?\"<>|\r\n]*"),
18
+ lambda m: "[path]" + m.group(0).split("\\")[-1]),
19
+ (re.compile(r"(?i)\b(?:\\\\[^\\\s]+\\[^\s]+)"), "[path]"),
20
+ (re.compile(r"(?:/Users/|/home/|~)[^\s\"':]+"),
21
+ lambda m: "[path]" + m.group(0).rsplit("/", 1)[-1]),
22
+ )
23
+
24
+
25
+ def scrub_text(text, limit=600):
26
+ """剥离常见密钥、令牌和绝对路径,再按字符上限截断。"""
27
+ if not isinstance(text, str):
28
+ text = "" if text is None else str(text)
29
+ out = text
30
+ for pattern, replacement in _KEY_PATTERNS:
31
+ out = pattern.sub(replacement, out)
32
+ for pattern, replacement in _PATH_PATTERNS:
33
+ try:
34
+ out = pattern.sub(replacement, out)
35
+ except Exception:
36
+ pass
37
+ out = out.strip()
38
+ return out[:limit] + "…" if len(out) > limit else out