codebee 0.1.95 → 0.1.96

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 (138) hide show
  1. package/CHANGELOG.md +725 -720
  2. package/LICENSE +21 -21
  3. package/README.md +515 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +100 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +209 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +1680 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +143 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mocks.py +64 -64
  46. package/app/core/notify.py +201 -201
  47. package/app/core/novel_quality.py +181 -189
  48. package/app/core/openapi.py +177 -154
  49. package/app/core/operations.py +228 -228
  50. package/app/core/paihang.py +145 -145
  51. package/app/core/pipeline.py +43 -19
  52. package/app/core/planner.py +17 -11
  53. package/app/core/policy.py +52 -0
  54. package/app/core/portguard.py +111 -111
  55. package/app/core/presence.py +77 -77
  56. package/app/core/preview.py +293 -293
  57. package/app/core/prompt_guard.py +78 -78
  58. package/app/core/publish/__init__.py +7 -7
  59. package/app/core/publish/auto.py +26 -3
  60. package/app/core/publish/browser.py +655 -655
  61. package/app/core/publish/chaprows.py +138 -0
  62. package/app/core/publish/fanqie.py +210 -165
  63. package/app/core/publish/flow.py +753 -640
  64. package/app/core/publish/flows-fanqie-calibrated.json +349 -336
  65. package/app/core/publish/ledger.py +13 -1
  66. package/app/core/publish/manager.py +1150 -1118
  67. package/app/core/publish/qimao.py +176 -176
  68. package/app/core/publish/volumes.py +108 -0
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/quality_gate.py +194 -194
  71. package/app/core/redact.py +38 -38
  72. package/app/core/repeat_guard.py +129 -129
  73. package/app/core/retrieval.py +137 -137
  74. package/app/core/router.py +398 -398
  75. package/app/core/runner.py +2238 -2238
  76. package/app/core/selfupdate.py +384 -384
  77. package/app/core/session_log.py +161 -161
  78. package/app/core/sessions.py +312 -312
  79. package/app/core/settings.py +20 -0
  80. package/app/core/settings_schema.py +270 -270
  81. package/app/core/share_page.py +114 -114
  82. package/app/core/skill_scan.py +139 -139
  83. package/app/core/skillpacks/fanqie-novel.md +8 -6
  84. package/app/core/skillpacks/market/character-bible.md +66 -66
  85. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  86. package/app/core/skillpacks/market/git-workflow.md +57 -57
  87. package/app/core/skillpacks/market/release-notes.md +72 -72
  88. package/app/core/skillpacks/market/weekly-report.md +71 -71
  89. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  90. package/app/core/skillpacks/qimao-signing.md +14 -11
  91. package/app/core/skills.py +32 -21
  92. package/app/core/step_runner.py +132 -132
  93. package/app/core/store.py +9 -0
  94. package/app/core/story_tracking.py +275 -275
  95. package/app/core/task_compile.py +199 -199
  96. package/app/core/telemetry.py +292 -292
  97. package/app/core/tlsctx.py +64 -64
  98. package/app/core/token_meter.py +147 -147
  99. package/app/core/tracing.py +207 -144
  100. package/app/core/upstream.py +41 -41
  101. package/app/core/usage.py +1038 -1038
  102. package/app/core/volumes.py +389 -389
  103. package/app/core/webhooks.py +89 -0
  104. package/app/core/wxdigest.py +858 -858
  105. package/app/core/zentao.py +2692 -2692
  106. package/app/main.py +104 -9
  107. package/app/mcp_server.py +244 -244
  108. package/app/pick_dialog.py +110 -110
  109. package/app/ui/app.js +286 -18
  110. package/app/ui/artdiff.js +74 -74
  111. package/app/ui/assets/community-qr.png +0 -0
  112. package/app/ui/assets/donate-alipay.jpg +0 -0
  113. package/app/ui/assets/donate-qr.jpg +0 -0
  114. package/app/ui/board.html +162 -162
  115. package/app/ui/board.js +305 -305
  116. package/app/ui/i18n.js +33 -0
  117. package/app/ui/icons/bee.svg +79 -79
  118. package/app/ui/index.html +32 -3
  119. package/app/ui/manifest.json +16 -16
  120. package/app/ui/qrcode.js +2297 -2297
  121. package/app/ui/style.css +196 -2
  122. package/bin/tutti.js +147 -147
  123. package/package.json +48 -48
  124. package/tools/wx_reader.py +261 -261
  125. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  126. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  127. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  128. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  129. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  130. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  131. package/app/.playwright-cli/run-detail-after.png +0 -0
  132. package/app/output/_createbook_body.json +0 -1
  133. package/app/output/_probe_fq_click.py +0 -50
  134. package/app/output/_probe_fq_form.py +0 -42
  135. package/app/output/_probe_fq_modal.py +0 -55
  136. package/app/output/_probe_fq_points.py +0 -71
  137. package/app/output/_probe_fq_submit.py +0 -34
  138. package/app/output/_probe_fq_watch.py +0 -62
