codebee 0.1.94 → 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 (139) hide show
  1. package/CHANGELOG.md +725 -715
  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/flows.py +8 -1
  39. package/app/core/goal_service.py +167 -167
  40. package/app/core/health.py +389 -389
  41. package/app/core/history.py +38 -38
  42. package/app/core/hooks.py +162 -162
  43. package/app/core/jobs.py +25 -4
  44. package/app/core/knowledge_pipeline.py +86 -0
  45. package/app/core/manager.py +2232 -2232
  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 +43 -19
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +52 -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/prompt_guard.py +78 -78
  59. package/app/core/publish/__init__.py +7 -7
  60. package/app/core/publish/auto.py +26 -3
  61. package/app/core/publish/browser.py +655 -655
  62. package/app/core/publish/chaprows.py +138 -0
  63. package/app/core/publish/fanqie.py +210 -165
  64. package/app/core/publish/flow.py +753 -640
  65. package/app/core/publish/flows-fanqie-calibrated.json +349 -336
  66. package/app/core/publish/ledger.py +13 -1
  67. package/app/core/publish/manager.py +1150 -1118
  68. package/app/core/publish/qimao.py +176 -176
  69. package/app/core/publish/volumes.py +108 -0
  70. package/app/core/publish/ws.py +139 -139
  71. package/app/core/quality_gate.py +194 -194
  72. package/app/core/redact.py +38 -38
  73. package/app/core/repeat_guard.py +129 -129
  74. package/app/core/retrieval.py +137 -137
  75. package/app/core/router.py +398 -398
  76. package/app/core/runner.py +2238 -2238
  77. package/app/core/selfupdate.py +384 -384
  78. package/app/core/session_log.py +161 -161
  79. package/app/core/sessions.py +312 -312
  80. package/app/core/settings.py +20 -0
  81. package/app/core/settings_schema.py +270 -270
  82. package/app/core/share_page.py +114 -114
  83. package/app/core/skill_scan.py +139 -139
  84. package/app/core/skillpacks/fanqie-novel.md +8 -6
  85. package/app/core/skillpacks/market/character-bible.md +66 -66
  86. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  87. package/app/core/skillpacks/market/git-workflow.md +57 -57
  88. package/app/core/skillpacks/market/release-notes.md +72 -72
  89. package/app/core/skillpacks/market/weekly-report.md +71 -71
  90. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  91. package/app/core/skillpacks/qimao-signing.md +14 -11
  92. package/app/core/skills.py +32 -21
  93. package/app/core/step_runner.py +132 -132
  94. package/app/core/store.py +9 -0
  95. package/app/core/story_tracking.py +275 -275
  96. package/app/core/task_compile.py +199 -199
  97. package/app/core/telemetry.py +292 -292
  98. package/app/core/tlsctx.py +64 -64
  99. package/app/core/token_meter.py +147 -147
  100. package/app/core/tracing.py +207 -144
  101. package/app/core/upstream.py +41 -41
  102. package/app/core/usage.py +1038 -1038
  103. package/app/core/volumes.py +389 -389
  104. package/app/core/webhooks.py +89 -0
  105. package/app/core/wxdigest.py +858 -858
  106. package/app/core/zentao.py +2692 -2692
  107. package/app/main.py +104 -9
  108. package/app/mcp_server.py +244 -244
  109. package/app/pick_dialog.py +110 -110
  110. package/app/ui/app.js +287 -19
  111. package/app/ui/artdiff.js +74 -74
  112. package/app/ui/assets/community-qr.png +0 -0
  113. package/app/ui/assets/donate-alipay.jpg +0 -0
  114. package/app/ui/assets/donate-qr.jpg +0 -0
  115. package/app/ui/board.html +162 -162
  116. package/app/ui/board.js +305 -305
  117. package/app/ui/i18n.js +33 -0
  118. package/app/ui/icons/bee.svg +79 -79
  119. package/app/ui/index.html +32 -3
  120. package/app/ui/manifest.json +16 -16
  121. package/app/ui/qrcode.js +2297 -2297
  122. package/app/ui/style.css +196 -2
  123. package/bin/tutti.js +147 -147
  124. package/package.json +48 -48
  125. package/tools/wx_reader.py +261 -261
  126. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  127. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  128. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  129. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  130. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  131. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  132. package/app/.playwright-cli/run-detail-after.png +0 -0
  133. package/app/output/_createbook_body.json +0 -1
  134. package/app/output/_probe_fq_click.py +0 -50
  135. package/app/output/_probe_fq_form.py +0 -42
  136. package/app/output/_probe_fq_modal.py +0 -55
  137. package/app/output/_probe_fq_points.py +0 -71
  138. package/app/output/_probe_fq_submit.py +0 -34
  139. package/app/output/_probe_fq_watch.py +0 -62
