codebee 0.1.101 → 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 (134) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -21
  3. package/README.md +3 -6
  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 -426
  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/board.py +21 -17
  24. package/app/core/bookmeta.py +1036 -1036
  25. package/app/core/bookmeta_catalog.py +180 -180
  26. package/app/core/branching.py +137 -137
  27. package/app/core/builtin_agent.py +13 -4
  28. package/app/core/capability.py +144 -144
  29. package/app/core/catalog.py +28 -6
  30. package/app/core/chaptersafe.py +32 -32
  31. package/app/core/checkpoints.py +117 -31
  32. package/app/core/cleanup.py +303 -303
  33. package/app/core/command_auth.py +302 -302
  34. package/app/core/compaction.py +201 -201
  35. package/app/core/contracts.py +255 -255
  36. package/app/core/covergen.py +356 -356
  37. package/app/core/defectretro.py +43 -43
  38. package/app/core/diagnostics.py +114 -114
  39. package/app/core/dispatch.py +1 -1
  40. package/app/core/dispatch_log.py +128 -128
  41. package/app/core/env_scrub.py +85 -85
  42. package/app/core/errorlog.py +137 -137
  43. package/app/core/eval_matrix.py +91 -82
  44. package/app/core/evaluation.py +174 -174
  45. package/app/core/failure_notify.py +290 -290
  46. package/app/core/flow_graph.py +33 -33
  47. package/app/core/goal_service.py +167 -167
  48. package/app/core/health.py +389 -389
  49. package/app/core/history.py +38 -38
  50. package/app/core/hooks.py +162 -162
  51. package/app/core/knowledge_pipeline.py +86 -86
  52. package/app/core/manager.py +2385 -2385
  53. package/app/core/mcp_client.py +7 -2
  54. package/app/core/mocks.py +64 -64
  55. package/app/core/notify.py +201 -201
  56. package/app/core/novel_quality.py +181 -181
  57. package/app/core/openapi.py +217 -209
  58. package/app/core/operations.py +228 -228
  59. package/app/core/paihang.py +145 -145
  60. package/app/core/pipeline.py +6 -3
  61. package/app/core/policy.py +9 -0
  62. package/app/core/portguard.py +111 -111
  63. package/app/core/presence.py +77 -77
  64. package/app/core/preview.py +293 -293
  65. package/app/core/project_memory.py +206 -206
  66. package/app/core/prompt_guard.py +78 -78
  67. package/app/core/publish/__init__.py +7 -7
  68. package/app/core/publish/auto.py +659 -562
  69. package/app/core/publish/chaprows.py +138 -138
  70. package/app/core/publish/fanqie.py +16 -10
  71. package/app/core/publish/flow.py +29 -1
  72. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  73. package/app/core/publish/manager.py +1185 -1177
  74. package/app/core/publish/qimao.py +193 -193
  75. package/app/core/publish/volumes.py +108 -108
  76. package/app/core/publish/ws.py +139 -139
  77. package/app/core/quality_gate.py +201 -201
  78. package/app/core/redact.py +38 -38
  79. package/app/core/repeat_guard.py +129 -129
  80. package/app/core/retrieval.py +137 -137
  81. package/app/core/sandbox_runtime.py +95 -0
  82. package/app/core/selfupdate.py +384 -384
  83. package/app/core/session_log.py +161 -161
  84. package/app/core/sessions.py +312 -312
  85. package/app/core/settings.py +215 -215
  86. package/app/core/settings_schema.py +282 -282
  87. package/app/core/share_page.py +114 -114
  88. package/app/core/skill_scan.py +139 -139
  89. package/app/core/skillpacks/market/character-bible.md +66 -66
  90. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  91. package/app/core/skillpacks/market/git-workflow.md +57 -57
  92. package/app/core/skillpacks/market/release-notes.md +72 -72
  93. package/app/core/skillpacks/market/weekly-report.md +71 -71
  94. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  95. package/app/core/step_runner.py +132 -132
  96. package/app/core/store.py +13 -3
  97. package/app/core/story_tracking.py +275 -275
  98. package/app/core/task_compile.py +199 -199
  99. package/app/core/telemetry.py +292 -292
  100. package/app/core/tlsctx.py +64 -64
  101. package/app/core/token_meter.py +147 -147
  102. package/app/core/tool_outputs.py +107 -107
  103. package/app/core/tracing.py +207 -207
  104. package/app/core/upstream.py +41 -41
  105. package/app/core/usage.py +1038 -1038
  106. package/app/core/volumes.py +389 -389
  107. package/app/core/webhooks.py +89 -89
  108. package/app/core/wxdigest.py +865 -865
  109. package/app/core/zentao.py +2692 -2692
  110. package/app/main.py +1 -0
  111. package/app/mcp_server.py +244 -244
  112. package/app/output/_createbook_body.json +1 -0
  113. package/app/output/_probe_fq_click.py +50 -0
  114. package/app/output/_probe_fq_form.py +42 -0
  115. package/app/output/_probe_fq_modal.py +55 -0
  116. package/app/output/_probe_fq_points.py +71 -0
  117. package/app/output/_probe_fq_submit.py +34 -0
  118. package/app/output/_probe_fq_watch.py +62 -0
  119. package/app/output/workbench-clean-preview.jpg +0 -0
  120. package/app/output/workbench-reference-render.png +0 -0
  121. package/app/output/workbench-shell.png +0 -0
  122. package/app/pick_dialog.py +110 -110
  123. package/app/ui/app.js +165 -29
  124. package/app/ui/artdiff.js +74 -74
  125. package/app/ui/hive3d.js +491 -223
  126. package/app/ui/i18n.js +18 -1
  127. package/app/ui/icons/bee.svg +79 -79
  128. package/app/ui/index.html +24 -4
  129. package/app/ui/manifest.json +16 -16
  130. package/app/ui/qrcode.js +2297 -2297
  131. package/app/ui/style.css +548 -18
  132. package/bin/tutti.js +147 -147
  133. package/package.json +1 -1
  134. package/tools/wx_reader.py +261 -261