@@ -1,194 +1,194 @@
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
-
131
- blockers = []
132
- warnings = []
133
- if verdict.get("publishable") is not True:
134
- blockers.append("最近一次质量评审未达标")
135
- if verdict.get("global_pass") is False:
136
- blockers.append("全局一致性评审未通过")
137
- if verdict.get("global_reviewer_count", 0) < MIN_REVIEWERS:
138
- blockers.append("有效全局评审不足 %d 名" % MIN_REVIEWERS)
139
- if verdict.get("global_reviewer_consensus") is False:
140
- blockers.append("全局评审未形成共识")
141
- chapter_scores = verdict.get("chapter_scores") or []
142
- if target_chapter is not None:
143
- try:
144
- target = int(target_chapter)
145
- except (TypeError, ValueError):
146
- target = 0
147
- try:
148
- reviewed_end = int(verdict.get("end_chapter") or 0)
149
- except (TypeError, ValueError):
150
- reviewed_end = 0
151
- if target > 0 and reviewed_end < target:
152
- blockers.append("目标章节超出最近评审版本(目标第 %d 章,评审至第 %d 章)" %
153
- (target, reviewed_end))
154
- if chapter_scores and any(c.get("passed") is not True for c in chapter_scores):
155
- blockers.append("存在未达标章节")
156
- if any(c.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS or
157
- c.get("reviewer_consensus") is False for c in chapter_scores):
158
- blockers.append("存在评审人数不足或无共识章节")
159
- majors = [x for x in (verdict.get("major_issues") or [])
160
- if isinstance(x, dict) and x.get("severity") == "major"]
161
- if majors:
162
- blockers.append("存在 major 问题(共 %d 条)" % len(majors))
163
-
164
- if action == "signing":
165
- checkpoint = verdict.get("signing_checkpoint") or {}
166
- if checkpoint.get("passed") is not True:
167
- blockers.append("2 万字签约检查点未通过")
168
- if checkpoint.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS:
169
- blockers.append("签约检查点有效评审不足 %d 名" % MIN_REVIEWERS)
170
- if checkpoint.get("reviewer_consensus") is False:
171
- blockers.append("签约检查点评审未形成共识")
172
- remote = platform_version if isinstance(platform_version, dict) else {}
173
- remote_n = remote.get("remote_published")
174
- if remote_n is None:
175
- remote_n = remote.get("remote_total")
176
- end_chapter = int(target_chapter or verdict.get("end_chapter") or 0)
177
- if end_chapter and (remote_n is None or int(remote_n or 0) < end_chapter):
178
- blockers.append("平台版本与申请评审版本不一致(本地第 %d 章,平台已发布第 %d 章)" %
179
- (end_chapter, int(remote_n or 0)))
180
-
181
- forced = bool(force)
182
- if blockers and forced:
183
- if not force_confirmed:
184
- blockers.append("强制放行必须二次确认")
185
- if not str(force_reason or "").strip():
186
- blockers.append("强制放行必须填写人工复核原因")
187
- if force_confirmed and str(force_reason or "").strip() and \
188
- all("强制放行必须" not in x for x in blockers):
189
- warnings = list(blockers)
190
- blockers = []
191
- allowed = not blockers
192
- return {"allowed": allowed, "status": "allowed" if allowed else "blocked",
193
- "forced": bool(allowed and forced), "blockers": blockers,
194
- "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
+
131
+ blockers = []
132
+ warnings = []
133
+ if verdict.get("publishable") is not True:
134
+ blockers.append("最近一次质量评审未达标")
135
+ if verdict.get("global_pass") is False:
136
+ blockers.append("全局一致性评审未通过")
137
+ if verdict.get("global_reviewer_count", 0) < MIN_REVIEWERS:
138
+ blockers.append("有效全局评审不足 %d 名" % MIN_REVIEWERS)
139
+ if verdict.get("global_reviewer_consensus") is False:
140
+ blockers.append("全局评审未形成共识")
141
+ chapter_scores = verdict.get("chapter_scores") or []
142
+ if target_chapter is not None:
143
+ try:
144
+ target = int(target_chapter)
145
+ except (TypeError, ValueError):
146
+ target = 0
147
+ try:
148
+ reviewed_end = int(verdict.get("end_chapter") or 0)
149
+ except (TypeError, ValueError):
150
+ reviewed_end = 0
151
+ if target > 0 and reviewed_end < target:
152
+ blockers.append("目标章节超出最近评审版本(目标第 %d 章,评审至第 %d 章)" %
153
+ (target, reviewed_end))
154
+ if chapter_scores and any(c.get("passed") is not True for c in chapter_scores):
155
+ blockers.append("存在未达标章节")
156
+ if any(c.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS or
157
+ c.get("reviewer_consensus") is False for c in chapter_scores):
158
+ blockers.append("存在评审人数不足或无共识章节")
159
+ majors = [x for x in (verdict.get("major_issues") or [])
160
+ if isinstance(x, dict) and x.get("severity") == "major"]
161
+ if majors:
162
+ blockers.append("存在 major 问题(共 %d 条)" % len(majors))
163
+
164
+ if action == "signing":
165
+ checkpoint = verdict.get("signing_checkpoint") or {}
166
+ if checkpoint.get("passed") is not True:
167
+ blockers.append("2 万字签约检查点未通过")
168
+ if checkpoint.get("reviewer_count", MIN_REVIEWERS) < MIN_REVIEWERS:
169
+ blockers.append("签约检查点有效评审不足 %d 名" % MIN_REVIEWERS)
170
+ if checkpoint.get("reviewer_consensus") is False:
171
+ blockers.append("签约检查点评审未形成共识")
172
+ remote = platform_version if isinstance(platform_version, dict) else {}
173
+ remote_n = remote.get("remote_published")
174
+ if remote_n is None:
175
+ remote_n = remote.get("remote_total")
176
+ end_chapter = int(target_chapter or verdict.get("end_chapter") or 0)
177
+ if end_chapter and (remote_n is None or int(remote_n or 0) < end_chapter):
178
+ blockers.append("平台版本与申请评审版本不一致(本地第 %d 章,平台已发布第 %d 章)" %
179
+ (end_chapter, int(remote_n or 0)))
180
+
181
+ forced = bool(force)
182
+ if blockers and forced:
183
+ if not force_confirmed:
184
+ blockers.append("强制放行必须二次确认")
185
+ if not str(force_reason or "").strip():
186
+ blockers.append("强制放行必须填写人工复核原因")
187
+ if force_confirmed and str(force_reason or "").strip() and \
188
+ all("强制放行必须" not in x for x in blockers):
189
+ warnings = list(blockers)
190
+ blockers = []
191
+ allowed = not blockers
192
+ return {"allowed": allowed, "status": "allowed" if allowed else "blocked",
193
+ "forced": bool(allowed and forced), "blockers": blockers,
194
+ "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