codebee 0.1.102 → 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 (121) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -21
  3. package/README.md +4 -8
  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 -457
  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/bookmeta.py +1036 -1036
  24. package/app/core/bookmeta_catalog.py +180 -180
  25. package/app/core/branching.py +137 -137
  26. package/app/core/capability.py +144 -144
  27. package/app/core/chaptersafe.py +32 -32
  28. package/app/core/cleanup.py +303 -303
  29. package/app/core/command_auth.py +302 -302
  30. package/app/core/compaction.py +201 -201
  31. package/app/core/contracts.py +255 -255
  32. package/app/core/covergen.py +356 -356
  33. package/app/core/defectretro.py +43 -43
  34. package/app/core/diagnostics.py +114 -114
  35. package/app/core/dispatch.py +1 -1
  36. package/app/core/dispatch_log.py +128 -128
  37. package/app/core/env_scrub.py +85 -85
  38. package/app/core/errorlog.py +137 -137
  39. package/app/core/eval_matrix.py +91 -91
  40. package/app/core/evaluation.py +174 -174
  41. package/app/core/failure_notify.py +290 -290
  42. package/app/core/flow_graph.py +33 -33
  43. package/app/core/goal_service.py +167 -167
  44. package/app/core/health.py +389 -389
  45. package/app/core/history.py +38 -38
  46. package/app/core/hooks.py +162 -162
  47. package/app/core/knowledge_pipeline.py +86 -86
  48. package/app/core/manager.py +2385 -2385
  49. package/app/core/mocks.py +64 -64
  50. package/app/core/notify.py +201 -201
  51. package/app/core/novel_quality.py +181 -181
  52. package/app/core/openapi.py +217 -217
  53. package/app/core/operations.py +228 -228
  54. package/app/core/paihang.py +145 -145
  55. package/app/core/pipeline.py +2 -2
  56. package/app/core/portguard.py +111 -111
  57. package/app/core/presence.py +77 -77
  58. package/app/core/preview.py +293 -293
  59. package/app/core/project_memory.py +206 -206
  60. package/app/core/prompt_guard.py +78 -78
  61. package/app/core/publish/__init__.py +7 -7
  62. package/app/core/publish/auto.py +659 -659
  63. package/app/core/publish/chaprows.py +138 -138
  64. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  65. package/app/core/publish/manager.py +1185 -1185
  66. package/app/core/publish/qimao.py +193 -193
  67. package/app/core/publish/volumes.py +108 -108
  68. package/app/core/publish/ws.py +139 -139
  69. package/app/core/quality_gate.py +201 -201
  70. package/app/core/redact.py +38 -38
  71. package/app/core/repeat_guard.py +129 -129
  72. package/app/core/retrieval.py +137 -137
  73. package/app/core/sandbox_runtime.py +95 -95
  74. package/app/core/selfupdate.py +384 -384
  75. package/app/core/session_log.py +161 -161
  76. package/app/core/sessions.py +312 -312
  77. package/app/core/settings.py +215 -215
  78. package/app/core/settings_schema.py +282 -282
  79. package/app/core/share_page.py +114 -114
  80. package/app/core/skill_scan.py +139 -139
  81. package/app/core/skillpacks/market/character-bible.md +66 -66
  82. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  83. package/app/core/skillpacks/market/git-workflow.md +57 -57
  84. package/app/core/skillpacks/market/release-notes.md +72 -72
  85. package/app/core/skillpacks/market/weekly-report.md +71 -71
  86. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  87. package/app/core/step_runner.py +132 -132
  88. package/app/core/story_tracking.py +275 -275
  89. package/app/core/task_compile.py +199 -199
  90. package/app/core/telemetry.py +292 -292
  91. package/app/core/tlsctx.py +64 -64
  92. package/app/core/token_meter.py +147 -147
  93. package/app/core/tool_outputs.py +107 -107
  94. package/app/core/tracing.py +207 -207
  95. package/app/core/upstream.py +41 -41
  96. package/app/core/usage.py +1038 -1038
  97. package/app/core/volumes.py +389 -389
  98. package/app/core/webhooks.py +89 -89
  99. package/app/core/wxdigest.py +865 -865
  100. package/app/core/zentao.py +2692 -2692
  101. package/app/mcp_server.py +244 -244
  102. package/app/output/_createbook_body.json +1 -0
  103. package/app/output/_probe_fq_click.py +50 -0
  104. package/app/output/_probe_fq_form.py +42 -0
  105. package/app/output/_probe_fq_modal.py +55 -0
  106. package/app/output/_probe_fq_points.py +71 -0
  107. package/app/output/_probe_fq_submit.py +34 -0
  108. package/app/output/_probe_fq_watch.py +62 -0
  109. package/app/output/workbench-clean-preview.jpg +0 -0
  110. package/app/output/workbench-reference-render.png +0 -0
  111. package/app/output/workbench-shell.png +0 -0
  112. package/app/pick_dialog.py +110 -110
  113. package/app/ui/app.js +2 -3
  114. package/app/ui/artdiff.js +74 -74
  115. package/app/ui/i18n.js +0 -1
  116. package/app/ui/icons/bee.svg +79 -79
  117. package/app/ui/manifest.json +16 -16
  118. package/app/ui/qrcode.js +2297 -2297
  119. package/bin/tutti.js +147 -147
  120. package/package.json +1 -1
  121. 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