@@ -1,174 +1,174 @@
1
- # -*- coding: utf-8 -*-
2
- """Expiring model/provider evaluation conclusions.
3
-
4
- An evaluation is evidence captured at a point in time, not a permanent
5
- capability claim. Results are kept as a small, content-free local ledger so
6
- the UI or a router can distinguish a fresh ``passed`` result from an expired
7
- one. A provider health failure invalidates matching conclusions immediately;
8
- the health probe can show recovery, but only a fresh evaluation restores
9
- ``passed``.
10
- """
11
- from __future__ import annotations
12
-
13
- import json
14
- import math
15
- import os
16
- from pathlib import Path
17
- import threading
18
- import time
19
-
20
- DEFAULT_TTL_SECONDS = 24 * 60 * 60
21
- _LOCK = threading.RLock()
22
-
23
-
24
- def _path() -> Path:
25
- from . import paths
26
- return Path(paths.DATA_DIR) / "evaluations.json"
27
-
28
-
29
- def ttl_seconds() -> float:
30
- try:
31
- return max(60.0, float(os.environ.get(
32
- "TUTTI_EVALUATION_TTL_S", DEFAULT_TTL_SECONDS)))
33
- except (TypeError, ValueError):
34
- return float(DEFAULT_TTL_SECONDS)
35
-
36
-
37
- def _read() -> dict:
38
- try:
39
- value = json.loads(_path().read_text(encoding="utf-8"))
40
- return value if isinstance(value, dict) else {}
41
- except (OSError, json.JSONDecodeError):
42
- return {}
43
-
44
-
45
- def _write(value: dict) -> None:
46
- path = _path()
47
- try:
48
- path.parent.mkdir(parents=True, exist_ok=True)
49
- tmp = path.with_suffix(".tmp")
50
- tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2),
51
- encoding="utf-8")
52
- tmp.replace(path)
53
- except OSError:
54
- # Evaluation is telemetry. A read-only data directory must not turn
55
- # a successful model call into a task failure.
56
- pass
57
-
58
-
59
- def key(kind: str, provider_id: str, model: str = "", protocol: str = "",
60
- key_id: str = "") -> str:
61
- base = "%s|%s|%s" % (str(kind or "provider"),
62
- str(provider_id or ""), str(model or ""))
63
- suffix = "|".join((str(protocol or ""), str(key_id or "")))
64
- return base if not suffix.strip("|") else base + "|" + suffix
65
-
66
-
67
- def decorate(result: dict, *, now=None) -> dict:
68
- """Attach an evaluated-at timestamp and expiry to an API response."""
69
- out = dict(result or {})
70
- now = time.time() if now is None else float(now)
71
- out["evaluated_at"] = now
72
- out["expires_at"] = now + ttl_seconds()
73
- out["fresh"] = True
74
- return out
75
-
76
-
77
- def record(kind: str, provider_id: str, model: str, result: dict,
78
- *, protocol: str = "", requested_model: str = "",
79
- served_model: str = "", key_id: str = "") -> dict:
80
- """Persist safe metadata for one completed evaluation and return it."""
81
- out = decorate(result)
82
- entry = {
83
- "kind": str(kind or "provider"),
84
- "provider_id": str(provider_id or ""),
85
- "model": str(model or ""),
86
- "status": str(out.get("status") or "failed"),
87
- "ok": bool(out.get("ok")),
88
- "evaluated_at": out["evaluated_at"],
89
- "expires_at": out["expires_at"],
90
- "protocol": str(protocol or ""),
91
- "key_id": str(key_id or result.get("key_id") or ""),
92
- "requested_model": str(requested_model or model or ""),
93
- "served_model": str(served_model or ""),
94
- }
95
- with _LOCK:
96
- data = _read()
97
- data.setdefault("entries", {})[key(
98
- kind, provider_id, model, protocol, key_id or result.get("key_id") or "")] = entry
99
- _write(data)
100
- return out
101
-
102
-
103
- def invalidate_provider(provider_id: str, model: str = "",
104
- *, reason: str = "health_failure") -> None:
105
- """Invalidate one model, or all conclusions, after a health event."""
106
- provider_id = str(provider_id or "").strip()
107
- if not provider_id:
108
- return
109
- with _LOCK:
110
- data = _read()
111
- at = time.time()
112
- invalidations = data.setdefault("invalidated", {})
113
- invalidations[provider_id] = {
114
- "at": at, "reason": str(reason or "health_failure")[:80]}
115
- if model:
116
- invalidations[key("model", provider_id, model)] = {
117
- "at": at, "reason": str(reason or "health_failure")[:80]}
118
- _write(data)
119
-
120
-
121
- def _safe_float(value, default=0.0) -> float:
122
- try:
123
- result = float(value)
124
- return result if math.isfinite(result) else float(default)
125
- except (TypeError, ValueError):
126
- return float(default)
127
-
128
-
129
- def get(kind: str, provider_id: str, model: str = "", *, now=None):
130
- """Read an evaluation, returning a stale marker after TTL/health expiry."""
131
- now = time.time() if now is None else float(now)
132
- with _LOCK:
133
- data = _read()
134
- entries = data.get("entries") or {}
135
- base_key = key(kind, provider_id, model)
136
- entry = entries.get(base_key)
137
- matches = [value for name, value in entries.items()
138
- if str(name).startswith(base_key + "|") and isinstance(value, dict)]
139
- candidates = ([entry] if isinstance(entry, dict) else []) + matches
140
- entry = max(candidates, key=lambda value: _safe_float(value.get("evaluated_at")),
141
- default=None)
142
- if not isinstance(entry, dict):
143
- return None
144
- out = dict(entry)
145
- invalidated = data.get("invalidated") or {}
146
- exact_invalid = invalidated.get(key(kind, provider_id, model)) or {}
147
- provider_invalid = invalidated.get(str(provider_id or "")) or {}
148
- invalid = max((candidate for candidate in (exact_invalid, provider_invalid)
149
- if isinstance(candidate, dict)),
150
- key=lambda candidate: _safe_float(candidate.get("at")),
151
- default={})
152
- invalid_at = _safe_float(invalid.get("at"))
153
- expires_at = _safe_float(out.get("expires_at"))
154
- stale_reason = ""
155
- if invalid_at and invalid_at >= _safe_float(out.get("evaluated_at")):
156
- stale_reason = str(invalid.get("reason") or "health_failure")
157
- elif expires_at <= now:
158
- stale_reason = "ttl_expired"
159
- if stale_reason:
160
- out["fresh"] = False
161
- out["ok"] = False
162
- out["status"] = "stale"
163
- out["stale_reason"] = stale_reason
164
- else:
165
- out["fresh"] = True
166
- return out
167
-
168
-
169
- def reset_for_tests() -> None:
170
- with _LOCK:
171
- try:
172
- _path().unlink()
173
- except OSError:
174
- pass
1
+ # -*- coding: utf-8 -*-
2
+ """Expiring model/provider evaluation conclusions.
3
+
4
+ An evaluation is evidence captured at a point in time, not a permanent
5
+ capability claim. Results are kept as a small, content-free local ledger so
6
+ the UI or a router can distinguish a fresh ``passed`` result from an expired
7
+ one. A provider health failure invalidates matching conclusions immediately;
8
+ the health probe can show recovery, but only a fresh evaluation restores
9
+ ``passed``.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import math
15
+ import os
16
+ from pathlib import Path
17
+ import threading
18
+ import time
19
+
20
+ DEFAULT_TTL_SECONDS = 24 * 60 * 60
21
+ _LOCK = threading.RLock()
22
+
23
+
24
+ def _path() -> Path:
25
+ from . import paths
26
+ return Path(paths.DATA_DIR) / "evaluations.json"
27
+
28
+
29
+ def ttl_seconds() -> float:
30
+ try:
31
+ return max(60.0, float(os.environ.get(
32
+ "TUTTI_EVALUATION_TTL_S", DEFAULT_TTL_SECONDS)))
33
+ except (TypeError, ValueError):
34
+ return float(DEFAULT_TTL_SECONDS)
35
+
36
+
37
+ def _read() -> dict:
38
+ try:
39
+ value = json.loads(_path().read_text(encoding="utf-8"))
40
+ return value if isinstance(value, dict) else {}
41
+ except (OSError, json.JSONDecodeError):
42
+ return {}
43
+
44
+
45
+ def _write(value: dict) -> None:
46
+ path = _path()
47
+ try:
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ tmp = path.with_suffix(".tmp")
50
+ tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2),
51
+ encoding="utf-8")
52
+ tmp.replace(path)
53
+ except OSError:
54
+ # Evaluation is telemetry. A read-only data directory must not turn
55
+ # a successful model call into a task failure.
56
+ pass
57
+
58
+
59
+ def key(kind: str, provider_id: str, model: str = "", protocol: str = "",
60
+ key_id: str = "") -> str:
61
+ base = "%s|%s|%s" % (str(kind or "provider"),
62
+ str(provider_id or ""), str(model or ""))
63
+ suffix = "|".join((str(protocol or ""), str(key_id or "")))
64
+ return base if not suffix.strip("|") else base + "|" + suffix
65
+
66
+
67
+ def decorate(result: dict, *, now=None) -> dict:
68
+ """Attach an evaluated-at timestamp and expiry to an API response."""
69
+ out = dict(result or {})
70
+ now = time.time() if now is None else float(now)
71
+ out["evaluated_at"] = now
72
+ out["expires_at"] = now + ttl_seconds()
73
+ out["fresh"] = True
74
+ return out
75
+
76
+
77
+ def record(kind: str, provider_id: str, model: str, result: dict,
78
+ *, protocol: str = "", requested_model: str = "",
79
+ served_model: str = "", key_id: str = "") -> dict:
80
+ """Persist safe metadata for one completed evaluation and return it."""
81
+ out = decorate(result)
82
+ entry = {
83
+ "kind": str(kind or "provider"),
84
+ "provider_id": str(provider_id or ""),
85
+ "model": str(model or ""),
86
+ "status": str(out.get("status") or "failed"),
87
+ "ok": bool(out.get("ok")),
88
+ "evaluated_at": out["evaluated_at"],
89
+ "expires_at": out["expires_at"],
90
+ "protocol": str(protocol or ""),
91
+ "key_id": str(key_id or result.get("key_id") or ""),
92
+ "requested_model": str(requested_model or model or ""),
93
+ "served_model": str(served_model or ""),
94
+ }
95
+ with _LOCK:
96
+ data = _read()
97
+ data.setdefault("entries", {})[key(
98
+ kind, provider_id, model, protocol, key_id or result.get("key_id") or "")] = entry
99
+ _write(data)
100
+ return out
101
+
102
+
103
+ def invalidate_provider(provider_id: str, model: str = "",
104
+ *, reason: str = "health_failure") -> None:
105
+ """Invalidate one model, or all conclusions, after a health event."""
106
+ provider_id = str(provider_id or "").strip()
107
+ if not provider_id:
108
+ return
109
+ with _LOCK:
110
+ data = _read()
111
+ at = time.time()
112
+ invalidations = data.setdefault("invalidated", {})
113
+ invalidations[provider_id] = {
114
+ "at": at, "reason": str(reason or "health_failure")[:80]}
115
+ if model:
116
+ invalidations[key("model", provider_id, model)] = {
117
+ "at": at, "reason": str(reason or "health_failure")[:80]}
118
+ _write(data)
119
+
120
+
121
+ def _safe_float(value, default=0.0) -> float:
122
+ try:
123
+ result = float(value)
124
+ return result if math.isfinite(result) else float(default)
125
+ except (TypeError, ValueError):
126
+ return float(default)
127
+
128
+
129
+ def get(kind: str, provider_id: str, model: str = "", *, now=None):
130
+ """Read an evaluation, returning a stale marker after TTL/health expiry."""
131
+ now = time.time() if now is None else float(now)
132
+ with _LOCK:
133
+ data = _read()
134
+ entries = data.get("entries") or {}
135
+ base_key = key(kind, provider_id, model)
136
+ entry = entries.get(base_key)
137
+ matches = [value for name, value in entries.items()
138
+ if str(name).startswith(base_key + "|") and isinstance(value, dict)]
139
+ candidates = ([entry] if isinstance(entry, dict) else []) + matches
140
+ entry = max(candidates, key=lambda value: _safe_float(value.get("evaluated_at")),
141
+ default=None)
142
+ if not isinstance(entry, dict):
143
+ return None
144
+ out = dict(entry)
145
+ invalidated = data.get("invalidated") or {}
146
+ exact_invalid = invalidated.get(key(kind, provider_id, model)) or {}
147
+ provider_invalid = invalidated.get(str(provider_id or "")) or {}
148
+ invalid = max((candidate for candidate in (exact_invalid, provider_invalid)
149
+ if isinstance(candidate, dict)),
150
+ key=lambda candidate: _safe_float(candidate.get("at")),
151
+ default={})
152
+ invalid_at = _safe_float(invalid.get("at"))
153
+ expires_at = _safe_float(out.get("expires_at"))
154
+ stale_reason = ""
155
+ if invalid_at and invalid_at >= _safe_float(out.get("evaluated_at")):
156
+ stale_reason = str(invalid.get("reason") or "health_failure")
157
+ elif expires_at <= now:
158
+ stale_reason = "ttl_expired"
159
+ if stale_reason:
160
+ out["fresh"] = False
161
+ out["ok"] = False
162
+ out["status"] = "stale"
163
+ out["stale_reason"] = stale_reason
164
+ else:
165
+ out["fresh"] = True
166
+ return out
167
+
168
+
169
+ def reset_for_tests() -> None:
170
+ with _LOCK:
171
+ try:
172
+ _path().unlink()
173
+ except OSError:
174
+ pass