@mingozhou/dsh-replay 0.4.1 → 0.4.2
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/README.md +225 -148
- package/README.zh.md +225 -154
- package/assets/icon.png +0 -0
- package/lib/client.js +954 -2418
- package/lib/core.js +557 -86
- package/lib/index.js +1043 -51
- package/lib/types/src/client/api.d.ts +44 -0
- package/lib/types/src/client/components/AuditView.d.ts +2 -1
- package/lib/types/src/client/components/CompareView.d.ts +2 -1
- package/lib/types/src/client/components/DashboardView.d.ts +8 -0
- package/lib/types/src/client/components/ExportDialog.d.ts +9 -0
- package/lib/types/src/client/components/ReplayApp.d.ts +4 -2
- package/lib/types/src/client/components/ReplayModal.d.ts +1 -1
- package/lib/types/src/client/components/SessionPicker.d.ts +4 -3
- package/lib/types/src/client/components/TimelineView.d.ts +2 -1
- package/lib/types/src/client/export.d.ts +6 -1
- package/lib/types/src/client/i18n.d.ts +8 -1
- package/lib/types/src/client/pricing.d.ts +6 -0
- package/lib/types/src/core/audit.d.ts +18 -0
- package/lib/types/src/core/compare.d.ts +8 -3
- package/lib/types/src/core/cost.d.ts +21 -3
- package/lib/types/src/core/index.d.ts +1 -0
- package/lib/types/src/core/redact.d.ts +40 -0
- package/lib/types/src/core/timeline.d.ts +9 -0
- package/lib/types/src/index.d.ts +52 -1
- package/lib/viewer.js +282 -29
- package/package.json +118 -107
- package/scripts/build.mjs +7 -1
- package/src/client/api.ts +114 -10
- package/src/client/components/AuditView.tsx +89 -10
- package/src/client/components/CompareView.tsx +89 -13
- package/src/client/components/DashboardView.tsx +232 -0
- package/src/client/components/ExportDialog.tsx +136 -0
- package/src/client/components/ItemDetail.tsx +36 -4
- package/src/client/components/OverviewView.tsx +150 -34
- package/src/client/components/ReplayApp.tsx +207 -60
- package/src/client/components/ReplayModal.tsx +41 -6
- package/src/client/components/SessionPicker.tsx +55 -27
- package/src/client/components/TimelineView.tsx +106 -41
- package/src/client/export.ts +20 -4
- package/src/client/i18n.ts +266 -45
- package/src/client/index.ts +7 -5
- package/src/client/mascot-image.ts +1 -1
- package/src/client/pricing.ts +90 -0
- package/src/client/styles.css +250 -1
- package/src/client/viewer-entry.tsx +25 -4
- package/src/core/audit.ts +274 -67
- package/src/core/compare.ts +0 -0
- package/src/core/cost.ts +94 -16
- package/src/core/index.ts +1 -0
- package/src/core/redact.ts +182 -0
- package/src/core/timeline.ts +107 -12
- package/src/index.ts +290 -54
package/src/client/i18n.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lightweight EN/中文 i18n for the
|
|
2
|
+
* Lightweight EN/中文 i18n for the review UI. The dictionary is plain data;
|
|
3
3
|
* `useI18n()` provides `t(key, params)` plus the current language and setter.
|
|
4
4
|
* Language resolution: explicit choice (persisted) > browser language > en.
|
|
5
|
+
*
|
|
6
|
+
* A tiny external store keeps the sidebar button, the modal chrome and the
|
|
7
|
+
* embedded app in sync when the language is toggled from any of them.
|
|
5
8
|
*/
|
|
6
9
|
import React from 'react'
|
|
7
10
|
|
|
@@ -12,31 +15,81 @@ const STORAGE_KEY = 'dshr-lang'
|
|
|
12
15
|
type Dict = Record<string, string>
|
|
13
16
|
|
|
14
17
|
const en: Dict = {
|
|
15
|
-
'btn.replay': 'Session
|
|
18
|
+
'btn.replay': 'Session Review',
|
|
16
19
|
'modal.close': 'Close',
|
|
17
|
-
'
|
|
20
|
+
'modal.title': 'Session review',
|
|
21
|
+
'app.reviewSub': 'read-only · no model calls · no token spend',
|
|
22
|
+
'app.refresh': 'Refresh',
|
|
23
|
+
'app.updated': 'updated {time}',
|
|
24
|
+
'app.back': 'All sessions',
|
|
25
|
+
'app.retry': 'Retry',
|
|
26
|
+
'lang.switch': 'Switch language',
|
|
27
|
+
'picker.title': 'Pick a session to review',
|
|
18
28
|
'picker.empty': 'No sessions yet — run something first!',
|
|
29
|
+
'picker.search': 'Search title, id or workspace…',
|
|
30
|
+
'picker.allWorkspaces': 'All workspaces',
|
|
31
|
+
'picker.live': 'live',
|
|
32
|
+
'picker.count': '{n} sessions',
|
|
33
|
+
'picker.subagent': 'subagent',
|
|
34
|
+
'picker.fork': 'fork',
|
|
19
35
|
'exp.button': 'Export HTML',
|
|
36
|
+
'exp.title': 'Export a shareable replay',
|
|
37
|
+
'exp.warning': 'A session log is raw data. This export masks common credentials, but you should still review it before sharing.',
|
|
38
|
+
'exp.redact': 'Redact common secrets and home paths',
|
|
39
|
+
'exp.includeRaw': 'Include unredacted raw data (not recommended)',
|
|
40
|
+
'exp.size': 'Estimated size: {size}',
|
|
41
|
+
'exp.redactions': '{n} secret/home-path values will be masked',
|
|
42
|
+
'exp.confirm': 'Export HTML',
|
|
43
|
+
'exp.cancel': 'Cancel',
|
|
44
|
+
'exp.note': 'The exported file is standalone: it runs offline and makes no network requests.',
|
|
20
45
|
'exp.fail': 'Export failed: {error}',
|
|
21
46
|
'ov.cost': '≈ cost ({label}, est.)',
|
|
47
|
+
'ov.costPanel': 'cost estimate',
|
|
48
|
+
'ov.costNote': 'Estimate from recorded tokens. Prices are editable and may lag provider changes.',
|
|
49
|
+
'ov.perModel': 'per model',
|
|
50
|
+
'ov.risk': 'risk findings',
|
|
51
|
+
'ov.noCost': 'No pricing matched this model',
|
|
52
|
+
'ov.currency': 'Currency',
|
|
53
|
+
'ov.editPrices': 'Edit prices',
|
|
54
|
+
'ov.priceSaved': 'Prices saved',
|
|
55
|
+
'ov.priceReset': 'Reset defaults',
|
|
56
|
+
'ov.updated': 'updated {date}',
|
|
22
57
|
'flt.search': 'filter events…',
|
|
23
|
-
'app.loadingText': 'Jing Xiaoshen is
|
|
58
|
+
'app.loadingText': 'Jing Xiaoshen is sorting the log…',
|
|
59
|
+
'tab.dashboard': 'Dashboard',
|
|
24
60
|
'tab.overview': 'Overview',
|
|
25
61
|
'ov.tokensChart': 'cumulative tokens',
|
|
26
62
|
'ov.toolTime': 'tool time by tool',
|
|
27
63
|
'ov.inputTokens': 'input tokens (billed)',
|
|
28
64
|
'ov.outputTokens': 'output tokens',
|
|
29
|
-
'ov.findings': '
|
|
65
|
+
'ov.findings': 'risk findings',
|
|
30
66
|
'tab.timeline': 'Timeline',
|
|
31
67
|
'tab.audit': 'Audit',
|
|
32
68
|
'tab.forks': 'Forks',
|
|
33
69
|
'tab.compare': 'Compare',
|
|
34
70
|
'app.loading': 'Loading session…',
|
|
35
71
|
'app.error': 'Failed to load session — {error}. Is the dsh-replay host plugin enabled?',
|
|
36
|
-
'app.openSession': 'Open a session to
|
|
72
|
+
'app.openSession': 'Open a session to review it.',
|
|
73
|
+
'dash.title': 'Session archive',
|
|
74
|
+
'dash.subtitle': 'Every session on this machine, at a glance.',
|
|
75
|
+
'dash.sessions': 'sessions',
|
|
76
|
+
'dash.tokens': 'billed tokens',
|
|
77
|
+
'dash.cost': 'estimated cost',
|
|
78
|
+
'dash.risk': 'high-risk findings',
|
|
79
|
+
'dash.recent': 'recent sessions',
|
|
80
|
+
'dash.empty': 'No sessions yet — run something first!',
|
|
81
|
+
'dash.thisMonth': 'this month',
|
|
82
|
+
'dash.allTime': 'all time',
|
|
83
|
+
'dash.live': 'live',
|
|
84
|
+
'dash.open': 'Open',
|
|
85
|
+
'dash.errors': 'tool errors',
|
|
86
|
+
'dash.scanned': 'indexed {n} sessions',
|
|
37
87
|
'hdr.turns': '{n} turns',
|
|
38
88
|
'play.event': 'event {i}/{n}',
|
|
39
89
|
'play.position': 'timeline position',
|
|
90
|
+
'tl.play': 'play',
|
|
91
|
+
'tl.pause': 'pause',
|
|
92
|
+
'tl.speed': 'playback speed',
|
|
40
93
|
'ts.title': 'tokens per step',
|
|
41
94
|
'ts.input': 'input (billed)',
|
|
42
95
|
'ts.output': 'output',
|
|
@@ -45,6 +98,10 @@ const en: Dict = {
|
|
|
45
98
|
'ts.barTitle': 'turn {turn} step {step} — in {in}, out {out}',
|
|
46
99
|
'tl.empty': 'No renderable events in this session yet.',
|
|
47
100
|
'tl.turn': 'Turn {n}',
|
|
101
|
+
'tl.reasoning': 'thinking',
|
|
102
|
+
'tl.system': 'system prompt',
|
|
103
|
+
'tl.model': 'model selected: {model}',
|
|
104
|
+
'tl.summary': 'summary',
|
|
48
105
|
'kind.user': 'user',
|
|
49
106
|
'kind.assistant': 'assistant',
|
|
50
107
|
'kind.tool': 'tool',
|
|
@@ -62,13 +119,29 @@ const en: Dict = {
|
|
|
62
119
|
'detail.cacheRead': 'cache read',
|
|
63
120
|
'detail.cacheWrite': 'cache write',
|
|
64
121
|
'detail.output': 'output',
|
|
65
|
-
'detail.reasoning': '
|
|
122
|
+
'detail.reasoning': 'reasoning',
|
|
66
123
|
'detail.shadowed': 'shadowed by compaction',
|
|
124
|
+
'detail.copy': 'copy',
|
|
125
|
+
'detail.copied': 'copied',
|
|
67
126
|
'sev.critical': 'critical',
|
|
68
127
|
'sev.serious': 'serious',
|
|
69
128
|
'sev.warning': 'warning',
|
|
70
129
|
'sev.info': 'info',
|
|
71
|
-
'audit.empty': 'No findings —
|
|
130
|
+
'audit.empty': 'No findings — no rule matched this session.',
|
|
131
|
+
'audit.noRisk': 'No risk findings.',
|
|
132
|
+
'audit.coverage': 'Regex scan over tool names and arguments. Tool output content is not inspected; false negatives and false positives are possible.',
|
|
133
|
+
'audit.scanned': 'scanned {n} events · {rules} rules',
|
|
134
|
+
'audit.baseline': '{n} baseline config events ignored',
|
|
135
|
+
'audit.riskOnly': 'risk only',
|
|
136
|
+
'audit.all': 'all',
|
|
137
|
+
'audit.export': 'export report',
|
|
138
|
+
'audit.category.shell': 'shell',
|
|
139
|
+
'audit.category.file': 'file',
|
|
140
|
+
'audit.category.network': 'network',
|
|
141
|
+
'audit.category.approval': 'approval',
|
|
142
|
+
'audit.category.config': 'config',
|
|
143
|
+
'audit.category.error': 'error',
|
|
144
|
+
'audit.category.secret': 'credential',
|
|
72
145
|
'cmp.with': 'Compare with',
|
|
73
146
|
'cmp.parent': '(parent)',
|
|
74
147
|
'cmp.fork': '(fork)',
|
|
@@ -86,9 +159,19 @@ const en: Dict = {
|
|
|
86
159
|
'cmp.duration': 'duration',
|
|
87
160
|
'cmp.toolMix': 'tool mix',
|
|
88
161
|
'cmp.tool': 'tool',
|
|
162
|
+
'cmp.cost': 'est. cost',
|
|
163
|
+
'cmp.risk': 'risk findings',
|
|
89
164
|
'cmp.divergence':
|
|
90
165
|
'These sessions share lineage and diverge at seq {seq} — identical before it, different history after.',
|
|
166
|
+
'cmp.divergenceA': 'A · first differing event',
|
|
167
|
+
'cmp.divergenceB': 'B · first differing event',
|
|
91
168
|
'cmp.aria': 'session to compare against',
|
|
169
|
+
'cmp.relation': 'lineage',
|
|
170
|
+
'cmp.relation.same': 'same session',
|
|
171
|
+
'cmp.relation.a-parent-of-b': 'A is an ancestor of B',
|
|
172
|
+
'cmp.relation.b-parent-of-a': 'B is an ancestor of A',
|
|
173
|
+
'cmp.relation.siblings': 'siblings (shared parent)',
|
|
174
|
+
'cmp.relation.none': 'no direct lineage',
|
|
92
175
|
'forks.empty': 'No sessions found.',
|
|
93
176
|
'forks.edge': 'forked @ {n} events',
|
|
94
177
|
'forks.subagent': ' · subagent',
|
|
@@ -96,21 +179,24 @@ const en: Dict = {
|
|
|
96
179
|
'forks.aria': 'session fork tree',
|
|
97
180
|
'forks.untitled': 'Untitled',
|
|
98
181
|
'forks.forkTag': ' · fork',
|
|
99
|
-
'forks.hint': 'Session lineage: forked sessions and subagents hang under their parent as a tree; sessions without lineage are listed as cards. Click any card to
|
|
100
|
-
|
|
182
|
+
'forks.hint': 'Session lineage: forked sessions and subagents hang under their parent as a tree; sessions without lineage are listed as cards. Click any card to review it.',
|
|
183
|
+
'forks.compare': 'compare',
|
|
184
|
+
'forks.open': 'open',
|
|
101
185
|
'f.shell.dangerous': 'shell: {why}',
|
|
102
186
|
'f.shell.plain': 'shell command via {name}',
|
|
103
187
|
'f.file.sensitive': 'file write touches sensitive path',
|
|
104
188
|
'f.file.write': 'file mutation via {name}',
|
|
189
|
+
'f.file.secret': 'credential material written to a file',
|
|
190
|
+
'f.file.destructive': 'destructive file operation via {name}',
|
|
105
191
|
'f.read.sensitive': 'access to sensitive path via {name}',
|
|
106
|
-
'f.network': 'network
|
|
192
|
+
'f.network': 'network request may carry sensitive data via {name}',
|
|
193
|
+
'f.output.secret': 'tool output contains credential material',
|
|
107
194
|
'f.approval.denied': 'user denied an approval',
|
|
108
195
|
'f.sandbox.change': 'sandbox mode changed',
|
|
109
196
|
'f.permission.change': 'permission preset changed',
|
|
110
197
|
'f.subagent': 'subagent spawned ({mode})',
|
|
111
198
|
'f.tool.error': 'tool failed: {name}',
|
|
112
199
|
'f.turn.error': 'turn ended abnormally: {kind}',
|
|
113
|
-
// dangerous-shell reasons
|
|
114
200
|
'why.rm-rf': 'recursive/forced delete',
|
|
115
201
|
'why.sudo': 'privilege escalation',
|
|
116
202
|
'why.chmod': 'permission change',
|
|
@@ -119,7 +205,12 @@ const en: Dict = {
|
|
|
119
205
|
'why.disk': 'raw disk operation',
|
|
120
206
|
'why.remote': 'remote host access',
|
|
121
207
|
'why.creds': 'credential material in command',
|
|
122
|
-
|
|
208
|
+
'why.exec-policy': 'script execution policy change',
|
|
209
|
+
'why.system-tamper': 'boot/shadow/log tampering',
|
|
210
|
+
'why.persistence': 'persistence or account change',
|
|
211
|
+
'why.encoded-shell': 'encoded PowerShell command',
|
|
212
|
+
'why.exfil': 'possible credential exfiltration',
|
|
213
|
+
'why.publish': 'package/release publish',
|
|
123
214
|
'meta.turnEnd': 'turn ended: {reason}',
|
|
124
215
|
'meta.compactSummary': 'context compacted (summary)',
|
|
125
216
|
'meta.compactPrune': 'context pruned',
|
|
@@ -132,42 +223,99 @@ const en: Dict = {
|
|
|
132
223
|
'meta.seed': 'seed boundary (fork/resume point)',
|
|
133
224
|
'meta.retry': 'model request retried',
|
|
134
225
|
'meta.goal': 'goal changed',
|
|
226
|
+
'meta.system': 'system prompt',
|
|
227
|
+
'meta.model': 'model: {model}',
|
|
228
|
+
'meta.command': 'command: {command}',
|
|
135
229
|
}
|
|
136
230
|
|
|
137
231
|
const zh: Dict = {
|
|
138
|
-
'btn.replay': '
|
|
232
|
+
'btn.replay': '会话复盘',
|
|
139
233
|
'modal.close': '关闭',
|
|
140
|
-
'
|
|
141
|
-
'
|
|
234
|
+
'modal.title': '会话复盘',
|
|
235
|
+
'app.reviewSub': '只读 · 不调用模型 · 不消耗 token',
|
|
236
|
+
'app.refresh': '刷新',
|
|
237
|
+
'app.updated': '更新于 {time}',
|
|
238
|
+
'app.back': '全部会话',
|
|
239
|
+
'app.retry': '重试',
|
|
240
|
+
'lang.switch': '切换语言',
|
|
241
|
+
'picker.title': '选择要复盘的会话',
|
|
242
|
+
'picker.empty': '还没有会话 — 先去跑点什么吧!',
|
|
243
|
+
'picker.search': '搜索标题、ID 或工作区…',
|
|
244
|
+
'picker.allWorkspaces': '全部工作区',
|
|
245
|
+
'picker.live': '运行中',
|
|
246
|
+
'picker.count': '{n} 个会话',
|
|
247
|
+
'picker.subagent': '子 Agent',
|
|
248
|
+
'picker.fork': '分支',
|
|
142
249
|
'exp.button': '导出 HTML',
|
|
143
|
-
'exp.
|
|
144
|
-
'
|
|
250
|
+
'exp.title': '导出可分享的复盘',
|
|
251
|
+
'exp.warning': '会话日志是原始数据。导出会遮罩常见密钥,但分享前仍建议自己检查一遍。',
|
|
252
|
+
'exp.redact': '遮罩常见密钥和用户主目录',
|
|
253
|
+
'exp.includeRaw': '包含未脱敏原文(不推荐)',
|
|
254
|
+
'exp.size': '预计大小:{size}',
|
|
255
|
+
'exp.redactions': '将遮罩 {n} 处密钥/路径',
|
|
256
|
+
'exp.confirm': '导出 HTML',
|
|
257
|
+
'exp.cancel': '取消',
|
|
258
|
+
'exp.note': '导出文件完全离线,不会发起任何网络请求。',
|
|
259
|
+
'exp.fail': '导出失败:{error}',
|
|
260
|
+
'ov.cost': '≈ 成本({label},估算)',
|
|
261
|
+
'ov.costPanel': '成本估算',
|
|
262
|
+
'ov.costNote': '根据日志中的 token 估算;价格可编辑,可能滞后于官方调价。',
|
|
263
|
+
'ov.perModel': '分模型',
|
|
264
|
+
'ov.risk': '风险发现',
|
|
265
|
+
'ov.noCost': '没有匹配到该模型的价格',
|
|
266
|
+
'ov.currency': '货币',
|
|
267
|
+
'ov.editPrices': '编辑价格',
|
|
268
|
+
'ov.priceSaved': '价格已保存',
|
|
269
|
+
'ov.priceReset': '恢复默认',
|
|
270
|
+
'ov.updated': '更新于 {date}',
|
|
145
271
|
'flt.search': '搜索事件…',
|
|
146
|
-
'app.loadingText': '
|
|
272
|
+
'app.loadingText': '鲸小深正在整理日志…',
|
|
273
|
+
'tab.dashboard': '仪表盘',
|
|
147
274
|
'tab.overview': '概览',
|
|
148
275
|
'ov.tokensChart': '累计 Token',
|
|
149
276
|
'ov.toolTime': '各工具耗时',
|
|
150
|
-
'ov.inputTokens': '输入 token
|
|
277
|
+
'ov.inputTokens': '输入 token(计费)',
|
|
151
278
|
'ov.outputTokens': '输出 token',
|
|
152
|
-
'ov.findings': '
|
|
279
|
+
'ov.findings': '风险发现',
|
|
153
280
|
'tab.timeline': '时间线',
|
|
154
281
|
'tab.audit': '审计',
|
|
155
282
|
'tab.forks': '分支',
|
|
156
283
|
'tab.compare': '对比',
|
|
157
284
|
'app.loading': '会话加载中…',
|
|
158
285
|
'app.error': '会话加载失败 — {error}。请确认 dsh-replay 宿主插件已启用。',
|
|
159
|
-
'app.openSession': '
|
|
286
|
+
'app.openSession': '打开一个会话即可复盘。',
|
|
287
|
+
'dash.title': '会话档案',
|
|
288
|
+
'dash.subtitle': '这台机器上的所有会话,一眼看完。',
|
|
289
|
+
'dash.sessions': '会话',
|
|
290
|
+
'dash.tokens': '计费 token',
|
|
291
|
+
'dash.cost': '估算成本',
|
|
292
|
+
'dash.risk': '高危发现',
|
|
293
|
+
'dash.recent': '最近会话',
|
|
294
|
+
'dash.empty': '还没有会话 — 先去跑点什么吧!',
|
|
295
|
+
'dash.thisMonth': '本月',
|
|
296
|
+
'dash.allTime': '全部',
|
|
297
|
+
'dash.live': '运行中',
|
|
298
|
+
'dash.open': '打开',
|
|
299
|
+
'dash.errors': '工具错误',
|
|
300
|
+
'dash.scanned': '已索引 {n} 个会话',
|
|
160
301
|
'hdr.turns': '{n} 轮',
|
|
161
302
|
'play.event': '事件 {i}/{n}',
|
|
162
303
|
'play.position': '时间线位置',
|
|
304
|
+
'tl.play': '播放',
|
|
305
|
+
'tl.pause': '暂停',
|
|
306
|
+
'tl.speed': '播放速度',
|
|
163
307
|
'ts.title': '每步 token',
|
|
164
|
-
'ts.input': '
|
|
308
|
+
'ts.input': '输入(计费)',
|
|
165
309
|
'ts.output': '输出',
|
|
166
310
|
'ts.total': 'Σ 输入 {in} · 输出 {out}',
|
|
167
311
|
'ts.cacheRead': ' · 缓存读 {n}',
|
|
168
|
-
'ts.barTitle': '第 {turn} 轮第 {step} 步 — 输入 {in}
|
|
312
|
+
'ts.barTitle': '第 {turn} 轮第 {step} 步 — 输入 {in},输出 {out}',
|
|
169
313
|
'tl.empty': '该会话暂无可渲染事件。',
|
|
170
314
|
'tl.turn': '第 {n} 轮',
|
|
315
|
+
'tl.reasoning': '思考',
|
|
316
|
+
'tl.system': '系统提示词',
|
|
317
|
+
'tl.model': '选择模型:{model}',
|
|
318
|
+
'tl.summary': '摘要',
|
|
171
319
|
'kind.user': '用户',
|
|
172
320
|
'kind.assistant': '助手',
|
|
173
321
|
'kind.tool': '工具',
|
|
@@ -175,26 +323,42 @@ const zh: Dict = {
|
|
|
175
323
|
'detail.message': '消息',
|
|
176
324
|
'detail.arguments': '参数',
|
|
177
325
|
'detail.result': '结果',
|
|
178
|
-
'detail.usage': 'Token
|
|
326
|
+
'detail.usage': 'Token 用量(本步)',
|
|
179
327
|
'detail.eventData': '事件数据',
|
|
180
328
|
'detail.callId': '调用 ID',
|
|
181
329
|
'detail.duration': '耗时',
|
|
182
330
|
'detail.noResult': '未记录结果',
|
|
183
331
|
'detail.error': '错误',
|
|
184
|
-
'detail.inputUncached': '
|
|
332
|
+
'detail.inputUncached': '输入(未缓存)',
|
|
185
333
|
'detail.cacheRead': '缓存读',
|
|
186
334
|
'detail.cacheWrite': '缓存写',
|
|
187
335
|
'detail.output': '输出',
|
|
188
|
-
'detail.reasoning': '
|
|
336
|
+
'detail.reasoning': '推理过程',
|
|
189
337
|
'detail.shadowed': '已被压缩遮蔽',
|
|
338
|
+
'detail.copy': '复制',
|
|
339
|
+
'detail.copied': '已复制',
|
|
190
340
|
'sev.critical': '严重',
|
|
191
341
|
'sev.serious': '较高',
|
|
192
342
|
'sev.warning': '警告',
|
|
193
343
|
'sev.info': '信息',
|
|
194
|
-
'audit.empty': '
|
|
344
|
+
'audit.empty': '未命中当前规则。',
|
|
345
|
+
'audit.noRisk': '未发现风险。',
|
|
346
|
+
'audit.coverage': '基于工具名/参数的正则扫描;不检查工具输出内容,可能漏报或误报。',
|
|
347
|
+
'audit.scanned': '扫描了 {n} 个事件 · {rules} 条规则',
|
|
348
|
+
'audit.baseline': '已忽略 {n} 条初始配置',
|
|
349
|
+
'audit.riskOnly': '只看风险',
|
|
350
|
+
'audit.all': '全部',
|
|
351
|
+
'audit.export': '导出报告',
|
|
352
|
+
'audit.category.shell': 'Shell',
|
|
353
|
+
'audit.category.file': '文件',
|
|
354
|
+
'audit.category.network': '网络',
|
|
355
|
+
'audit.category.approval': '审批',
|
|
356
|
+
'audit.category.config': '配置',
|
|
357
|
+
'audit.category.error': '错误',
|
|
358
|
+
'audit.category.secret': '凭据',
|
|
195
359
|
'cmp.with': '对比对象',
|
|
196
|
-
'cmp.parent': '
|
|
197
|
-
'cmp.fork': '
|
|
360
|
+
'cmp.parent': '(父会话)',
|
|
361
|
+
'cmp.fork': '(分支)',
|
|
198
362
|
'cmp.none': '没有其他会话可对比。',
|
|
199
363
|
'cmp.metric': '指标',
|
|
200
364
|
'cmp.delta': 'Δ',
|
|
@@ -203,34 +367,49 @@ const zh: Dict = {
|
|
|
203
367
|
'cmp.events': '事件数',
|
|
204
368
|
'cmp.toolCalls': '工具调用',
|
|
205
369
|
'cmp.toolErrors': '工具错误',
|
|
206
|
-
'cmp.inputTokens': '输入 token
|
|
370
|
+
'cmp.inputTokens': '输入 token(计费)',
|
|
207
371
|
'cmp.outputTokens': '输出 token',
|
|
208
372
|
'cmp.cacheTokens': '缓存读 token',
|
|
209
373
|
'cmp.duration': '时长',
|
|
210
374
|
'cmp.toolMix': '工具分布',
|
|
211
375
|
'cmp.tool': '工具',
|
|
212
|
-
'cmp.
|
|
376
|
+
'cmp.cost': '估算成本',
|
|
377
|
+
'cmp.risk': '风险发现',
|
|
378
|
+
'cmp.divergence': '这两个会话同源,自 seq {seq} 起分歧 — 之前完全一致,之后历史不同。',
|
|
379
|
+
'cmp.divergenceA': 'A · 首个差异事件',
|
|
380
|
+
'cmp.divergenceB': 'B · 首个差异事件',
|
|
213
381
|
'cmp.aria': '选择对比会话',
|
|
382
|
+
'cmp.relation': '血缘',
|
|
383
|
+
'cmp.relation.same': '同一会话',
|
|
384
|
+
'cmp.relation.a-parent-of-b': 'A 是 B 的祖先',
|
|
385
|
+
'cmp.relation.b-parent-of-a': 'B 是 A 的祖先',
|
|
386
|
+
'cmp.relation.siblings': '同源分支(共享父会话)',
|
|
387
|
+
'cmp.relation.none': '无直接血缘',
|
|
214
388
|
'forks.empty': '未找到会话。',
|
|
215
389
|
'forks.edge': '于第 {n} 个事件处分叉',
|
|
216
|
-
'forks.subagent': ' · 子Agent',
|
|
390
|
+
'forks.subagent': ' · 子 Agent',
|
|
217
391
|
'forks.orphan': ' · 父会话缺失',
|
|
218
392
|
'forks.aria': '会话分支树',
|
|
219
393
|
'forks.untitled': '未命名会话',
|
|
220
394
|
'forks.forkTag': ' · 分支',
|
|
221
|
-
'forks.hint': '
|
|
222
|
-
'
|
|
395
|
+
'forks.hint': '会话血缘:fork 出的会话和子 Agent 会挂在父会话下形成树;没有血缘的会话平铺为卡片。点击任意卡片即可复盘该会话。',
|
|
396
|
+
'forks.compare': '对比',
|
|
397
|
+
'forks.open': '打开',
|
|
398
|
+
'f.shell.dangerous': '危险 shell:{why}',
|
|
223
399
|
'f.shell.plain': '通过 {name} 执行 shell 命令',
|
|
224
400
|
'f.file.sensitive': '文件写入涉及敏感路径',
|
|
225
401
|
'f.file.write': '通过 {name} 修改文件',
|
|
402
|
+
'f.file.secret': '凭据被写入文件',
|
|
403
|
+
'f.file.destructive': '通过 {name} 执行破坏性文件操作',
|
|
226
404
|
'f.read.sensitive': '通过 {name} 访问敏感路径',
|
|
227
|
-
'f.network': '通过 {name}
|
|
405
|
+
'f.network': '通过 {name} 的网络请求可能携带敏感数据',
|
|
406
|
+
'f.output.secret': '工具输出包含凭据',
|
|
228
407
|
'f.approval.denied': '用户拒绝了一次审批',
|
|
229
408
|
'f.sandbox.change': '沙箱模式变更',
|
|
230
409
|
'f.permission.change': '权限预设变更',
|
|
231
|
-
'f.subagent': '派生子 Agent
|
|
232
|
-
'f.tool.error': '
|
|
233
|
-
'f.turn.error': '
|
|
410
|
+
'f.subagent': '派生子 Agent({mode})',
|
|
411
|
+
'f.tool.error': '工具失败:{name}',
|
|
412
|
+
'f.turn.error': '轮次异常结束:{kind}',
|
|
234
413
|
'why.rm-rf': '递归/强制删除',
|
|
235
414
|
'why.sudo': '提权操作',
|
|
236
415
|
'why.chmod': '权限修改',
|
|
@@ -239,18 +418,27 @@ const zh: Dict = {
|
|
|
239
418
|
'why.disk': '底层磁盘操作',
|
|
240
419
|
'why.remote': '远程主机访问',
|
|
241
420
|
'why.creds': '命令中包含凭据',
|
|
242
|
-
'
|
|
243
|
-
'
|
|
421
|
+
'why.exec-policy': '修改脚本执行策略',
|
|
422
|
+
'why.system-tamper': '篡改启动/卷影/日志',
|
|
423
|
+
'why.persistence': '持久化或账户变更',
|
|
424
|
+
'why.encoded-shell': '编码的 PowerShell 命令',
|
|
425
|
+
'why.exfil': '可能外传凭据',
|
|
426
|
+
'why.publish': '发布包/Release',
|
|
427
|
+
'meta.turnEnd': '轮次结束:{reason}',
|
|
428
|
+
'meta.compactSummary': '上下文已压缩(摘要)',
|
|
244
429
|
'meta.compactPrune': '上下文已修剪',
|
|
245
|
-
'meta.approvalAsked': '
|
|
246
|
-
'meta.approvalDecided': '
|
|
430
|
+
'meta.approvalAsked': '请求审批:{tool}',
|
|
431
|
+
'meta.approvalDecided': '审批结果:{outcome}',
|
|
247
432
|
'meta.sandbox': '沙箱模式变更',
|
|
248
433
|
'meta.permission': '权限预设变更',
|
|
249
434
|
'meta.plan': '计划模式变更',
|
|
250
|
-
'meta.subagent': '派生子 Agent
|
|
251
|
-
'meta.seed': '
|
|
435
|
+
'meta.subagent': '派生子 Agent({mode})',
|
|
436
|
+
'meta.seed': '种子边界(fork/resume 点)',
|
|
252
437
|
'meta.retry': '模型请求已重试',
|
|
253
438
|
'meta.goal': '目标变更',
|
|
439
|
+
'meta.system': '系统提示词',
|
|
440
|
+
'meta.model': '模型:{model}',
|
|
441
|
+
'meta.command': '命令:{command}',
|
|
254
442
|
}
|
|
255
443
|
|
|
256
444
|
const DICTS: Record<Lang, Dict> = { en, zh }
|
|
@@ -288,6 +476,31 @@ export function persistLang(lang: Lang): void {
|
|
|
288
476
|
}
|
|
289
477
|
}
|
|
290
478
|
|
|
479
|
+
// ---- tiny language store -------------------------------------------------
|
|
480
|
+
let cachedLang: Lang | undefined
|
|
481
|
+
const listeners = new Set<() => void>()
|
|
482
|
+
|
|
483
|
+
export function getLang(): Lang {
|
|
484
|
+
cachedLang ??= detectLang()
|
|
485
|
+
return cachedLang
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function setLang(lang: Lang): void {
|
|
489
|
+
if (cachedLang === lang) return
|
|
490
|
+
cachedLang = lang
|
|
491
|
+
persistLang(lang)
|
|
492
|
+
for (const listener of listeners) listener()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function subscribeLang(listener: () => void): () => void {
|
|
496
|
+
listeners.add(listener)
|
|
497
|
+
return () => listeners.delete(listener)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function useLang(): Lang {
|
|
501
|
+
return React.useSyncExternalStore(subscribeLang, getLang, getLang)
|
|
502
|
+
}
|
|
503
|
+
|
|
291
504
|
export interface I18n {
|
|
292
505
|
lang: Lang
|
|
293
506
|
t: T
|
|
@@ -354,6 +567,14 @@ export function localizeItemLabel(t: T, item: TimelineItem): string {
|
|
|
354
567
|
return t('meta.retry')
|
|
355
568
|
case 'goal/change':
|
|
356
569
|
return t('meta.goal')
|
|
570
|
+
case 'system/message':
|
|
571
|
+
return t('meta.system')
|
|
572
|
+
case 'model/selection':
|
|
573
|
+
return t('meta.model', { model: String(field(d, 'model') ?? '') })
|
|
574
|
+
case 'command/run': {
|
|
575
|
+
const command = field(d, 'command')
|
|
576
|
+
return t('meta.command', { command: typeof command === 'string' ? command : JSON.stringify(d) })
|
|
577
|
+
}
|
|
357
578
|
default:
|
|
358
579
|
return item.label
|
|
359
580
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import React, { useSyncExternalStore } from 'react'
|
|
|
12
12
|
import { HttpReplayApi } from './api.js'
|
|
13
13
|
import { ReplayApp } from './components/ReplayApp.js'
|
|
14
14
|
import { ReplayModal } from './components/ReplayModal.js'
|
|
15
|
-
import {
|
|
15
|
+
import { makeT, useLang } from './i18n.js'
|
|
16
16
|
import { mascotImage } from './mascot-image.js'
|
|
17
17
|
// esbuild is configured with `loader: { '.css': 'text' }`
|
|
18
18
|
// eslint-disable-next-line import/no-unresolved
|
|
@@ -93,13 +93,14 @@ function SidebarIcon(): React.ReactElement {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function SidebarReplayButton(): React.ReactElement {
|
|
96
|
-
const
|
|
96
|
+
const lang = useLang()
|
|
97
|
+
const t = makeT(lang)
|
|
97
98
|
return React.createElement(
|
|
98
99
|
'button',
|
|
99
100
|
{
|
|
100
101
|
className: 'dshr-sidebar-btn',
|
|
101
102
|
onClick: () => modalStore.set(true),
|
|
102
|
-
title: t('btn.replay')
|
|
103
|
+
title: `${t('btn.replay')} — ${t('app.reviewSub')}`,
|
|
103
104
|
},
|
|
104
105
|
React.createElement(SidebarIcon),
|
|
105
106
|
React.createElement('span', null, t('btn.replay')),
|
|
@@ -108,8 +109,9 @@ function SidebarReplayButton(): React.ReactElement {
|
|
|
108
109
|
|
|
109
110
|
function ReplayOverlay(): React.ReactElement | null {
|
|
110
111
|
const open = useSyncExternalStore(modalStore.subscribe, modalStore.isOpen, modalStore.isOpen)
|
|
112
|
+
const lang = useLang()
|
|
111
113
|
if (!open) return null
|
|
112
|
-
const t = makeT(
|
|
114
|
+
const t = makeT(lang)
|
|
113
115
|
return React.createElement(ReplayModal, {
|
|
114
116
|
api,
|
|
115
117
|
onClose: () => modalStore.set(false),
|
|
@@ -128,7 +130,7 @@ export function apply(ctx: ClientContextLike): void {
|
|
|
128
130
|
|
|
129
131
|
ctx.slots.inject('conversation.view', () =>
|
|
130
132
|
ctx.slots.register(
|
|
131
|
-
{ name: 'conversation.view', id: '
|
|
133
|
+
{ name: 'conversation.view', id: 'review', order: 60, label: 'Review' },
|
|
132
134
|
ReplayTab,
|
|
133
135
|
),
|
|
134
136
|
)
|