codebee 0.1.95 → 0.1.97

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 (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -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 +246 -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 +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -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/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. 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