codebee 0.1.103 → 0.1.104

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