@@ -1,183 +1,183 @@
1
- # -*- coding: utf-8 -*-
2
- """蜂巢保全:服务无论以何种方式退出,本服务拉起的蜜蜂 CLI 一起带走。
3
-
4
- 背景(2026-09-23 用户定案):CodeBee 关闭后——无论正常退出、Ctrl+C、崩溃
5
- 还是被 taskkill 硬杀——在跑的 CLI/蜜蜂(codex/claude/kimi 等)必须一起杀
6
- 掉,否则孤儿进程把内存吃满。当天 16:50 一次服务重启就留过一只孤儿
7
- claude.exe(直到管道断裂才饿死)。
8
-
9
- Windows:Job Object(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)。蜜蜂 Popen 一出生
10
- 就 AssignProcessToJobObject 挂进蜂巢,整棵子进程树自动继承巢内身份;服务进
11
- 程死亡(哪怕 os._exit 跳过 atexit、被 TerminateProcess 断头)时内核关闭巢柄,
12
- 负责把巢内进程全部收走——这是唯一能覆盖「任意死法」的机制。
13
- - 升级重启的新实例(selfupdate.relaunch)不经 run_process、不挂巢,得以
14
- 存活——否则就地重启等于自杀。桌面蜜蜂、explorer、cmd /k 交互终端同理豁免。
15
- POSIX(含 Mac):没有 Job Object 也没有 Linux 的 prctl PDEATHSIG,靠登记 +
16
- atexit 清扫兜底:run_process 的子进程本就 start_new_session(自成进程组,
17
- pgid==pid),sweep 用 killpg 连树杀。正常退出 / Ctrl+C / SIGTERM/SIGHUP
18
- (main.py 已把它们转成 SystemExit,atexit 照跑)全覆盖;仅 kill -9 无解
19
- (任何机制都救不了,属已知边界)。
20
-
21
- 本模块绝不抛异常:挂巢失败只降级为旧行为(取消/超时收尸仍在),绝不影响
22
- 蜜蜂正常起跑。不依赖 pywin32,ctypes 直调 kernel32。
23
- """
24
- from __future__ import annotations
25
-
26
- import atexit
27
- import collections
28
- import os
29
- import signal
30
- import weakref
31
-
32
- # 近期蜜蜂登记:只保最近若干条防无限增长。Windows 上 Job 是主机制、登记只是
33
- # atexit 兜底的补充;POSIX 上登记是主机制(sweep 全靠它)。
34
- _RECENT = collections.deque(maxlen=256)
35
-
36
-
37
- # ---------------------------------------------------------------- Windows 巢
38
- if os.name == "nt":
39
- import ctypes
40
- from ctypes import wintypes
41
-
42
- _LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
43
- _INFO_JOB_OBJECT_EXTENDED_LIMIT = 9
44
- _STILL_ACTIVE = 259
45
-
46
- class _IO_COUNTERS(ctypes.Structure):
47
- _fields_ = [(n, ctypes.c_ulonglong) for n in (
48
- "ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
49
- "ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
50
-
51
- class _JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
52
- _fields_ = [
53
- ("PerProcessUserTimeLimit", ctypes.c_longlong),
54
- ("PerJobUserTimeLimit", ctypes.c_longlong),
55
- ("LimitFlags", wintypes.DWORD),
56
- ("MinimumWorkingSetSize", ctypes.c_size_t),
57
- ("MaximumWorkingSetSize", ctypes.c_size_t),
58
- ("ActiveProcessLimit", wintypes.DWORD),
59
- ("Affinity", ctypes.c_size_t), # ULONG_PTR
60
- ("PriorityClass", wintypes.DWORD),
61
- ("SchedulingClass", wintypes.DWORD),
62
- ]
63
-
64
- class _JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
65
- _fields_ = [
66
- ("BasicLimitInformation", _JOBOBJECT_BASIC_LIMIT_INFORMATION),
67
- ("IoInfo", _IO_COUNTERS),
68
- ("ProcessMemoryLimit", ctypes.c_size_t),
69
- ("JobMemoryLimit", ctypes.c_size_t),
70
- ("PeakProcessMemoryUsed", ctypes.c_size_t),
71
- ("PeakJobMemoryUsed", ctypes.c_size_t),
72
- ]
73
-
74
- _kernel32 = ctypes.windll.kernel32
75
-
76
- class BeeNest:
77
- """一个 KILL_ON_JOB_CLOSE 巢。生产用模块单例 NEST;测试自建私有巢。"""
78
-
79
- def __init__(self):
80
- self._handle = None
81
- self.assigned = 0
82
-
83
- def _ensure_job(self):
84
- if self._handle:
85
- return self._handle
86
- h = _kernel32.CreateJobObjectW(None, None)
87
- if not h:
88
- return None
89
- info = _JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
90
- info.BasicLimitInformation.LimitFlags = _LIMIT_KILL_ON_JOB_CLOSE
91
- if not _kernel32.SetInformationJobObject(
92
- h, _INFO_JOB_OBJECT_EXTENDED_LIMIT,
93
- ctypes.byref(info), ctypes.sizeof(info)):
94
- _kernel32.CloseHandle(h)
95
- return None
96
- self._handle = h
97
- return h
98
-
99
- def adopt(self, proc):
100
- h = self._ensure_job()
101
- if not h:
102
- return
103
- handle = getattr(proc, "_handle", None)
104
- if not handle:
105
- return
106
- # 子进程树(cmd /c 垫片 → claude.exe → 其孙)自动继承巢内身份,
107
- # 只需挂直系子进程。Win8+ 支持嵌套 Job,外层有别的 Job 也不碍事。
108
- if _kernel32.AssignProcessToJobObject(h, int(handle)):
109
- self.assigned += 1
110
-
111
- def close(self):
112
- """主动关巢:巢内进程全部被内核收走(等价于服务进程死亡)。"""
113
- if self._handle:
114
- try:
115
- _kernel32.CloseHandle(self._handle)
116
- except Exception:
117
- pass
118
- self._handle = None
119
-
120
- else:
121
- class BeeNest:
122
- """POSIX 占位巢:杀全靠 sweep()(登记 + killpg),close 无事可做。"""
123
-
124
- def __init__(self):
125
- self.assigned = 0
126
-
127
- def adopt(self, proc):
128
- self.assigned += 1
129
-
130
- def close(self):
131
- pass
132
-
133
-
134
- NEST = BeeNest()
135
-
136
-
137
- def adopt(proc):
138
- """把蜜蜂进程挂巢:服务死则蜜蜂亡。绝不抛异常、绝不拖慢起跑。"""
139
- if proc is None:
140
- return
141
- try:
142
- _RECENT.append((proc.pid, weakref.ref(proc)))
143
- NEST.adopt(proc)
144
- except Exception:
145
- pass
146
-
147
-
148
- def sweep():
149
- """退出清扫:登记在册且仍存活的蜜蜂连树杀。
150
-
151
- POSIX 主机制(atexit 在正常退出/Ctrl+C/SIGTERM→SystemExit 时都会跑);
152
- Windows 只是 Job 的兜底(Job 建不出来或句柄异常时仍能杀到登记的直系
153
- 进程,孙进程交给既有 taskkill /T 收尸路径)。Windows 端拿不到 Popen
154
- 存活证据的条目不盲杀——防 PID 复用误伤无辜进程(反正有 Job 兜底)。
155
- """
156
- while _RECENT:
157
- pid, ref = _RECENT.popleft()
158
- proc = ref() if ref else None
159
- if proc is not None:
160
- try:
161
- if proc.poll() is not None:
162
- continue # 已退出,跳过
163
- except Exception:
164
- pass
165
- if os.name == "nt":
166
- if proc is None:
167
- continue
168
- try:
169
- os.kill(pid, signal.SIGTERM) # Windows 语义 = TerminateProcess
170
- except Exception:
171
- pass
172
- else:
173
- # run_process 子进程 start_new_session:pgid==pid,killpg 连树杀
174
- try:
175
- os.killpg(pid, signal.SIGKILL)
176
- except Exception:
177
- try:
178
- os.kill(pid, signal.SIGKILL)
179
- except Exception:
180
- pass
181
-
182
-
183
- atexit.register(sweep)
1
+ # -*- coding: utf-8 -*-
2
+ """蜂巢保全:服务无论以何种方式退出,本服务拉起的蜜蜂 CLI 一起带走。
3
+
4
+ 背景(2026-09-23 用户定案):CodeBee 关闭后——无论正常退出、Ctrl+C、崩溃
5
+ 还是被 taskkill 硬杀——在跑的 CLI/蜜蜂(codex/claude/kimi 等)必须一起杀
6
+ 掉,否则孤儿进程把内存吃满。当天 16:50 一次服务重启就留过一只孤儿
7
+ claude.exe(直到管道断裂才饿死)。
8
+
9
+ Windows:Job Object(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)。蜜蜂 Popen 一出生
10
+ 就 AssignProcessToJobObject 挂进蜂巢,整棵子进程树自动继承巢内身份;服务进
11
+ 程死亡(哪怕 os._exit 跳过 atexit、被 TerminateProcess 断头)时内核关闭巢柄,
12
+ 负责把巢内进程全部收走——这是唯一能覆盖「任意死法」的机制。
13
+ - 升级重启的新实例(selfupdate.relaunch)不经 run_process、不挂巢,得以
14
+ 存活——否则就地重启等于自杀。桌面蜜蜂、explorer、cmd /k 交互终端同理豁免。
15
+ POSIX(含 Mac):没有 Job Object 也没有 Linux 的 prctl PDEATHSIG,靠登记 +
16
+ atexit 清扫兜底:run_process 的子进程本就 start_new_session(自成进程组,
17
+ pgid==pid),sweep 用 killpg 连树杀。正常退出 / Ctrl+C / SIGTERM/SIGHUP
18
+ (main.py 已把它们转成 SystemExit,atexit 照跑)全覆盖;仅 kill -9 无解
19
+ (任何机制都救不了,属已知边界)。
20
+
21
+ 本模块绝不抛异常:挂巢失败只降级为旧行为(取消/超时收尸仍在),绝不影响
22
+ 蜜蜂正常起跑。不依赖 pywin32,ctypes 直调 kernel32。
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import atexit
27
+ import collections
28
+ import os
29
+ import signal
30
+ import weakref
31
+
32
+ # 近期蜜蜂登记:只保最近若干条防无限增长。Windows 上 Job 是主机制、登记只是
33
+ # atexit 兜底的补充;POSIX 上登记是主机制(sweep 全靠它)。
34
+ _RECENT = collections.deque(maxlen=256)
35
+
36
+
37
+ # ---------------------------------------------------------------- Windows 巢
38
+ if os.name == "nt":
39
+ import ctypes
40
+ from ctypes import wintypes
41
+
42
+ _LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
43
+ _INFO_JOB_OBJECT_EXTENDED_LIMIT = 9
44
+ _STILL_ACTIVE = 259
45
+
46
+ class _IO_COUNTERS(ctypes.Structure):
47
+ _fields_ = [(n, ctypes.c_ulonglong) for n in (
48
+ "ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
49
+ "ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
50
+
51
+ class _JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
52
+ _fields_ = [
53
+ ("PerProcessUserTimeLimit", ctypes.c_longlong),
54
+ ("PerJobUserTimeLimit", ctypes.c_longlong),
55
+ ("LimitFlags", wintypes.DWORD),
56
+ ("MinimumWorkingSetSize", ctypes.c_size_t),
57
+ ("MaximumWorkingSetSize", ctypes.c_size_t),
58
+ ("ActiveProcessLimit", wintypes.DWORD),
59
+ ("Affinity", ctypes.c_size_t), # ULONG_PTR
60
+ ("PriorityClass", wintypes.DWORD),
61
+ ("SchedulingClass", wintypes.DWORD),
62
+ ]
63
+
64
+ class _JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
65
+ _fields_ = [
66
+ ("BasicLimitInformation", _JOBOBJECT_BASIC_LIMIT_INFORMATION),
67
+ ("IoInfo", _IO_COUNTERS),
68
+ ("ProcessMemoryLimit", ctypes.c_size_t),
69
+ ("JobMemoryLimit", ctypes.c_size_t),
70
+ ("PeakProcessMemoryUsed", ctypes.c_size_t),
71
+ ("PeakJobMemoryUsed", ctypes.c_size_t),
72
+ ]
73
+
74
+ _kernel32 = ctypes.windll.kernel32
75
+
76
+ class BeeNest:
77
+ """一个 KILL_ON_JOB_CLOSE 巢。生产用模块单例 NEST;测试自建私有巢。"""
78
+
79
+ def __init__(self):
80
+ self._handle = None
81
+ self.assigned = 0
82
+
83
+ def _ensure_job(self):
84
+ if self._handle:
85
+ return self._handle
86
+ h = _kernel32.CreateJobObjectW(None, None)
87
+ if not h:
88
+ return None
89
+ info = _JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
90
+ info.BasicLimitInformation.LimitFlags = _LIMIT_KILL_ON_JOB_CLOSE
91
+ if not _kernel32.SetInformationJobObject(
92
+ h, _INFO_JOB_OBJECT_EXTENDED_LIMIT,
93
+ ctypes.byref(info), ctypes.sizeof(info)):
94
+ _kernel32.CloseHandle(h)
95
+ return None
96
+ self._handle = h
97
+ return h
98
+
99
+ def adopt(self, proc):
100
+ h = self._ensure_job()
101
+ if not h:
102
+ return
103
+ handle = getattr(proc, "_handle", None)
104
+ if not handle:
105
+ return
106
+ # 子进程树(cmd /c 垫片 → claude.exe → 其孙)自动继承巢内身份,
107
+ # 只需挂直系子进程。Win8+ 支持嵌套 Job,外层有别的 Job 也不碍事。
108
+ if _kernel32.AssignProcessToJobObject(h, int(handle)):
109
+ self.assigned += 1
110
+
111
+ def close(self):
112
+ """主动关巢:巢内进程全部被内核收走(等价于服务进程死亡)。"""
113
+ if self._handle:
114
+ try:
115
+ _kernel32.CloseHandle(self._handle)
116
+ except Exception:
117
+ pass
118
+ self._handle = None
119
+
120
+ else:
121
+ class BeeNest:
122
+ """POSIX 占位巢:杀全靠 sweep()(登记 + killpg),close 无事可做。"""
123
+
124
+ def __init__(self):
125
+ self.assigned = 0
126
+
127
+ def adopt(self, proc):
128
+ self.assigned += 1
129
+
130
+ def close(self):
131
+ pass
132
+
133
+
134
+ NEST = BeeNest()
135
+
136
+
137
+ def adopt(proc):
138
+ """把蜜蜂进程挂巢:服务死则蜜蜂亡。绝不抛异常、绝不拖慢起跑。"""
139
+ if proc is None:
140
+ return
141
+ try:
142
+ _RECENT.append((proc.pid, weakref.ref(proc)))
143
+ NEST.adopt(proc)
144
+ except Exception:
145
+ pass
146
+
147
+
148
+ def sweep():
149
+ """退出清扫:登记在册且仍存活的蜜蜂连树杀。
150
+
151
+ POSIX 主机制(atexit 在正常退出/Ctrl+C/SIGTERM→SystemExit 时都会跑);
152
+ Windows 只是 Job 的兜底(Job 建不出来或句柄异常时仍能杀到登记的直系
153
+ 进程,孙进程交给既有 taskkill /T 收尸路径)。Windows 端拿不到 Popen
154
+ 存活证据的条目不盲杀——防 PID 复用误伤无辜进程(反正有 Job 兜底)。
155
+ """
156
+ while _RECENT:
157
+ pid, ref = _RECENT.popleft()
158
+ proc = ref() if ref else None
159
+ if proc is not None:
160
+ try:
161
+ if proc.poll() is not None:
162
+ continue # 已退出,跳过
163
+ except Exception:
164
+ pass
165
+ if os.name == "nt":
166
+ if proc is None:
167
+ continue
168
+ try:
169
+ os.kill(pid, signal.SIGTERM) # Windows 语义 = TerminateProcess
170
+ except Exception:
171
+ pass
172
+ else:
173
+ # run_process 子进程 start_new_session:pgid==pid,killpg 连树杀
174
+ try:
175
+ os.killpg(pid, signal.SIGKILL)
176
+ except Exception:
177
+ try:
178
+ os.kill(pid, signal.SIGKILL)
179
+ except Exception:
180
+ pass
181
+
182
+
183
+ atexit.register(sweep)
@@ -1,102 +1,102 @@
1
- # -*- coding: utf-8 -*-
2
- """评测基准台存储层(L0):data/eval_bench.json 的读写与「实测分软信号」。
3
-
4
- evalbench(L2,跑评测)与 dispatch(L0,模型链评分)都要摸这份数据——
5
- 读写下沉到这里,两边同层合法引用(dispatch 不能 import L2 的 evalbench,
6
- 分层政策 L_i 只准 import L_j (j<=i))。
7
-
8
- 实测分软信号口径(写入 docs/execution-standard.md 候选打分权重表):
9
- bonus = clamp((overall − 7.0) × 1.5, −4.5, +4.5)
10
- 7 分以下开始倒扣、9 分封顶 +4.5;无数据/超新鲜度记 0——缺数据不猜分。
11
- 新鲜度 14 天:评测会过期(evaluation.py 同理念),过期实测不参与排序。
12
- """
13
- from __future__ import annotations
14
-
15
- import json
16
- import threading
17
- import time
18
- from pathlib import Path
19
-
20
- from . import paths
21
-
22
- _LOCK = threading.RLock()
23
- FRESH_DAYS = 14 # 实测分新鲜度(天);0 = 永不过期(测试用)
24
- _BASELINE = 7.0 # 及格线:低于它开始倒扣
25
- _K = 1.5 # 斜率:每 1 分 ±1.5
26
- _MAX_BONUS = 4.5 # 封顶(与在线信号 ±6 同量级,翻不动硬约束)
27
-
28
-
29
- def _path():
30
- return Path(paths.DATA_DIR) / "eval_bench.json"
31
-
32
-
33
- def read_doc():
34
- with _LOCK:
35
- try:
36
- data = json.loads(_path().read_text(encoding="utf-8"))
37
- return data if isinstance(data, dict) else {}
38
- except Exception:
39
- return {}
40
-
41
-
42
- def write_doc(data):
43
- with _LOCK:
44
- _path().parent.mkdir(parents=True, exist_ok=True)
45
- tmp = _path().with_suffix(".tmp")
46
- tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
47
- tmp.replace(_path())
48
-
49
-
50
- def read_results():
51
- results = read_doc().get("results")
52
- return [r for r in results if isinstance(r, dict)] if isinstance(results, list) else []
53
-
54
-
55
- def _fresh(row, max_age_days):
56
- """行是否在新鲜度窗口内。ts 缺失/解析失败按过期处理(宁缺毋滥)。"""
57
- if max_age_days <= 0:
58
- return True
59
- try:
60
- ts = time.mktime(time.strptime(str(row.get("ts") or ""),
61
- "%Y-%m-%d %H:%M:%S"))
62
- except ValueError:
63
- return False
64
- return (time.time() - ts) <= max_age_days * 86400.0
65
-
66
-
67
- def bonus_map(max_age_days=FRESH_DAYS):
68
- """{(provider_id, model): {"overall": x, "ts": s}}——每键取最新一条
69
- 已得分记录,且必须在新鲜度窗口内。"""
70
- latest = {}
71
- for r in read_results():
72
- if not r.get("scored") or r.get("overall") is None:
73
- continue
74
- key = (str(r.get("provider_id") or ""), str(r.get("model") or ""))
75
- if not key[1]:
76
- continue
77
- prev = latest.get(key)
78
- if prev is None or str(r.get("ts") or "") > str(prev.get("ts") or ""):
79
- latest[key] = r
80
- out = {}
81
- for k, v in latest.items():
82
- if not _fresh(v, max_age_days):
83
- continue
84
- overall = float(v["overall"])
85
- out[k] = {"overall": overall, "ts": str(v.get("ts") or ""),
86
- "bonus": round(max(-_MAX_BONUS, min(_MAX_BONUS,
87
- (overall - _BASELINE) * _K)), 2)}
88
- return out
89
-
90
-
91
- def bonus_for(provider_id, model, max_age_days=FRESH_DAYS):
92
- """单条目的实测软信号。返回 (score, reason);无数据/过期 → (0.0, "")。"""
93
- try:
94
- info = bonus_map(max_age_days).get((str(provider_id or ""), str(model or "")))
95
- if not info:
96
- return 0.0, ""
97
- score = max(-_MAX_BONUS, min(_MAX_BONUS, (info["overall"] - _BASELINE) * _K))
98
- score = round(score, 2)
99
- return score, "实测 %.1f 分(%+.1f,%s)" % (
100
- info["overall"], score, (info.get("ts") or "")[:10])
101
- except Exception:
102
- return 0.0, ""
1
+ # -*- coding: utf-8 -*-
2
+ """评测基准台存储层(L0):data/eval_bench.json 的读写与「实测分软信号」。
3
+
4
+ evalbench(L2,跑评测)与 dispatch(L0,模型链评分)都要摸这份数据——
5
+ 读写下沉到这里,两边同层合法引用(dispatch 不能 import L2 的 evalbench,
6
+ 分层政策 L_i 只准 import L_j (j<=i))。
7
+
8
+ 实测分软信号口径(写入 docs/execution-standard.md 候选打分权重表):
9
+ bonus = clamp((overall − 7.0) × 1.5, −4.5, +4.5)
10
+ 7 分以下开始倒扣、9 分封顶 +4.5;无数据/超新鲜度记 0——缺数据不猜分。
11
+ 新鲜度 14 天:评测会过期(evaluation.py 同理念),过期实测不参与排序。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import threading
17
+ import time
18
+ from pathlib import Path
19
+
20
+ from . import paths
21
+
22
+ _LOCK = threading.RLock()
23
+ FRESH_DAYS = 14 # 实测分新鲜度(天);0 = 永不过期(测试用)
24
+ _BASELINE = 7.0 # 及格线:低于它开始倒扣
25
+ _K = 1.5 # 斜率:每 1 分 ±1.5
26
+ _MAX_BONUS = 4.5 # 封顶(与在线信号 ±6 同量级,翻不动硬约束)
27
+
28
+
29
+ def _path():
30
+ return Path(paths.DATA_DIR) / "eval_bench.json"
31
+
32
+
33
+ def read_doc():
34
+ with _LOCK:
35
+ try:
36
+ data = json.loads(_path().read_text(encoding="utf-8"))
37
+ return data if isinstance(data, dict) else {}
38
+ except Exception:
39
+ return {}
40
+
41
+
42
+ def write_doc(data):
43
+ with _LOCK:
44
+ _path().parent.mkdir(parents=True, exist_ok=True)
45
+ tmp = _path().with_suffix(".tmp")
46
+ tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
47
+ tmp.replace(_path())
48
+
49
+
50
+ def read_results():
51
+ results = read_doc().get("results")
52
+ return [r for r in results if isinstance(r, dict)] if isinstance(results, list) else []
53
+
54
+
55
+ def _fresh(row, max_age_days):
56
+ """行是否在新鲜度窗口内。ts 缺失/解析失败按过期处理(宁缺毋滥)。"""
57
+ if max_age_days <= 0:
58
+ return True
59
+ try:
60
+ ts = time.mktime(time.strptime(str(row.get("ts") or ""),
61
+ "%Y-%m-%d %H:%M:%S"))
62
+ except ValueError:
63
+ return False
64
+ return (time.time() - ts) <= max_age_days * 86400.0
65
+
66
+
67
+ def bonus_map(max_age_days=FRESH_DAYS):
68
+ """{(provider_id, model): {"overall": x, "ts": s}}——每键取最新一条
69
+ 已得分记录,且必须在新鲜度窗口内。"""
70
+ latest = {}
71
+ for r in read_results():
72
+ if not r.get("scored") or r.get("overall") is None:
73
+ continue
74
+ key = (str(r.get("provider_id") or ""), str(r.get("model") or ""))
75
+ if not key[1]:
76
+ continue
77
+ prev = latest.get(key)
78
+ if prev is None or str(r.get("ts") or "") > str(prev.get("ts") or ""):
79
+ latest[key] = r
80
+ out = {}
81
+ for k, v in latest.items():
82
+ if not _fresh(v, max_age_days):
83
+ continue
84
+ overall = float(v["overall"])
85
+ out[k] = {"overall": overall, "ts": str(v.get("ts") or ""),
86
+ "bonus": round(max(-_MAX_BONUS, min(_MAX_BONUS,
87
+ (overall - _BASELINE) * _K)), 2)}
88
+ return out
89
+
90
+
91
+ def bonus_for(provider_id, model, max_age_days=FRESH_DAYS):
92
+ """单条目的实测软信号。返回 (score, reason);无数据/过期 → (0.0, "")。"""
93
+ try:
94
+ info = bonus_map(max_age_days).get((str(provider_id or ""), str(model or "")))
95
+ if not info:
96
+ return 0.0, ""
97
+ score = max(-_MAX_BONUS, min(_MAX_BONUS, (info["overall"] - _BASELINE) * _K))
98
+ score = round(score, 2)
99
+ return score, "实测 %.1f 分(%+.1f,%s)" % (
100
+ info["overall"], score, (info.get("ts") or "")[:10])
101
+ except Exception:
102
+ return 0.0, ""
package/app/core/board.py CHANGED
@@ -66,22 +66,16 @@ def run_detail(run_id):
66
66
  }
67
67
 
68
68
 
69
- def _tail(step):
70
- """步骤实时流(正文优先,其次思考)的最后一行,给大屏跑马灯。"""
71
- for key in ("stream", "thinking"):
72
- v = step.get(key)
73
- if isinstance(v, str) and v.strip():
74
- lines = [ln.strip() for ln in v.strip().splitlines() if ln.strip()]
75
- if lines:
76
- return lines[-1][:_TAIL_MAX]
77
- return ""
78
-
79
-
80
- def _live_block(step):
69
+ def _live_windows(run_id, step):
81
70
  """步骤实时流窗口:尾部多行(正文优先,其次思考),供大屏看它在想什么。
82
71
 
83
72
  返回 (来源标签 code, 文本);标签是 code(output/thinking),由前端 i18n
84
- 翻译——大屏要做多语言,后端不下发中文。都没有返回 ("", "")。
73
+ 翻译——大屏要做多语言,后端不下发中文。
74
+
75
+ 内置智能体步骤边跑边把流写进步骤记录(store.stream_step);外部 CLI 步骤
76
+ 只落日志文件、run.json 里没有 stream——此前的连载全 CLI 流程在大屏上永远
77
+ 「等待实时输出…」(弹窗日志却有内容,2026-10-10 用户反馈)。故回退读日志
78
+ 尾巴:与蜂巢卡 hiveTick 同源同窗口(tail 夹取,不整读大日志)。
85
79
  """
86
80
  for key, code in (("stream", "output"), ("thinking", "thinking")):
87
81
  v = step.get(key)
@@ -91,6 +85,14 @@ def _live_block(step):
91
85
  lines = [ln for ln in lines if ln]
92
86
  if lines:
93
87
  return code, "\n".join(lines[-_TAIL_LINES:])
88
+ rel = step.get("log")
89
+ if rel and run_id:
90
+ from . import store
91
+ text = store.read_step_log(str(run_id), str(rel), tail=2400, pretty=True)
92
+ lines = [ln.strip()[:_TAIL_LINE_CHARS] for ln in (text or "").splitlines()]
93
+ lines = [ln for ln in lines if ln]
94
+ if lines:
95
+ return "output", "\n".join(lines[-_TAIL_LINES:])
94
96
  return "", ""
95
97
 
96
98
 
@@ -129,18 +131,19 @@ def _duration_s(run):
129
131
  return None
130
132
 
131
133
 
132
- def _step_view(steps, prov_map):
134
+ def _step_view(steps, prov_map, run_id=""):
133
135
  """进度概要:完成数 + 当前运行中步骤(含模型/厂商/实时流)。"""
134
136
  done = sum(1 for s in steps if s.get("status") == "done")
135
137
  cur = next((s for s in steps if s.get("status") == "running"), None)
136
138
  cur_v = None
137
139
  if cur:
138
- live_label, live_text = _live_block(cur)
140
+ live_label, live_text = _live_windows(run_id, cur)
139
141
  model = str(cur.get("model") or "")[:60]
142
+ tail = (live_text.splitlines() or [""])[-1][:_TAIL_MAX] if live_text else ""
140
143
  cur_v = {"n": cur.get("n"), "role": cur.get("role") or "",
141
144
  "agent": cur.get("agent_label") or cur.get("agent") or "",
142
145
  "summary": (cur.get("summary") or "")[:80],
143
- "tail": _tail(cur),
146
+ "tail": tail,
144
147
  "model": model, "provider": prov_map.get(model, "") if model else "",
145
148
  "live_label": live_label, "live_text": live_text}
146
149
  return done, len(steps), cur_v
@@ -167,7 +170,8 @@ def _build():
167
170
  run = latest.get(tid)
168
171
  status = t.get("status")
169
172
  if status == "running" and run and run.get("status") == "running":
170
- done, total, cur = _step_view(run.get("steps") or [], prov_map)
173
+ done, total, cur = _step_view(run.get("steps") or [], prov_map,
174
+ run_id=run.get("id"))
171
175
  running.append({
172
176
  "task_id": tid, "run_id": run.get("id"),
173
177
  "title": t.get("title") or "", "goal": (t.get("goal") or "")[:120],