dsh-better-tips 0.12.1 → 0.13.0
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.
- package/CHANGELOG.md +23 -0
- package/README.md +54 -122
- package/client.js +268 -124
- package/index.js +202 -12
- package/package.json +1 -1
- package/test/client.test.mjs +677 -188
- package/test/host.test.mjs +417 -3
package/client.js
CHANGED
|
@@ -10,32 +10,36 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
const zh = {
|
|
11
11
|
'ui.authProtected': '认证错误正文已隐藏,避免回显凭据。请核对密钥、接口地址和模型权限。',
|
|
12
12
|
'ui.authBodyProtected': '认证响应正文已受保护;仅保留安全的状态与请求标识。',
|
|
13
|
-
'ui.details': '
|
|
13
|
+
'ui.details': '底层原始详情', 'ui.bottom': '底层', 'ui.unconfirmed': '关联未确认',
|
|
14
14
|
'ui.candidates': '候选捕获 · {count}', 'ui.loading': '加载中',
|
|
15
15
|
'ui.empty': '未取得额外网络诊断', 'ui.disabled': '额外网络诊断已关闭', 'ui.captureUnavailable': '额外网络诊断不可用', 'ui.loadFailed': '详情读取失败 · {message}',
|
|
16
16
|
'ui.refresh': '重新读取', 'ui.bodyPending': '响应正文读取中', 'ui.bodyEmpty': '响应正文为空',
|
|
17
17
|
'ui.bodyTruncated': '响应正文已截断', 'ui.bodyUnreadable': '响应正文读取失败',
|
|
18
|
-
'ui.copyError': '复制完整错误', 'ui.copyCoordinate': '复制日志坐标', 'ui.copyPath': '复制路径',
|
|
18
|
+
'ui.copyCapture': '复制截获错误', 'ui.copyError': '复制完整错误', 'ui.copyCoordinate': '复制日志坐标', 'ui.copyPath': '复制路径',
|
|
19
19
|
'ui.copied': '已复制', 'ui.copyFailed': '复制失败,请手动选择文本', 'ui.copying': '复制中',
|
|
20
20
|
'ui.error': '失败', 'ui.stopped': '已停止', 'ui.retry': '重试',
|
|
21
21
|
'ui.retried': '已重试', 'ui.cancelled': '重试取消', 'ui.scheduled': '等待重试',
|
|
22
22
|
'compact.title': '上下文已压缩', 'compact.completed': '{items} 条 · ~{tokens} tokens',
|
|
23
23
|
'compact.running': '压缩中', 'compact.done': '压缩完成',
|
|
24
24
|
'compact.failed': '压缩失败', 'compact.failedAuto': '自动压缩失败',
|
|
25
|
+
'compact.recordMissing': '本次压缩的具体失败记录尚未加载,当前仅有命令提示。', 'compact.recordFailed': '本次压缩失败记录读取失败。',
|
|
26
|
+
'compact.errorProtected': '压缩失败;持久化诊断缺少错误类型,正文已隐藏以避免回显凭据。',
|
|
25
27
|
'compact.attempt': '尝试 {index}',
|
|
26
28
|
'ui.expand': '错误详情', 'ui.original': 'DSH 原生报错', 'ui.network': '插件捕获的底层报错',
|
|
27
29
|
'ui.nativeExcerpt': 'DSH 原生报错 · 摘要', 'ui.overviewSource': '插件整理 · 重试概览', 'ui.open': '展开', 'ui.close': '收起',
|
|
30
|
+
'ui.earlierErrors': '更早的报错 · {count}', 'ui.earlierBlock': '更早的报错',
|
|
28
31
|
'ui.history': '重试记录 · {count}', 'ui.record': '失败记录 {index}',
|
|
29
|
-
'ui.
|
|
32
|
+
'ui.categoryOverview': '类别概览', 'ui.different': '{count} 类错误',
|
|
30
33
|
'ui.attempts': '请求尝试 · {count}', 'ui.logSection': '日志定位',
|
|
31
34
|
'ui.timingMatch': '按时间关联 · 未确认请求身份', 'ui.wait': '计划等待 {ms} ms',
|
|
32
35
|
'ui.historyNote': '按发生顺序;同一重试的等待与开始状态仅记一次。',
|
|
33
|
-
'report.copy': '
|
|
34
|
-
'report.fallback': '复制失败,可选择下方报告文本',
|
|
36
|
+
'report.copy': '一键复制排障报告', 'report.building': '生成中', 'report.partial': '已复制 · 部分信息缺失',
|
|
37
|
+
'report.fallback': '复制失败,可选择下方报告文本',
|
|
35
38
|
'report.notice': '分享前检查:报错或响应可能回显输入;仅过滤常见凭据,不保证清除个人信息。',
|
|
36
39
|
'compact.usage': '摘要请求用量(非会话总量)', 'log.contextWarning': '相邻事件可能含会话内容,分享前请检查。',
|
|
37
40
|
'report.responseState': '响应状态',
|
|
38
41
|
'report.intro': '记录于{time}:使用“{provider}”渠道的“{model}”模型时遇到{error}。',
|
|
42
|
+
'report.plugins': '已启用第三方插件', 'report.pluginsPartial': '部分插件信息未能确认', 'report.pluginsUnavailable': '清单读取失败或不可用', 'report.pluginsNone': '无',
|
|
39
43
|
'report.missing': '未记录', 'report.version': '截止生成本报告时', 'report.native': 'DSH 原生报错', 'report.underlying': '插件捕获的底层报错',
|
|
40
44
|
'report.noEvidence': '未取得唯一匹配的底层证据', 'report.ambiguous': '有多个候选,未混入报告', 'report.readFailed': '读取失败',
|
|
41
45
|
'report.unconfirmed': '同一时间附近捕获到以下报错:', 'report.summaryError': '摘要请求报错',
|
|
@@ -47,32 +51,36 @@ window.__ModuleLoader__.load({
|
|
|
47
51
|
const en = {
|
|
48
52
|
'ui.authProtected': 'Authentication error text is hidden to avoid echoing credentials. Check the key, endpoint and model permissions.',
|
|
49
53
|
'ui.authBodyProtected': 'Authentication response text is protected; only safe status and request identifiers are retained.',
|
|
50
|
-
'ui.details': '
|
|
54
|
+
'ui.details': 'Underlying raw details', 'ui.bottom': 'Underlying', 'ui.unconfirmed': 'Unconfirmed match',
|
|
51
55
|
'ui.candidates': 'Candidate captures · {count}', 'ui.loading': 'Loading',
|
|
52
56
|
'ui.empty': 'No additional network diagnosis', 'ui.disabled': 'Additional network diagnosis is disabled', 'ui.captureUnavailable': 'Additional network diagnosis unavailable', 'ui.loadFailed': 'Details unavailable · {message}',
|
|
53
57
|
'ui.refresh': 'Read again', 'ui.bodyPending': 'Reading response body', 'ui.bodyEmpty': 'Response body is empty',
|
|
54
58
|
'ui.bodyTruncated': 'Response body truncated', 'ui.bodyUnreadable': 'Response body read failed',
|
|
55
|
-
'ui.copyError': 'Copy full error', 'ui.copyCoordinate': 'Copy log coordinates', 'ui.copyPath': 'Copy path',
|
|
59
|
+
'ui.copyCapture': 'Copy captured error', 'ui.copyError': 'Copy full error', 'ui.copyCoordinate': 'Copy log coordinates', 'ui.copyPath': 'Copy path',
|
|
56
60
|
'ui.copied': 'Copied', 'ui.copyFailed': 'Copy failed; select the text manually', 'ui.copying': 'Copying',
|
|
57
61
|
'ui.error': 'Failed', 'ui.stopped': 'Stopped', 'ui.retry': 'Retrying',
|
|
58
62
|
'ui.retried': 'Retried', 'ui.cancelled': 'Retry cancelled', 'ui.scheduled': 'Retry scheduled',
|
|
59
63
|
'compact.title': 'Context compacted', 'compact.completed': '{items} items · ~{tokens} tokens',
|
|
60
64
|
'compact.running': 'Compacting', 'compact.done': 'Compaction complete',
|
|
61
65
|
'compact.failed': 'Compaction failed', 'compact.failedAuto': 'Automatic compaction failed',
|
|
66
|
+
'compact.recordMissing': 'The specific failure record for this compaction is not loaded; only the command notice is available.', 'compact.recordFailed': 'Could not read this compaction failure record.',
|
|
67
|
+
'compact.errorProtected': 'Compaction failed; durable diagnosis has no error type, so its text is withheld to avoid credential echoes.',
|
|
62
68
|
'compact.attempt': 'Attempt {index}',
|
|
63
69
|
'ui.expand': 'Error details', 'ui.original': 'DSH native error', 'ui.network': 'Plugin-captured underlying error',
|
|
64
70
|
'ui.nativeExcerpt': 'DSH native error · excerpt', 'ui.overviewSource': 'Plugin summary · retries', 'ui.open': 'Expand', 'ui.close': 'Collapse',
|
|
71
|
+
'ui.earlierErrors': 'Earlier errors · {count}', 'ui.earlierBlock': 'Earlier error',
|
|
65
72
|
'ui.history': 'Retry history · {count}', 'ui.record': 'Failure record {index}',
|
|
66
|
-
'ui.
|
|
73
|
+
'ui.categoryOverview': 'Category overview', 'ui.different': '{count} error categories',
|
|
67
74
|
'ui.attempts': 'Request attempts · {count}', 'ui.logSection': 'Log location',
|
|
68
75
|
'ui.timingMatch': 'Time correlation · request identity unconfirmed', 'ui.wait': 'planned wait {ms} ms',
|
|
69
76
|
'ui.historyNote': 'Chronological; scheduled and started states of one retry count once.',
|
|
70
77
|
'report.copy': 'Copy troubleshooting report', 'report.building': 'Preparing', 'report.partial': 'Copied · some information missing',
|
|
71
|
-
'report.fallback': 'Copy failed; select the report below',
|
|
78
|
+
'report.fallback': 'Copy failed; select the report below',
|
|
72
79
|
'report.notice': 'Review before sharing: errors/responses may echo input. Common credential filtering is not personal-data removal.',
|
|
73
80
|
'compact.usage': 'Summary request usage (not session total)', 'log.contextWarning': 'Nearby events may contain conversation content. Review before sharing.',
|
|
74
81
|
'report.responseState': 'Response state',
|
|
75
82
|
'report.intro': 'At {time}, model “{model}” via provider “{provider}” returned {error}.',
|
|
83
|
+
'report.plugins': 'Enabled third-party plugins', 'report.pluginsPartial': 'Some plugin information could not be verified', 'report.pluginsUnavailable': 'Inventory unavailable or read failed', 'report.pluginsNone': 'none',
|
|
76
84
|
'report.missing': 'not recorded', 'report.version': 'As of this report', 'report.native': 'DSH native error', 'report.underlying': 'Plugin-captured underlying error',
|
|
77
85
|
'report.noEvidence': 'No uniquely matched underlying evidence', 'report.ambiguous': 'Multiple candidates omitted', 'report.readFailed': 'Read failed',
|
|
78
86
|
'report.unconfirmed': 'The following was captured around the same time:', 'report.summaryError': 'Summary request error',
|
|
@@ -193,13 +201,13 @@ window.__ModuleLoader__.load({
|
|
|
193
201
|
'.bt_error[open]>summary:before,.bt_compact[open]>summary:before{content:"⌄"}',
|
|
194
202
|
'.bt_title{flex:none;color:var(--dsw-alias-label-secondary)}',
|
|
195
203
|
'.bt_error .bt_primary{color:var(--dsw-alias-state-error-primary)}',
|
|
196
|
-
'.bt_primary{font:inherit;font-family:ui-monospace,monospace;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}',
|
|
204
|
+
'.bt_primary{color:var(--dsw-alias-state-error-primary);font:inherit;font-family:ui-monospace,monospace;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}',
|
|
197
205
|
'.bt_logOpen{flex:1 1 100%;min-width:0}',
|
|
198
206
|
'.bt_actions{display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;min-width:0}',
|
|
199
|
-
'.bt_action,.bt_logButton{border:
|
|
200
|
-
'.bt_action:hover,.bt_logButton:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}',
|
|
207
|
+
'.bt_action,.bt_logButton{display:inline-flex;align-items:center;justify-content:center;gap:4px;border:1px solid var(--dsw-alias-border-l1);border-radius:3px;background:transparent;color:var(--dsw-alias-label-primary);font:400 12px/1.4 system-ui,sans-serif;min-height:24px;padding:2px 7px;cursor:pointer;text-decoration:none;width:fit-content}',
|
|
208
|
+
'.bt_action:hover,.bt_logButton:hover{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}',
|
|
201
209
|
'.bt_action:disabled,.bt_logButton:disabled{cursor:not-allowed;opacity:.55;text-decoration:none}',
|
|
202
|
-
'.bt_action:focus-visible{outline:2px solid var(--dsw-alias-
|
|
210
|
+
'.bt_action:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
203
211
|
'.bt_flag{flex:none;font-size:11px;color:var(--dsw-alias-label-secondary)}',
|
|
204
212
|
'.bt_body{margin:4px 0 6px 14px;display:grid;gap:6px;min-width:0}',
|
|
205
213
|
'.bt_body>.bt_refresh{justify-self:start}',
|
|
@@ -208,27 +216,33 @@ window.__ModuleLoader__.load({
|
|
|
208
216
|
'border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-sm);user-select:text}',
|
|
209
217
|
'.bt_meta{color:var(--dsw-alias-label-secondary);font-size:12px;overflow-wrap:anywhere}',
|
|
210
218
|
'.bt_capture{display:grid;gap:3px;min-width:0}',
|
|
211
|
-
'.bt_advice,.bt_captureAdvice{display:flex;align-items:baseline;gap:8px;color:var(--dsw-alias-label-
|
|
212
|
-
'.bt_logButton:focus-visible{outline:2px solid var(--dsw-alias-
|
|
219
|
+
'.bt_advice,.bt_captureAdvice{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;padding:2px 0;color:var(--dsw-alias-label-primary);font-size:12px;overflow-wrap:anywhere}.bt_advice>.bt_title,.bt_captureAdvice>.bt_title{font-weight:600;color:var(--dsw-alias-state-error-primary)}',
|
|
220
|
+
'.bt_logButton:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
213
221
|
'.bt_logPanel{margin:6px 0;padding:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;min-width:0}',
|
|
214
222
|
'.bt_logTarget{border-left:3px solid var(--dsw-alias-state-error-primary);padding-left:8px;min-width:0}',
|
|
215
223
|
'.bt_logHeading{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}',
|
|
216
224
|
'.bt_advice{grid-column:1/-1;margin:1px 0 2px;min-width:0}',
|
|
217
225
|
'.bt_compact{color:var(--dsw-alias-label-secondary)}',
|
|
218
|
-
'.bt_error>summary:focus-visible,.bt_compact>summary:focus-visible{outline:2px solid var(--dsw-alias-
|
|
226
|
+
'.bt_error>summary:focus-visible,.bt_compact>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
219
227
|
'.bt_detailBody{margin:4px 0 8px 10px;gap:10px}.bt_section{min-width:0;border-top:1px solid var(--dsw-alias-border-l1);padding-top:7px}.bt_heading{margin:0 0 4px;font:inherit;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
220
|
-
'.bt_section>summary,.bt_attempt>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_section>summary:focus-visible,.bt_attempt>summary:focus-visible{outline:2px solid var(--dsw-alias-
|
|
221
|
-
'.bt_attempt>summary,.bt_network>summary{list-style:none}.bt_attempt>summary::-webkit-details-marker,.bt_network>summary::-webkit-details-marker{display:none}.bt_attempt>summary:before,.bt_network>summary:before{content:"›";flex:none;color:var(--dsw-alias-label-secondary)}.bt_attempt[open]>summary:before,.bt_network[open]>summary:before{content:"⌄"}.bt_attemptPreview{font:inherit;font-family:ui-monospace,monospace}.bt_original{max-height:340px;overflow:auto}.bt_network>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_network>summary:focus-visible{outline:2px solid var(--dsw-alias-
|
|
222
|
-
'.bt_reportText>summary{cursor:pointer;min-height:28px;font-size:12px;color:var(--dsw-alias-label-secondary)}.bt_reportText>summary:focus-visible,.bt_reportText pre:focus-visible{outline:2px solid var(--dsw-alias-
|
|
228
|
+
'.bt_section>summary,.bt_attempt>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_section>summary:focus-visible,.bt_attempt>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}.bt_attempt{min-width:0;margin:4px 0;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l1)}.bt_attempt>div{display:grid;gap:5px;padding:6px 0}.bt_attemptPreview{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}',
|
|
229
|
+
'.bt_attempt>summary,.bt_network>summary{list-style:none}.bt_attempt>summary::-webkit-details-marker,.bt_network>summary::-webkit-details-marker{display:none}.bt_attempt>summary:before,.bt_network>summary:before{content:"›";flex:none;color:var(--dsw-alias-label-secondary)}.bt_attempt[open]>summary:before,.bt_network[open]>summary:before{content:"⌄"}.bt_attemptPreview{font:inherit;font-family:ui-monospace,monospace}.bt_original{max-height:340px;overflow:auto}.bt_network>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_network>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
230
|
+
'.bt_reportText>summary{cursor:pointer;min-height:28px;font-size:12px;color:var(--dsw-alias-label-secondary)}.bt_reportText>summary:focus-visible,.bt_reportText pre:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
223
231
|
'.bt_source{font:600 12px/1.6 system-ui,sans-serif;color:var(--dsw-alias-label-primary)}',
|
|
224
232
|
'.bt_errorGroup details{min-width:0}.bt_errorGroup details>summary,.bt_compact>summary{display:flex;align-items:center;flex-wrap:wrap;gap:5px 8px;min-height:32px;padding:4px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;list-style:none}',
|
|
225
233
|
'.bt_errorGroup details>summary::-webkit-details-marker{display:none}.bt_errorGroup details>summary:before,.bt_compact>summary:before{content:none}.bt_errorGroup details[open]>summary:before,.bt_compact[open]>summary:before{content:none}',
|
|
226
234
|
'.bt_toggleLabel{display:inline-flex;flex:none;padding:1px 5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);font:600 12px/1.7 system-ui,sans-serif;color:var(--dsw-alias-label-primary)}.bt_toggleLabel>.bt_whenOpen{display:none}details[open]>summary>.bt_toggleLabel>.bt_whenOpen{display:inline}details[open]>summary>.bt_toggleLabel>.bt_whenClosed{display:none}',
|
|
227
|
-
'.bt_errorGroup details>summary:hover,.bt_compact>summary:hover{border-color:var(--dsw-alias-state-error-primary)}.bt_errorGroup details>summary:focus-visible{outline:2px solid var(--dsw-alias-
|
|
235
|
+
'.bt_errorGroup details>summary:hover,.bt_compact>summary:hover{border-color:var(--dsw-alias-state-error-primary)}.bt_errorGroup details>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
228
236
|
'.bt_detailBody{margin:0;padding:0}.bt_section{border-top:0;padding-top:4px}.bt_attempt{margin:8px 0;padding:0;border-left:0}',
|
|
229
237
|
'.bt_depth{display:grid;gap:8px;min-width:0;margin:8px 0 8px 10px;padding:4px 0 4px 12px;border-left:2px solid var(--dsw-alias-state-error-primary)}.bt_errorGroup .bt_depth>.bt_body{margin:0;padding:0}.bt_attempt>.bt_depth{padding:4px 0 4px 12px}.bt_logPanel{margin-left:10px;border-left:2px solid var(--dsw-alias-state-error-primary)}',
|
|
230
238
|
'.bt_heading{margin-bottom:5px}.bt_original{font-size:12px}.bt_toggleLabel[hidden]{display:none}',
|
|
231
|
-
'
|
|
239
|
+
'.bt_earlierNode{border-left:0;padding-left:0}.bt_earlierNode>.bt_depth,.bt_earlierErrors>.bt_depth,.bt_earlierError>.bt_depth{border-left:0;margin-left:0;padding-left:0}',
|
|
240
|
+
'.bt_earlierNode>summary{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;font-size:12px;color:var(--dsw-alias-label-secondary)}.bt_earlierNode>summary::-webkit-details-marker{display:none}.bt_earlierNode>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
|
|
241
|
+
'.bt_failureTitle{font-size:17px;font-weight:700;line-height:1.5;color:var(--dsw-alias-state-error-primary)}',
|
|
242
|
+
'.bt_detailPair{display:grid;grid-template-columns:minmax(0,1fr);align-items:start;gap:8px;min-width:0}.bt_detailPair>details{margin:0}.bt_detailPair .bt_depth{margin-left:0;padding-left:8px}',
|
|
243
|
+
'.bt_errorGroup>.bt_network>.bt_body{margin-left:0}.bt_primary{font-size:15px;line-height:1.6}.bt_original{font-size:14px}',
|
|
244
|
+
'.bt_history{padding-top:0;display:grid;gap:6px}.bt_history>.bt_attempt{margin:0}.bt_history .bt_attempt>.bt_network>.bt_body{margin-left:0}.bt_attempt>.bt_captureAdvice{display:flex;align-items:baseline;gap:4px 8px;padding:2px 0}',
|
|
245
|
+
'@media(max-width:640px){.bt_errorGroup{grid-template-columns:minmax(0,1fr);padding-left:9px}.bt_detailPair{grid-template-columns:minmax(0,1fr)}}',
|
|
232
246
|
].join('')
|
|
233
247
|
const text = (value) => value === undefined || value === null ? '' : String(value)
|
|
234
248
|
const rawFailure = (failure) => [failure?.code ? `[${failure.code}]` : '', text(failure?.message)].filter(Boolean).join(' ')
|
|
@@ -360,21 +374,51 @@ window.__ModuleLoader__.load({
|
|
|
360
374
|
function RefreshButton ({ refresh, t }) {
|
|
361
375
|
return refresh ? h('button', { type: 'button', className: 'bt_action bt_refresh', onClick: refresh }, t('ui.refresh')) : null
|
|
362
376
|
}
|
|
363
|
-
function
|
|
377
|
+
function capturePreview (detail) {
|
|
378
|
+
const headline = nativeHeadline(detail)
|
|
379
|
+
let message = ''
|
|
380
|
+
if (detail?.kind === 'response' && !detail.protected && detail.bodyReason !== 'credential-protection') {
|
|
381
|
+
message = text(detail.body)
|
|
382
|
+
if (!detail.bodyState || detail.bodyState === 'complete') {
|
|
383
|
+
try {
|
|
384
|
+
const parsed = JSON.parse(message)
|
|
385
|
+
const providerMessage = parsed?.error?.message ?? parsed?.message
|
|
386
|
+
if (typeof providerMessage === 'string') message = providerMessage
|
|
387
|
+
} catch { /* Non-JSON responses stay literal. */ }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return failurePreview({ message: [headline, message].filter(Boolean).join(' · ') })
|
|
391
|
+
}
|
|
392
|
+
function captureDetails (item, t, unconfirmed = false) {
|
|
393
|
+
return item?.detail ? disclosureBlock({ className: 'bt_networkDetails' },
|
|
394
|
+
h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.details')),
|
|
395
|
+
unconfirmed ? h('div', { className: 'bt_meta' }, t('ui.unconfirmed')) : null,
|
|
396
|
+
unconfirmed ? adviceRow(detailCheck(item.detail), t, 'bt_captureAdvice') : null,
|
|
397
|
+
h('pre', { className: 'bt_raw' }, captureText(item))) : null
|
|
398
|
+
}
|
|
399
|
+
function captureBlock (item, index, t, unconfirmed = false, showFailure = false, details = true, inlineCopy = true, captureAdvice = true) {
|
|
400
|
+
const preview = capturePreview(item?.detail)
|
|
364
401
|
return h('div', { className: 'bt_capture', key: `capture-${index}` },
|
|
365
402
|
unconfirmed ? h('span', { className: 'bt_meta' }, t('ui.unconfirmed')) : null,
|
|
366
403
|
showFailure && item?.failure ? h('span', { className: 'bt_meta' }, rawFailure(item.failure)) : null,
|
|
367
|
-
h('
|
|
368
|
-
|
|
404
|
+
preview.value ? h('code', { className: 'bt_primary' }, preview.value) : null,
|
|
405
|
+
inlineCopy ? h(CopyButton, { value: captureText(item), label: 'ui.copyCapture', t }) : null,
|
|
406
|
+
details ? captureDetails(item, t) : null,
|
|
407
|
+
item?.detail?.kind === 'response' && (item.detail.bodyReason === 'credential-protection' || item.detail.bodyState !== 'complete' && item.detail.bodyState !== undefined || !item.detail.body) ? h('span', { className: 'bt_meta' },
|
|
369
408
|
t(item.detail.bodyReason === 'credential-protection' ? 'ui.authBodyProtected' : item.detail.bodyState === 'pending' ? 'ui.bodyPending' : item.detail.bodyState === 'truncated' ? 'ui.bodyTruncated'
|
|
370
409
|
: item.detail.bodyState === 'unreadable' || item.detail.bodyState === 'error' ? 'ui.bodyUnreadable'
|
|
371
|
-
:
|
|
372
|
-
unconfirmed ? adviceRow(detailCheck(item?.detail), t, 'bt_captureAdvice') : null)
|
|
410
|
+
: 'ui.bodyEmpty')) : null,
|
|
411
|
+
unconfirmed && captureAdvice ? adviceRow(detailCheck(item?.detail), t, 'bt_captureAdvice') : null)
|
|
412
|
+
}
|
|
413
|
+
function networkSection (selection, state, refresh, t, details = true) {
|
|
414
|
+
return h('section', { className: 'bt_section bt_network' },
|
|
415
|
+
h('h4', { className: 'bt_heading' }, t('ui.network')),
|
|
416
|
+
evidenceBody({ state, ...selection, refresh, details }, t))
|
|
373
417
|
}
|
|
374
|
-
function evidenceBody ({ state, failure, chosen, unconfirmed, candidates = [], extra, refresh }, t) {
|
|
418
|
+
function evidenceBody ({ state, failure, chosen, unconfirmed, candidates = [], extra, refresh, details = true, inlineCopy = true, captureAdvice = true }, t) {
|
|
375
419
|
return h('div', { className: 'bt_body' },
|
|
376
420
|
rawFailure(failure) ? h('pre', { className: 'bt_raw' }, rawFailure(failure)) : null,
|
|
377
|
-
chosen?.detail ? captureBlock(chosen, 0, t, unconfirmed) : null,
|
|
421
|
+
chosen?.detail ? captureBlock(chosen, 0, t, unconfirmed, false, details, inlineCopy, captureAdvice) : null,
|
|
378
422
|
candidates.length ? disclosureBlock( null,
|
|
379
423
|
h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.candidates', { count: candidates.length })),
|
|
380
424
|
...candidates.map((item, index) => disclosureBlock( { className: 'bt_attempt', key: item.attemptId ?? index },
|
|
@@ -458,8 +502,12 @@ window.__ModuleLoader__.load({
|
|
|
458
502
|
.replace(/\b(?:api[_-]?key|token|secret)["']?\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, '[credential redacted]')
|
|
459
503
|
if (Array.isArray(value)) return value.map(safeLogValue)
|
|
460
504
|
if (!value || typeof value !== 'object') return value
|
|
505
|
+
const compactionError = value.type === 'compaction/end' && typeof value.data?.error === 'string'
|
|
506
|
+
? safeCompactionFailure(value.data.error) : undefined
|
|
461
507
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key,
|
|
462
|
-
value.
|
|
508
|
+
value.type === 'compaction/end' && key === 'data' && typeof item?.error === 'string'
|
|
509
|
+
? safeLogValue({ ...item, error: compactionError?.message || '[compaction diagnostic hidden]' })
|
|
510
|
+
: value.code === 'AUTH' && (key === 'message' || key === 'body') ? '[AUTH diagnostic hidden]' : safeLogValue(item)]))
|
|
463
511
|
}
|
|
464
512
|
const reportTime = (value, t) => {
|
|
465
513
|
if (!Number.isFinite(value) || Math.abs(value) > 8640000000000000) return t('report.missing')
|
|
@@ -493,11 +541,16 @@ window.__ModuleLoader__.load({
|
|
|
493
541
|
provider: row.provider || record?.provider || selected?.context?.provider || missing,
|
|
494
542
|
model: record?.model || selected?.context?.model || missing,
|
|
495
543
|
error: reportExcerpt(headline, 100, t).replace(/[.。…]+$/, ''),
|
|
496
|
-
}), t('report.version') + ': DSH ' + (runtime?.dshVersion || missing) + ' · Better Tips ' + (runtime?.pluginVersion || missing)
|
|
497
|
-
|
|
544
|
+
}), t('report.version') + ': DSH ' + (runtime?.dshVersion || missing) + ' · Better Tips ' + (runtime?.pluginVersion || missing)]
|
|
545
|
+
const plugins = Array.isArray(runtime?.enabledPlugins) ? runtime.enabledPlugins : []
|
|
546
|
+
const pluginNames = [...new Set(plugins.filter((plugin) => typeof plugin?.name === 'string' && plugin.name.trim())
|
|
547
|
+
.map((plugin) => plugin.name + '@' + (typeof plugin.version === 'string' && plugin.version.trim() ? plugin.version : missing)))].sort()
|
|
548
|
+
const inventory = runtime?.pluginsStatus === 'complete' ? pluginNames.join('、') || t('report.pluginsNone')
|
|
549
|
+
: (pluginNames.length ? pluginNames.join('、') + ' · ' : '') + t(runtime?.pluginsStatus === 'partial' ? 'report.pluginsPartial' : 'report.pluginsUnavailable')
|
|
550
|
+
lines.push(t('report.plugins') + ': ' + inventory, '', t('report.native') + ': ' + reportExcerpt(native, 600, t))
|
|
498
551
|
if (record || selection.unconfirmed) lines.push('', t('report.unconfirmed'))
|
|
499
552
|
if (record) lines.push(t('report.summaryError') + ': ' +
|
|
500
|
-
(protectedAuth ? t('report.protected') : reportExcerpt(rawFailure(selected?.failure ?? selected?.thrown) || missing, 300, t)))
|
|
553
|
+
(protectedAuth ? t('report.protected') : reportExcerpt(rawFailure(selected?.failure ?? selected?.thrown) || (selected?.finishReason ? 'finish=' + selected.finishReason : missing), 300, t)))
|
|
501
554
|
if (!detail) lines.push(t('report.underlying') + ': ' +
|
|
502
555
|
(error ? t('report.noEvidence') + ' · ' + t('report.readFailed') + ': ' + reportExcerpt(error, 80, t)
|
|
503
556
|
: capture?.installed === false ? t(capture.reason === 'config-disabled' ? 'ui.disabled' : 'ui.captureUnavailable')
|
|
@@ -552,11 +605,18 @@ window.__ModuleLoader__.load({
|
|
|
552
605
|
catch (error) { problem = controller.signal.aborted ? 'Request timeout' : text(error?.message ?? error) }
|
|
553
606
|
}
|
|
554
607
|
if (!alive.current || operation.current !== controller) return
|
|
608
|
+
let environment
|
|
609
|
+
try { environment = await fetchJson('api/better-tips/environment', controller.signal) }
|
|
610
|
+
catch { environment = undefined }
|
|
611
|
+
if (!alive.current || operation.current !== controller) return
|
|
555
612
|
const selected = select?.({ data }) ?? {}
|
|
556
|
-
const
|
|
613
|
+
const runtime = { ...data?.runtime, ...environment?.runtime,
|
|
614
|
+
enabledPlugins: environment?.runtime?.enabledPlugins,
|
|
615
|
+
pluginsStatus: Array.isArray(environment?.runtime?.enabledPlugins) ? environment.runtime.pluginsStatus ?? 'unavailable' : 'unavailable' }
|
|
616
|
+
const value = troubleshootingReport({ failure, row, title, runtime,
|
|
557
617
|
selection: selected, record: selected.record ?? record, error: problem, capture: data?.capture }, t)
|
|
558
618
|
setReport(value)
|
|
559
|
-
const partial = !
|
|
619
|
+
const partial = !runtime?.dshVersion || runtime.pluginsStatus !== 'complete' || !selected.chosen?.detail || !!problem
|
|
560
620
|
try {
|
|
561
621
|
if (typeof navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable')
|
|
562
622
|
await navigator.clipboard.writeText(value)
|
|
@@ -568,9 +628,7 @@ window.__ModuleLoader__.load({
|
|
|
568
628
|
h('div', { className: 'bt_actions' },
|
|
569
629
|
h('button', { type: 'button', className: 'bt_action bt_reportCopy', title: t('report.notice'), disabled: status === 'report.building', onClick: copy }, t('report.copy')),
|
|
570
630
|
status ? h('span', { className: 'bt_meta', role: 'status' }, t(status)) : null),
|
|
571
|
-
report ?
|
|
572
|
-
h('summary', { className: 'bt_meta' }, toggleLabel(t), t('report.preview')),
|
|
573
|
-
h('div', { className: 'bt_meta' }, t('report.notice')),
|
|
631
|
+
report && status === 'report.fallback' ? h('div', { className: 'bt_reportText' },
|
|
574
632
|
h('pre', { className: 'bt_raw', tabIndex: 0 }, report)) : null)
|
|
575
633
|
}
|
|
576
634
|
function RawLog ({ sessionId, node, t, capture, unconfirmed }) {
|
|
@@ -661,40 +719,40 @@ window.__ModuleLoader__.load({
|
|
|
661
719
|
const shortened = excerpt !== original || points.length > 120 || first.length < original.length
|
|
662
720
|
return { value: points.slice(0, 120).join('') + (points.length > 120 || first.length < excerpt.length ? '…' : ''), shortened }
|
|
663
721
|
}
|
|
722
|
+
function originalControls (failure, t) {
|
|
723
|
+
return h('div', { className: 'bt_originalControls' },
|
|
724
|
+
h('pre', { className: 'bt_original' }, rawFailure(failure)),
|
|
725
|
+
h(CopyButton, { value: rawFailure(failure), label: 'ui.copyError', t }))
|
|
726
|
+
}
|
|
664
727
|
function errorRow (title, failure, state, selection, t, log, disclosure = {}) {
|
|
665
|
-
const original = rawFailure(failure)
|
|
666
|
-
const preview = failurePreview(failure)
|
|
667
|
-
const bottom = nativeHeadline(selection.chosen?.detail)
|
|
668
728
|
const check = selection.check ?? (text(failure?.message) === 'Stream ended without finish_reason' ? 'finishReason'
|
|
669
729
|
: selection.chosen?.detail && !selection.unconfirmed ? detailCheck(selection.chosen.detail) : failureCheck(failure))
|
|
730
|
+
const nativeFallback = disclosure.fallbackNative && !selection.chosen?.detail
|
|
670
731
|
return h('div', { className: 'bt_errorGroup' },
|
|
671
|
-
h('div', { className: '
|
|
672
|
-
|
|
673
|
-
h('
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
disclosure.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
h('
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
log && !disclosure.historyOnly ? h('section', { className: 'bt_section' },
|
|
692
|
-
h('h4', { className: 'bt_heading' }, t('ui.logSection')),
|
|
693
|
-
h(RawLog, { ...log, key: JSON.stringify([log.sessionId, logCoordinate(log.node)]), t, capture: selection.chosen, unconfirmed: selection.unconfirmed })) : null)))
|
|
732
|
+
h('div', { className: 'bt_failureTitle' }, title),
|
|
733
|
+
nativeFallback ? h('section', { className: 'bt_section bt_nativeFallback' },
|
|
734
|
+
h('h4', { className: 'bt_heading' }, t('ui.original')),
|
|
735
|
+
originalControls(failure, t),
|
|
736
|
+
evidenceBody({ state }, t))
|
|
737
|
+
: disclosure.historyOnly ? null : networkSection(selection, state, null, t, false),
|
|
738
|
+
disclosure.nativeStatus ?? null,
|
|
739
|
+
disclosure.historyOnly ? null : failure?.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_protected' }, t('ui.authProtected')) : adviceRow(check, t),
|
|
740
|
+
disclosure.historyOnly ? null : disclosure.report ?? null,
|
|
741
|
+
disclosure.historyOnly ? disclosure.history : h('div', { className: 'bt_detailPair' },
|
|
742
|
+
captureDetails(selection.chosen, t),
|
|
743
|
+
disclosureBlock({ className: 'bt_error bt_disclosure', open: disclosure.open, onToggle: disclosure.onToggle },
|
|
744
|
+
h('summary', null, toggleLabel(t), h('span', { className: 'bt_title' }, t(nativeFallback ? 'ui.logSection' : 'ui.original')), disclosure.countLabel || null),
|
|
745
|
+
h('div', { className: 'bt_body bt_detailBody' },
|
|
746
|
+
nativeFallback ? null : originalControls(failure, t),
|
|
747
|
+
disclosure.history ?? null,
|
|
748
|
+
log ? h('section', { className: 'bt_section' },
|
|
749
|
+
h('h4', { className: 'bt_heading' }, t('ui.logSection')),
|
|
750
|
+
h(RawLog, { ...log, key: JSON.stringify([log.sessionId, logCoordinate(log.node)]), t, capture: selection.chosen, unconfirmed: selection.unconfirmed })) : null))),
|
|
751
|
+
!disclosure.historyOnly ? h(RefreshButton, { refresh: log?.refresh, t }) : null)
|
|
694
752
|
}
|
|
695
753
|
function ErrorCard ({ title, failure, sessionId, node, url, revision, select, history, countLabel, historyOnly, reportFailure, reportRow, t }) {
|
|
696
754
|
const [open, setOpen] = React.useState(false)
|
|
697
|
-
const [state, refresh] = useEvidence(
|
|
755
|
+
const [state, refresh] = useEvidence(url, revision)
|
|
698
756
|
const selection = select(state)
|
|
699
757
|
return errorRow(title, failure, state, selection, t, { sessionId, node, refresh }, {
|
|
700
758
|
open, onToggle: (event) => { if (!event.target || event.target === event.currentTarget) setOpen(event.currentTarget.open) },
|
|
@@ -719,43 +777,68 @@ window.__ModuleLoader__.load({
|
|
|
719
777
|
const delay = Number.isFinite(current.delayMs) ? ' · ' + t('ui.wait', { ms: Math.round(current.delayMs) }) : ''
|
|
720
778
|
return t(label) + ' ' + current.retry + '/' + maximum + delay
|
|
721
779
|
}
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
780
|
+
function isErrorNode (node) {
|
|
781
|
+
return node?.visibility !== 'hidden' && (node?.kind === 'turn-error' || node?.kind === 'model-retry' || node?.kind === KIND
|
|
782
|
+
|| node?.kind === 'manual-compaction' && node.data?.command?.outcome?.kind === 'error')
|
|
783
|
+
}
|
|
784
|
+
function followingError (snapshot, node) {
|
|
785
|
+
const order = snapshot?.order
|
|
786
|
+
const index = order?.indexOf(node?.key) ?? -1
|
|
787
|
+
if (index < 0 || !snapshot?.nodes?.get) return ''
|
|
788
|
+
let latest = ''
|
|
789
|
+
for (let at = index + 1; at < order.length; at++) {
|
|
790
|
+
const next = snapshot.nodes.get(order[at])
|
|
791
|
+
if (!next) return latest
|
|
792
|
+
if (isErrorNode(next)) latest = next.key
|
|
793
|
+
}
|
|
794
|
+
return latest
|
|
795
|
+
}
|
|
796
|
+
function ErrorSequenceRow ({ component, useChat, node, t, ...props }) {
|
|
797
|
+
const successor = typeof useChat === 'function' ? useChat((snapshot) => followingError(snapshot, node)) : ''
|
|
798
|
+
const identity = JSON.stringify([props.sessionId, node?.key, successor])
|
|
799
|
+
const [choice, setChoice] = React.useState(null)
|
|
800
|
+
const open = choice?.identity === identity && choice.open
|
|
801
|
+
const content = () => h(component, { ...props, node, t,
|
|
802
|
+
key: JSON.stringify([props.sessionId, node?.key, node?.id, logCoordinate(node), node?.data?.message, node?.data?.endedAt]) })
|
|
803
|
+
if (!successor || !isErrorNode(node)) return content()
|
|
804
|
+
return disclosureBlock({ className: 'bt_earlierNode bt_errorGroup', open,
|
|
805
|
+
onToggle: (event) => { if (!event.target || event.target === event.currentTarget) setChoice({ identity, open: event.currentTarget.open }) } },
|
|
806
|
+
h('summary', null, toggleLabel(t), t('ui.earlierBlock')),
|
|
807
|
+
open ? content() : null)
|
|
808
|
+
}
|
|
809
|
+
function earlierErrors (blocks, t) {
|
|
810
|
+
if (blocks.length < 2) return blocks
|
|
811
|
+
return [disclosureBlock({ className: 'bt_earlierErrors', key: 'earlier:' + blocks.at(-1).props.key },
|
|
812
|
+
h('summary', null, toggleLabel(t), t('ui.earlierErrors', { count: blocks.length - 1 })),
|
|
813
|
+
...blocks.slice(0, -1).map((block, index) => disclosureBlock({ className: 'bt_earlierError', key: block.props.key },
|
|
814
|
+
h('summary', null, toggleLabel(t), t('ui.record', { index: index + 1 })), block))), blocks.at(-1)]
|
|
728
815
|
}
|
|
729
816
|
function retryHistory (attempts, state, sessionId, node, t, refresh) {
|
|
817
|
+
const blocks = attempts.map((attempt, index) => {
|
|
818
|
+
const failure = attempt.failure ?? {}
|
|
819
|
+
const selection = selectStep(state, attempt.step, failure, attempt, sessionId)
|
|
820
|
+
const attemptNode = { ...node, kind: 'model-retry', data: { current: attempt } }
|
|
821
|
+
const check = text(failure.message) === 'Stream ended without finish_reason' ? 'finishReason'
|
|
822
|
+
: selection.chosen?.detail && !selection.unconfirmed ? detailCheck(selection.chosen.detail) : failureCheck(failure)
|
|
823
|
+
return h('div', { className: 'bt_attempt', key: attempt.seq ?? attempt.attemptId ?? index },
|
|
824
|
+
index < attempts.length - 1 ? h('div', { className: 'bt_heading' }, retryTitle(attempt, t)) : null,
|
|
825
|
+
networkSection({ ...selection, inlineCopy: false, captureAdvice: false }, state, null, t, false),
|
|
826
|
+
failure.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_meta' }, t('ui.authProtected')) : adviceRow(check, t, index === attempts.length - 1 ? 'bt_captureAdvice bt_advice' : 'bt_captureAdvice'),
|
|
827
|
+
selection.chosen?.detail ? h(CopyButton, { value: captureText(selection.chosen), label: 'ui.copyCapture', t }) : null,
|
|
828
|
+
h(ReportButton, { key: JSON.stringify([sessionId, attempt.seq, attempt.attemptId]),
|
|
829
|
+
url: requestUrl(sessionId, attempt.turn, 'retry'), state,
|
|
830
|
+
select: (fresh) => selectStep(fresh, attempt.step, failure, attempt, sessionId),
|
|
831
|
+
failure, row: attempt, title: retryTitle(attempt, t), t }),
|
|
832
|
+
h('div', { className: 'bt_detailPair' },
|
|
833
|
+
captureDetails(selection.chosen, t, selection.unconfirmed),
|
|
834
|
+
disclosureBlock({ className: 'bt_attemptNative' },
|
|
835
|
+
h('summary', null, toggleLabel(t), t('ui.original'), validSeq(attempt.seq) ? ' · seq=' + attempt.seq : ''),
|
|
836
|
+
originalControls(failure, t),
|
|
837
|
+
h(RawLog, { key: JSON.stringify([sessionId, logCoordinate(attemptNode)]), sessionId, node: attemptNode, t, capture: selection.chosen, unconfirmed: selection.unconfirmed }))))
|
|
838
|
+
})
|
|
839
|
+
const grouped = earlierErrors(blocks, t)
|
|
730
840
|
return h('section', { className: 'bt_section bt_history' },
|
|
731
|
-
h(
|
|
732
|
-
h('div', { className: 'bt_meta' }, t('ui.historyNote')),
|
|
733
|
-
h(RefreshButton, { refresh, t }),
|
|
734
|
-
...attempts.map((attempt, index) => {
|
|
735
|
-
const failure = attempt.failure ?? {}
|
|
736
|
-
const selection = selectStep(state, attempt.step, failure, attempt, sessionId)
|
|
737
|
-
const attemptNode = { ...node, kind: 'model-retry', data: { current: attempt } }
|
|
738
|
-
return disclosureBlock( { className: 'bt_attempt', key: attempt.seq ?? attempt.attemptId ?? index },
|
|
739
|
-
h('summary', null, toggleLabel(t), h('span', { className: 'bt_title' }, t('ui.record', { index: index + 1 })),
|
|
740
|
-
h('span', { className: 'bt_meta' }, retryTitle(attempt, t) + (validSeq(attempt.seq) ? ' · seq=' + attempt.seq : '')),
|
|
741
|
-
h('span', { className: 'bt_source' }, t('ui.nativeExcerpt')),
|
|
742
|
-
h('code', { className: 'bt_attemptPreview' }, failurePreview(failure).value)),
|
|
743
|
-
h('div', null,
|
|
744
|
-
h('h4', { className: 'bt_heading' }, t('ui.original')),
|
|
745
|
-
h('pre', { className: 'bt_original' }, rawFailure(failure)),
|
|
746
|
-
h(CopyButton, { value: rawFailure(failure), label: 'ui.copyError', t }),
|
|
747
|
-
h(ReportButton, { key: JSON.stringify([sessionId, attempt.seq, attempt.attemptId]),
|
|
748
|
-
url: requestUrl(sessionId, attempt.turn, 'retry'), state,
|
|
749
|
-
select: (fresh) => selectStep(fresh, attempt.step, failure, attempt, sessionId),
|
|
750
|
-
failure, row: attempt, title: retryTitle(attempt, t), t }),
|
|
751
|
-
failure.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_meta' }, t('ui.authProtected')) : adviceRow(text(failure.message) === 'Stream ended without finish_reason' ? 'finishReason' : selection.chosen?.detail && !selection.unconfirmed ? detailCheck(selection.chosen.detail) : failureCheck(failure), t, 'bt_captureAdvice'),
|
|
752
|
-
disclosureBlock( { className: 'bt_network' },
|
|
753
|
-
h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.network'),
|
|
754
|
-
nativeHeadline(selection.chosen?.detail) ? h('code', { className: 'bt_primary' }, nativeHeadline(selection.chosen.detail)) : null,
|
|
755
|
-
selection.unconfirmed ? h('span', { className: 'bt_flag' }, t('ui.unconfirmed')) : null),
|
|
756
|
-
evidenceBody({ state, ...selection, refresh }, t)),
|
|
757
|
-
h(RawLog, { key: JSON.stringify([sessionId, logCoordinate(attemptNode)]), sessionId, node: attemptNode, t, capture: selection.chosen, unconfirmed: selection.unconfirmed })))
|
|
758
|
-
}))
|
|
841
|
+
...grouped.toReversed(), h(RefreshButton, { refresh, t }))
|
|
759
842
|
}
|
|
760
843
|
function ModelRetryRow ({ node, sessionId, t = (key) => key }) {
|
|
761
844
|
const current = node?.data?.current ?? {}
|
|
@@ -763,13 +846,10 @@ window.__ModuleLoader__.load({
|
|
|
763
846
|
if (current.retry === undefined) return null
|
|
764
847
|
// Native node.attempts is producer-correlated durable history, not the volatile capture cache.
|
|
765
848
|
const attempts = node.data.attempts?.length ? node.data.attempts : [current]
|
|
766
|
-
|
|
767
|
-
const countLabel = t('ui.history', { count: attempts.length }) + (categories.size > 1 ? ' · ' + t('ui.different', { count: categories.size }) : '')
|
|
768
|
-
const summaryFailure = categories.size > 1 ? { message: t('ui.categoryOverview') + ' · ' + [...categories].join(' · ') } : failure
|
|
769
|
-
return h(ErrorCard, { title: retryTitle(current, t), failure: summaryFailure, reportFailure: failure, reportRow: current, sessionId, node, t, historyOnly: true,
|
|
849
|
+
return h(ErrorCard, { title: retryTitle(current, t), failure, sessionId, node, t, historyOnly: true,
|
|
770
850
|
url: requestUrl(sessionId, current.turn, 'retry'), revision: JSON.stringify([node.id, attempts.length, current.seq, current.retry, current.retryState, failure]),
|
|
771
|
-
select: (state) =>
|
|
772
|
-
|
|
851
|
+
select: (state) => selectStep(state, current.step, failure, current, sessionId),
|
|
852
|
+
history: (state, refresh) => retryHistory(attempts, state, sessionId, node, t, refresh) })
|
|
773
853
|
}
|
|
774
854
|
function compactionUrl (sessionId) {
|
|
775
855
|
return sessionId ? `api/better-tips/compaction?${new URLSearchParams({ sessionId, source: 'compaction-row' })}` : ''
|
|
@@ -779,7 +859,7 @@ window.__ModuleLoader__.load({
|
|
|
779
859
|
let best
|
|
780
860
|
let distance = Infinity
|
|
781
861
|
for (const record of state?.data?.records ?? []) {
|
|
782
|
-
if (record.status !== status && !(status === 'error' &&
|
|
862
|
+
if (record.status !== status && !(status === 'error' && ['thrown', 'max-tokens', 'incomplete'].includes(record.status))) continue
|
|
783
863
|
const start = record.startedAt
|
|
784
864
|
const end = record.endedAt ?? start
|
|
785
865
|
if (Number.isFinite(startAt) && start < startAt - 5000) continue
|
|
@@ -798,32 +878,95 @@ window.__ModuleLoader__.load({
|
|
|
798
878
|
h('h4', { className: 'bt_heading' }, t('ui.attempts', { count: record.attempts?.length ?? 0 })),
|
|
799
879
|
h('div', { className: 'bt_meta' }, t('ui.timingMatch')),
|
|
800
880
|
h('div', { className: 'bt_meta' }, facts.join(' · ')),
|
|
801
|
-
...(record.attempts ?? []).map((attempt, index) => {
|
|
881
|
+
...earlierErrors((record.attempts ?? []).map((attempt, index) => {
|
|
802
882
|
const failure = attempt.failure ?? attempt.thrown
|
|
803
|
-
return
|
|
804
|
-
h('
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
failure ?
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
883
|
+
return h('div', { className: 'bt_attempt', key: attempt.attemptId ?? record.id + ':' + (attempt.index ?? index) },
|
|
884
|
+
h('h4', { className: 'bt_heading' }, t('compact.attempt', { index: attempt.index ?? index + 1 }) + (attempt.finishReason ? ' · finish=' + attempt.finishReason : '')),
|
|
885
|
+
attempt.detail ? networkSection({ chosen: attempt, unconfirmed: true }, { data: {} }, null, t, false) : h('span', { className: 'bt_meta' }, t('ui.empty')),
|
|
886
|
+
h('div', { className: 'bt_detailPair' },
|
|
887
|
+
captureDetails(attempt, t),
|
|
888
|
+
failure ? disclosureBlock({ className: 'bt_attemptNative' },
|
|
889
|
+
h('summary', null, toggleLabel(t), t('ui.original')),
|
|
890
|
+
originalControls(failure, t)) : null))
|
|
891
|
+
}), t))
|
|
892
|
+
}
|
|
893
|
+
function safeCompactionFailure (message) {
|
|
894
|
+
if (typeof message !== 'string') return undefined
|
|
895
|
+
// errorChain persists text, not the original error code. Unknown text may
|
|
896
|
+
// be an AUTH message; never recover it merely because the transaction matches.
|
|
897
|
+
if (/\bAUTH\b/.test(message)) return { code: 'AUTH', message: '' }
|
|
898
|
+
if (message === 'summarization truncated at the token cap (incomplete checkpoint)') return { code: 'MAX_TOKENS', message }
|
|
899
|
+
if (message === 'summarization produced no text summary content'
|
|
900
|
+
|| message === 'compaction summary cannot contain image output'
|
|
901
|
+
|| /^summary is not smaller than the shadowed content \(\d+ estimated framed tokens >= \d+\)$/.test(message)
|
|
902
|
+
|| /^compaction: (?:session surface changed during summarization|selected surface changed before summarization began|the selected span changed during summarization|the selected span was rewritten during summarization)$/.test(message)) return { message }
|
|
903
|
+
return undefined
|
|
904
|
+
}
|
|
905
|
+
function useManualCompactionFailure (sessionId, command) {
|
|
906
|
+
const identity = JSON.stringify([sessionId, command?.commandId, command?.seq])
|
|
907
|
+
const [state, setState] = React.useState(null)
|
|
908
|
+
React.useEffect(() => {
|
|
909
|
+
if (!sessionId || !command?.commandId) return undefined
|
|
910
|
+
let cancelled = false
|
|
911
|
+
let release
|
|
912
|
+
const publish = (value) => { if (!cancelled) setState((previous) => value.error && previous?.identity === identity && previous.failure ? previous : { identity, ...value }) }
|
|
913
|
+
try {
|
|
914
|
+
const binding = sessionService?.binding(sessionId)
|
|
915
|
+
if (!binding?.eventSource) { publish({ missing: true }); return undefined }
|
|
916
|
+
const read = () => {
|
|
917
|
+
if (cancelled) return
|
|
918
|
+
try {
|
|
919
|
+
const events = (binding.eventSource.getSnapshot()?.entries ?? []).filter((entry) => entry.type === 'event').map((entry) => entry.event)
|
|
920
|
+
const own = events.filter((event) => event.data?.sourceCommandId === command.commandId)
|
|
921
|
+
const ids = new Set(own.filter((event) => event.type === 'compaction/start' || event.type === 'compaction/end').map((event) => event.data.compactionId).filter((id) => typeof id === 'string' && id))
|
|
922
|
+
const done = events.filter((event) => event.type === 'command/done' && event.data?.commandId === command.commandId)
|
|
923
|
+
const ends = ids.size === 1 && done.length <= 1 ? own.filter((event) => event.type === 'compaction/end' && ids.has(event.data.compactionId)
|
|
924
|
+
&& validSeq(event.seq) && (!validSeq(command.seq) || event.seq >= command.seq)
|
|
925
|
+
&& (done.length !== 1 || event.seq <= done[0].seq)) : []
|
|
926
|
+
const event = ends.length === 1 ? ends[0] : undefined
|
|
927
|
+
if (!event || typeof event.data.error !== 'string' || !event.data.error) { publish({ missing: true }); return }
|
|
928
|
+
const failure = safeCompactionFailure(event.data.error)
|
|
929
|
+
if (!failure) { publish({ missing: true }); return }
|
|
930
|
+
publish({ eventSeq: event.seq, compactionId: event.data.compactionId, time: event.time, failure })
|
|
931
|
+
} catch { publish({ error: true }) }
|
|
932
|
+
}
|
|
933
|
+
release = binding.eventSource.subscribe(read)
|
|
934
|
+
read()
|
|
935
|
+
if (validSeq(command.seq) && typeof binding.session?.loadThrough === 'function') {
|
|
936
|
+
Promise.resolve(binding.session.loadThrough(command.seq)).then(read, () => publish({ error: true }))
|
|
937
|
+
}
|
|
938
|
+
} catch { publish({ error: true }) }
|
|
939
|
+
return () => { cancelled = true; release?.() }
|
|
940
|
+
}, [identity])
|
|
941
|
+
return state?.identity === identity ? state : null
|
|
815
942
|
}
|
|
816
943
|
function CompactionErrorCard ({ title, fallback, sessionId, node, match, revision, t }) {
|
|
817
944
|
const [open, setOpen] = React.useState(false)
|
|
818
|
-
const [state, refresh] = useEvidence(
|
|
945
|
+
const [state, refresh] = useEvidence(compactionUrl(sessionId), revision)
|
|
819
946
|
const record = selectCompaction(state, match)
|
|
820
|
-
|
|
947
|
+
const command = node?.data?.command
|
|
948
|
+
const nativeRecord = useManualCompactionFailure(sessionId, command)
|
|
949
|
+
const chosen = record?.attempts?.at(-1)
|
|
950
|
+
const protect = chosen?.failure?.code === 'AUTH' || chosen?.thrown?.code === 'AUTH' || chosen?.detail?.protected
|
|
951
|
+
const nativeFailure = nativeRecord?.failure ?? (command ? { message: fallback } : safeCompactionFailure(fallback))
|
|
952
|
+
const failure = protect ? { code: 'AUTH', message: '' } : nativeFailure ?? { message: t('compact.errorProtected') }
|
|
953
|
+
const selection = { chosen, unconfirmed: !!record }
|
|
954
|
+
return errorRow(title, failure, state, selection, t, { sessionId, node, refresh }, {
|
|
955
|
+
nativeStatus: !chosen?.detail && (chosen?.failure || chosen?.thrown) ? h('section', { className: 'bt_summaryFailure' },
|
|
956
|
+
h('h4', { className: 'bt_heading' }, t('report.summaryError') + ' · ' + t('ui.unconfirmed')),
|
|
957
|
+
protect ? h('div', { className: 'bt_protected' }, t('ui.authProtected'))
|
|
958
|
+
: originalControls(chosen.failure ?? chosen.thrown, t))
|
|
959
|
+
: chosen?.finishReason ? h('section', { className: 'bt_summaryFailure' },
|
|
960
|
+
h('h4', { className: 'bt_heading' }, t('report.summaryError') + ' · ' + t('ui.unconfirmed')),
|
|
961
|
+
h('code', { className: 'bt_meta' }, 'finish=' + chosen.finishReason))
|
|
962
|
+
: command && !nativeRecord?.failure ? h('div', { className: 'bt_meta' }, t(nativeRecord?.error ? 'compact.recordFailed' : 'compact.recordMissing')) : null,
|
|
963
|
+
fallbackNative: true,
|
|
821
964
|
open, onToggle: (event) => { if (!event.target || event.target === event.currentTarget) setOpen(event.currentTarget.open) }, network: false,
|
|
822
|
-
report: h(ReportButton, { key: JSON.stringify([sessionId, logCoordinate(node), revision]),
|
|
823
|
-
url: compactionUrl(sessionId), state, failure
|
|
965
|
+
report: h(ReportButton, { key: JSON.stringify([sessionId, logCoordinate(node), revision, nativeRecord?.eventSeq, failure]),
|
|
966
|
+
url: compactionUrl(sessionId), state, failure, row: command ?? node?.data, title, t,
|
|
824
967
|
select: (fresh) => {
|
|
825
968
|
const matched = selectCompaction(fresh, match)
|
|
826
|
-
const attempt =
|
|
969
|
+
const attempt = matched?.attempts?.at(-1)
|
|
827
970
|
return { chosen: attempt, unconfirmed: !!matched, record: matched }
|
|
828
971
|
} }),
|
|
829
972
|
history: h(React.Fragment, null, compactionExtra(record, t),
|
|
@@ -896,7 +1039,8 @@ window.__ModuleLoader__.load({
|
|
|
896
1039
|
for (const [key, component] of [
|
|
897
1040
|
['turn-error', TurnErrorRow], ['model-retry', ModelRetryRow], ['compaction', CompactionRow],
|
|
898
1041
|
['manual-compaction', ManualCompactionRow], [KIND, CompactionFailureRow],
|
|
899
|
-
]) yield ctx.slots.register({ name: 'conversation.chat.node', key, priority: -100, locale: NS },
|
|
1042
|
+
]) yield ctx.slots.register({ name: 'conversation.chat.node', key, priority: -100, locale: NS },
|
|
1043
|
+
key === 'compaction' ? component : (props) => h(ErrorSequenceRow, { ...props, component }))
|
|
900
1044
|
})
|
|
901
1045
|
},
|
|
902
1046
|
}
|