oh-my-knowledge 0.51.2 → 0.52.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.
Files changed (49) hide show
  1. package/README.md +27 -6
  2. package/README.zh.md +27 -6
  3. package/dist/assets/agent-skills/omk/SKILL.md +8 -8
  4. package/dist/observability/codex-conversation-index.d.ts +88 -0
  5. package/dist/observability/codex-conversation-index.js +566 -0
  6. package/dist/observability/codex-protocol.d.ts +12 -0
  7. package/dist/observability/codex-protocol.js +78 -0
  8. package/dist/observability/codex-tool-status.d.ts +16 -0
  9. package/dist/observability/codex-tool-status.js +114 -0
  10. package/dist/observability/codex-trace-adapter.js +342 -119
  11. package/dist/observability/conversation-catalog.d.ts +38 -0
  12. package/dist/observability/conversation-catalog.js +573 -0
  13. package/dist/observability/conversation-index-process.d.ts +1 -0
  14. package/dist/observability/conversation-index-process.js +31 -0
  15. package/dist/observability/conversation-view-model.d.ts +5 -0
  16. package/dist/observability/conversation-view-model.js +100 -0
  17. package/dist/observability/experience.d.ts +3 -1
  18. package/dist/observability/experience.js +248 -7
  19. package/dist/observability/inbox.js +40 -2
  20. package/dist/observability/knowledge-debugger.d.ts +4 -0
  21. package/dist/observability/knowledge-debugger.js +364 -0
  22. package/dist/observability/polling-subscription-hub.d.ts +27 -0
  23. package/dist/observability/polling-subscription-hub.js +149 -0
  24. package/dist/observability/source-record-archive.d.ts +13 -0
  25. package/dist/observability/source-record-archive.js +324 -0
  26. package/dist/observability/task-window.d.ts +8 -0
  27. package/dist/observability/task-window.js +36 -0
  28. package/dist/observability/trace-ir.d.ts +66 -1
  29. package/dist/observability/trace-source.d.ts +1 -0
  30. package/dist/observability/trace-source.js +12 -5
  31. package/dist/observability/turn-index.d.ts +3 -0
  32. package/dist/observability/turn-index.js +251 -0
  33. package/dist/renderer/conversation-renderer.d.ts +9 -0
  34. package/dist/renderer/conversation-renderer.js +410 -0
  35. package/dist/renderer/icons.js +1 -0
  36. package/dist/renderer/inline-markdown.d.ts +6 -0
  37. package/dist/renderer/inline-markdown.js +158 -0
  38. package/dist/renderer/knowledge-debugger-renderer.d.ts +9 -0
  39. package/dist/renderer/knowledge-debugger-renderer.js +2280 -0
  40. package/dist/renderer/observation-inbox-renderer.js +9 -0
  41. package/dist/renderer/skill-list-renderer.js +2 -1
  42. package/dist/renderer/trajectory-live.d.ts +42 -0
  43. package/dist/renderer/trajectory-live.js +258 -0
  44. package/dist/renderer/trajectory-routing.d.ts +60 -0
  45. package/dist/renderer/trajectory-routing.js +351 -0
  46. package/dist/server/report-server.d.ts +4 -1
  47. package/dist/server/report-server.js +274 -6
  48. package/dist/types/observability.d.ts +225 -1
  49. package/package.json +8 -5
@@ -0,0 +1,410 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DEFAULT_LANG, e, layout } from './layout.js';
3
+ import { brandLogo, icon } from './icons.js';
4
+ import { inlineMarkdownText, renderSafeInlineMarkdown } from './inline-markdown.js';
5
+ export function buildConversationActivitySnapshot(model) {
6
+ let runningCount = 0;
7
+ const state = model.conversations.map((conversation) => {
8
+ const openTask = latestOpenConversationTask(conversation);
9
+ if (openTask)
10
+ runningCount += 1;
11
+ return [
12
+ conversation.threadId,
13
+ conversation.archived ? 1 : 0,
14
+ conversation.turnCount ?? null,
15
+ openTask?.turnId ?? null,
16
+ ];
17
+ });
18
+ return {
19
+ schemaVersion: 1,
20
+ revision: createHash('sha256').update(JSON.stringify(state)).digest('hex').slice(0, 24),
21
+ runningCount,
22
+ };
23
+ }
24
+ function latestOpenConversationTask(conversation) {
25
+ for (let index = conversation.tasks.length - 1; index >= 0; index -= 1) {
26
+ const task = conversation.tasks[index];
27
+ if (task?.status === 'open')
28
+ return task;
29
+ }
30
+ return undefined;
31
+ }
32
+ export function renderConversationIndexPage(model, lang = DEFAULT_LANG) {
33
+ const zh = lang === 'zh';
34
+ const langQuery = lang === DEFAULT_LANG ? '' : '?lang=en';
35
+ const activeCount = model.unarchivedConversationCount ?? model.conversations.filter((item) => !item.archived).length;
36
+ const archivedCount = model.archivedConversationCount ?? model.conversations.filter((item) => item.archived).length;
37
+ const activity = buildConversationActivitySnapshot(model);
38
+ const runningCount = activity.runningCount;
39
+ const conversations = [...model.conversations].sort((left, right) => (Number(latestOpenConversationTask(right) !== undefined) - Number(latestOpenConversationTask(left) !== undefined)));
40
+ const rows = conversations.map((conversation) => renderConversationRow(conversation, lang)).join('');
41
+ const content = model.conversations.length > 0
42
+ ? `<div class="conversation-list" role="list">${rows}</div>`
43
+ : `<div class="empty-state"><strong>${zh ? '还没有可浏览的 Codex 对话' : 'No Codex conversations yet'}</strong><span>${zh ? 'Studio 会直接读取 Codex 的本机会话索引,不需要先运行 observe。' : 'Studio reads the local Codex conversation index directly; observe is not required.'}</span></div>`;
44
+ return layout(zh ? 'Codex 对话' : 'Codex conversations', `
45
+ <main class="conversation-page conversation-index-app" data-activity-revision="${e(activity.revision)}">
46
+ <header class="conversation-app-head">
47
+ <a class="conversation-app-brand" href="/${langQuery}">
48
+ <span>${brandLogo(28)}</span><strong>OMK</strong><em>Studio</em>
49
+ </a>
50
+ <nav class="conversation-app-nav" aria-label="${zh ? 'Studio 一级导航' : 'Studio primary navigation'}">
51
+ <a class="is-active" href="/conversations${langQuery}">${zh ? '对话' : 'Conversations'}</a>
52
+ <a href="/knowledge${langQuery}">${zh ? '知识载体' : 'Knowledge'}</a>
53
+ </nav>
54
+ </header>
55
+ <div class="conversation-app-body">
56
+ <section class="conversation-browser" aria-label="${zh ? 'Codex 对话' : 'Codex conversations'}">
57
+ <header class="conversation-toolbar">
58
+ <div class="conversation-browser-title">
59
+ <h1>${zh ? '对话' : 'Conversations'}</h1>
60
+ </div>
61
+ <div class="conversation-toolbar-actions">
62
+ <div class="conversation-filters" role="group" aria-label="${zh ? '对话筛选' : 'Conversation filter'}">
63
+ ${runningCount > 0 ? filterButton('running', zh ? '进行中' : 'Running', runningCount) : ''}
64
+ ${filterButton('all', zh ? '全部' : 'All', model.conversations.length, true)}
65
+ ${filterButton('active', zh ? '未归档' : 'Unarchived', activeCount)}
66
+ ${filterButton('archived', zh ? '已归档' : 'Archived', archivedCount)}
67
+ </div>
68
+ <label class="conversation-search">${icon('search', { size: 16 })}<span class="sr-only">${zh ? '搜索对话' : 'Search conversations'}</span><input type="search" placeholder="${zh ? '搜索标题或工作目录' : 'Search title or workspace'}" data-conversation-search></label>
69
+ </div>
70
+ </header>
71
+ <div class="conversation-columns" aria-hidden="true">
72
+ <span>${zh ? '最近活动' : 'Recent activity'}</span>
73
+ <span>${zh ? '对话' : 'Conversation'}</span>
74
+ <span>${zh ? '工作目录' : 'Workspace'}</span>
75
+ <span>${zh ? '任务' : 'Tasks'}</span>
76
+ <span></span>
77
+ </div>
78
+ <div class="conversation-list-viewport">${content}</div>
79
+ <footer class="conversation-pager">
80
+ <span data-page-range></span>
81
+ <div class="pager-controls">
82
+ <button type="button" data-page-prev aria-label="${zh ? '上一页' : 'Previous page'}" title="${zh ? '上一页' : 'Previous page'}">←</button>
83
+ <span data-page-label></span>
84
+ <button type="button" data-page-next aria-label="${zh ? '下一页' : 'Next page'}" title="${zh ? '下一页' : 'Next page'}">→</button>
85
+ </div>
86
+ </footer>
87
+ </section>
88
+ </div>
89
+ </main>
90
+ <style>${CSS}</style>
91
+ <script>${paginationScript(lang)}</script>
92
+ `, lang);
93
+ }
94
+ export function renderConversationDetailPage(conversation, lang = DEFAULT_LANG) {
95
+ const zh = lang === 'zh';
96
+ const langSuffix = lang === DEFAULT_LANG ? '' : '?lang=en';
97
+ const taskRows = conversationTaskEntries(conversation.tasks)
98
+ .map(({ task, ordinal }) => renderTaskRow(task, ordinal, lang))
99
+ .join('');
100
+ return layout(zh ? '对话任务' : 'Conversation tasks', `
101
+ <main class="conversation-page conversation-detail-page">
102
+ <header class="conversation-page-head conversation-detail-head">
103
+ <div>
104
+ <a class="back-link" href="/conversations${langSuffix}">${zh ? '返回对话总览' : 'Back to conversations'}</a>
105
+ <p class="conversation-eyebrow">CODEX · ${e(shortThreadId(conversation.sourceThreadId))}</p>
106
+ <h1>${renderSafeInlineMarkdown(conversation.title)}</h1>
107
+ <div class="detail-meta">
108
+ ${conversation.model ? `<span>${e(conversation.model)}</span>` : ''}
109
+ ${conversation.cwd ? `<span title="${e(conversation.cwd)}">${e(compactPath(conversation.cwd))}</span>` : ''}
110
+ <span>${conversation.turnCount ?? conversation.tasks.length} ${zh ? '次任务' : 'tasks'}</span>
111
+ ${(conversation.toolCallCount ?? 0) > 0 ? `<span>${conversation.toolCallCount} ${zh ? '次工具调用' : 'tool calls'}</span>` : ''}
112
+ ${(conversation.toolFailureCount ?? 0) > 0 ? `<span class="failure-count">${conversation.toolFailureCount} ${zh ? '次失败' : 'failures'}</span>` : ''}
113
+ </div>
114
+ </div>
115
+ </header>
116
+ <section class="task-list" aria-label="${zh ? '任务列表' : 'Task list'}">
117
+ <header class="task-list-head"><span>${zh ? '任务' : 'Task'}</span><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '执行' : 'Execution'}</span><span></span></header>
118
+ ${taskRows || `<div class="empty-state"><strong>${zh ? '没有识别到任务边界' : 'No task boundaries found'}</strong><span>${zh ? '该对话的原始日志可能尚未写入完整的 turn 生命周期。' : 'The raw log may not contain complete turn lifecycle records yet.'}</span></div>`}
119
+ </section>
120
+ </main>
121
+ <style>${CSS}</style>
122
+ `, lang);
123
+ }
124
+ function renderConversationRow(conversation, lang) {
125
+ const zh = lang === 'zh';
126
+ const langSuffix = lang === DEFAULT_LANG ? '' : '?lang=en';
127
+ const updated = formatRelativeDate(conversation.endTimestamp, lang);
128
+ const workspace = conversation.cwd ? compactPath(conversation.cwd) : (zh ? '未知工作目录' : 'Unknown workspace');
129
+ const state = conversation.archived ? 'archived' : 'active';
130
+ const openTask = latestOpenConversationTask(conversation);
131
+ const openTaskHref = openTask ? taskTrajectoryHref(openTask, lang) : undefined;
132
+ const detailHref = `/conversations/${encodeURIComponent(conversation.threadId)}${langSuffix}`;
133
+ const searchable = `${conversation.title} ${conversation.preview ?? ''} ${conversation.cwd ?? ''}`.toLocaleLowerCase();
134
+ const context = [
135
+ (conversation.childThreadCount ?? 0) > 0
136
+ ? `${conversation.childThreadCount} ${zh ? '个子任务' : 'child tasks'}`
137
+ : undefined,
138
+ conversation.archived ? (zh ? '已归档' : 'Archived') : undefined,
139
+ ].filter((item) => Boolean(item));
140
+ const taskMetric = conversation.turnCount === undefined
141
+ ? '<span class="index-pending">—</span>'
142
+ : `<span><b>${conversation.turnCount}</b>${zh ? '任务' : 'tasks'}</span>`;
143
+ const activity = openTask
144
+ ? `<strong class="running-label"><i aria-hidden="true"></i>${zh ? '进行中' : 'Running'}</strong><span>${e(updated)} · ${e(conversation.model ?? 'Codex')}</span>`
145
+ : `<strong>${e(updated)}</strong><span>${e(conversation.model ?? 'Codex')}</span>`;
146
+ const liveTaskLink = openTaskHref
147
+ ? `<a class="live-task-link" href="${e(openTaskHref)}" aria-label="${zh ? '查看进行中任务的实时轨迹' : 'View the live trajectory for the running task'}"><i aria-hidden="true"></i>${zh ? '查看实时轨迹' : 'View live'}</a>`
148
+ : '';
149
+ return `<div class="conversation-row${openTask ? ' is-running' : ''}" role="listitem" data-state="${state}" data-running="${openTask ? 'true' : 'false'}" data-search="${e(searchable)}">
150
+ <a class="conversation-row-detail" href="${e(detailHref)}" aria-label="${zh ? '查看对话:' : 'View conversation: '}${e(inlineMarkdownText(conversation.title))}"></a>
151
+ <div class="conversation-activity">${activity}</div>
152
+ <div class="conversation-main">
153
+ <div class="conversation-title">${renderSafeInlineMarkdown(conversation.title, { links: 'text' })}</div>
154
+ ${context.length > 0 ? `<div class="conversation-context">${context.map((item) => `<span>${e(item)}</span>`).join('')}</div>` : ''}
155
+ </div>
156
+ <div class="conversation-workspace" title="${e(conversation.cwd ?? '')}">${e(workspace)}</div>
157
+ <div class="conversation-counts">${taskMetric}${liveTaskLink}</div>
158
+ <span class="row-arrow" aria-hidden="true">${icon('chevron-right', { size: 17 })}</span>
159
+ </div>`;
160
+ }
161
+ function conversationTaskEntries(tasks) {
162
+ return tasks
163
+ .map((task, index) => ({ task, ordinal: index + 1 }))
164
+ .sort((left, right) => (Number(right.task.status === 'open') - Number(left.task.status === 'open')
165
+ || left.ordinal - right.ordinal));
166
+ }
167
+ function renderTaskRow(task, ordinal, lang) {
168
+ const zh = lang === 'zh';
169
+ const href = taskTrajectoryHref(task, lang) ?? '#';
170
+ const status = statusLabel(task.status, lang);
171
+ return `<article class="task-row${task.status === 'open' ? ' is-running' : ''}" data-task-status="${e(task.status)}">
172
+ <div class="task-index">${String(ordinal).padStart(2, '0')}</div>
173
+ <div class="task-main"><div class="task-title">${renderSafeInlineMarkdown(task.title, { links: 'text' })}</div><div class="task-context">${task.eventCount} ${zh ? '条原始日志' : 'raw records'}</div></div>
174
+ <div class="task-time"><span>${e(formatTime(task.startTimestamp))}</span><small>${e(formatDuration(task.durationMs, lang))}</small></div>
175
+ <div class="task-execution"><span class="task-status status-${e(task.status)}">${e(status)}</span><small>${task.toolCallCount} ${zh ? '次调用' : 'calls'}${task.toolFailureCount > 0 ? ` · ${task.toolFailureCount} ${zh ? '次失败' : 'failures'}` : ''}</small></div>
176
+ <a class="trajectory-link" href="${e(href)}">${zh ? '查看任务轨迹' : 'View trajectory'} →</a>
177
+ </article>`;
178
+ }
179
+ function taskTrajectoryHref(task, lang) {
180
+ if (task.trajectoryHref)
181
+ return withLang(task.trajectoryHref, lang);
182
+ if (task.experienceSessionId)
183
+ return observationTrajectoryHref(task, lang);
184
+ return undefined;
185
+ }
186
+ function observationTrajectoryHref(task, lang) {
187
+ if (!task.experienceSessionId)
188
+ return '#';
189
+ const params = new URLSearchParams();
190
+ params.set('turnId', task.turnId);
191
+ if (lang !== DEFAULT_LANG)
192
+ params.set('lang', 'en');
193
+ return `/observe-debugger/${encodeURIComponent(task.experienceSessionId)}?${params.toString()}`;
194
+ }
195
+ function withLang(path, lang) {
196
+ if (lang === DEFAULT_LANG)
197
+ return path;
198
+ return `${path}${path.includes('?') ? '&' : '?'}lang=en`;
199
+ }
200
+ function filterButton(filter, label, count, active = false) {
201
+ const disabled = count === 0 && !active;
202
+ return `<button type="button" data-view-filter="${filter}" aria-pressed="${active}"${active ? ' class="is-active"' : ''}${disabled ? ' disabled' : ''}><span>${e(label)}</span><b>${count}</b></button>`;
203
+ }
204
+ function formatTime(timestamp) {
205
+ if (!timestamp)
206
+ return '—';
207
+ const date = new Date(timestamp);
208
+ if (Number.isNaN(date.getTime()))
209
+ return timestamp;
210
+ const pad = (value) => String(value).padStart(2, '0');
211
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
212
+ }
213
+ function formatRelativeDate(timestamp, lang) {
214
+ if (!timestamp)
215
+ return lang === 'zh' ? '时间未知' : 'Unknown time';
216
+ const value = new Date(timestamp);
217
+ if (Number.isNaN(value.getTime()))
218
+ return timestamp;
219
+ const elapsed = Date.now() - value.getTime();
220
+ if (elapsed >= 0 && elapsed < 60_000)
221
+ return lang === 'zh' ? '刚刚' : 'Just now';
222
+ if (elapsed >= 0 && elapsed < 3_600_000)
223
+ return lang === 'zh' ? `${Math.floor(elapsed / 60_000)} 分钟前` : `${Math.floor(elapsed / 60_000)}m ago`;
224
+ if (elapsed >= 0 && elapsed < 86_400_000)
225
+ return lang === 'zh' ? `${Math.floor(elapsed / 3_600_000)} 小时前` : `${Math.floor(elapsed / 3_600_000)}h ago`;
226
+ return formatTime(timestamp);
227
+ }
228
+ function formatDuration(durationMs, lang) {
229
+ if (durationMs === undefined)
230
+ return '—';
231
+ if (durationMs < 1000)
232
+ return `${durationMs} ms`;
233
+ if (durationMs < 60_000)
234
+ return `${(durationMs / 1000).toFixed(1)} ${lang === 'zh' ? '秒' : 's'}`;
235
+ const minutes = Math.floor(durationMs / 60_000);
236
+ const seconds = Math.round((durationMs % 60_000) / 1000);
237
+ return lang === 'zh' ? `${minutes} 分 ${seconds} 秒` : `${minutes}m ${seconds}s`;
238
+ }
239
+ function compactPath(value) {
240
+ const parts = value.replaceAll('\\', '/').split('/').filter(Boolean);
241
+ return parts.length <= 3 ? value : `…/${parts.slice(-3).join('/')}`;
242
+ }
243
+ function shortThreadId(value) {
244
+ return value.length > 18 ? `${value.slice(0, 8)}…${value.slice(-6)}` : value;
245
+ }
246
+ function statusLabel(status, lang) {
247
+ const zh = lang === 'zh';
248
+ const labels = {
249
+ completed: ['已完成', 'Completed'],
250
+ aborted: ['已中止', 'Aborted'],
251
+ interrupted: ['已打断', 'Interrupted'],
252
+ open: ['进行中', 'Open'],
253
+ unknown: ['未记录结束状态', 'End status not recorded'],
254
+ };
255
+ return labels[status][zh ? 0 : 1];
256
+ }
257
+ function paginationScript(lang) {
258
+ const emptyText = lang === 'zh' ? '没有匹配的对话' : 'No matching conversations';
259
+ const langQuery = lang === DEFAULT_LANG ? '' : '?lang=en';
260
+ return `
261
+ (() => {
262
+ const root = document.querySelector('.conversation-index-app');
263
+ const viewport = root?.querySelector('.conversation-list-viewport');
264
+ const list = root?.querySelector('.conversation-list');
265
+ const rows = [...(root?.querySelectorAll('.conversation-row') || [])];
266
+ const viewButtons = [...(root?.querySelectorAll('[data-view-filter]') || [])];
267
+ const search = root?.querySelector('[data-conversation-search]');
268
+ const previous = root?.querySelector('[data-page-prev]');
269
+ const next = root?.querySelector('[data-page-next]');
270
+ const pageLabel = root?.querySelector('[data-page-label]');
271
+ const pageRange = root?.querySelector('[data-page-range]');
272
+ let viewFilter = 'all';
273
+ let page = 0;
274
+ let pageSize = 1;
275
+ let visibleRows = rows;
276
+ let activityTimer;
277
+ let activityRequest;
278
+ let activityStopped = false;
279
+
280
+ const computePageSize = () => {
281
+ pageSize = Math.max(1, Math.floor((viewport?.clientHeight || 68) / 68));
282
+ list?.style.setProperty('--page-size', String(pageSize));
283
+ };
284
+
285
+ const renderPage = () => {
286
+ const query = (search?.value || '').trim().toLocaleLowerCase();
287
+ visibleRows = rows.filter((row) => {
288
+ const viewMatch = viewFilter === 'all'
289
+ || (viewFilter === 'running' ? row.dataset.running === 'true' : row.dataset.state === viewFilter);
290
+ const searchMatch = !query || (row.dataset.search || '').includes(query);
291
+ return viewMatch && searchMatch;
292
+ });
293
+ const pageCount = Math.max(1, Math.ceil(visibleRows.length / pageSize));
294
+ page = Math.min(page, pageCount - 1);
295
+ const start = page * pageSize;
296
+ const end = Math.min(start + pageSize, visibleRows.length);
297
+ for (const row of rows) row.hidden = true;
298
+ for (const row of visibleRows.slice(start, end)) row.hidden = false;
299
+ if (pageLabel) pageLabel.textContent = (page + 1) + ' / ' + pageCount;
300
+ if (pageRange) pageRange.textContent = visibleRows.length > 0
301
+ ? (start + 1) + '–' + end + ' / ' + visibleRows.length
302
+ : '${emptyText}';
303
+ if (previous) previous.disabled = page === 0;
304
+ if (next) next.disabled = page >= pageCount - 1;
305
+ };
306
+
307
+ for (const button of viewButtons) button.addEventListener('click', () => {
308
+ viewFilter = button.dataset.viewFilter || 'all';
309
+ page = 0;
310
+ for (const item of viewButtons) {
311
+ const active = item === button;
312
+ item.classList.toggle('is-active', active);
313
+ item.setAttribute('aria-pressed', String(active));
314
+ }
315
+ renderPage();
316
+ });
317
+ search?.addEventListener('input', () => {
318
+ page = 0;
319
+ renderPage();
320
+ });
321
+ previous?.addEventListener('click', () => {
322
+ page = Math.max(0, page - 1);
323
+ renderPage();
324
+ });
325
+ next?.addEventListener('click', () => {
326
+ page += 1;
327
+ renderPage();
328
+ });
329
+ root?.addEventListener('keydown', (event) => {
330
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement) return;
331
+ if (event.key === 'ArrowLeft' && !previous?.disabled) previous?.click();
332
+ if (event.key === 'ArrowRight' && !next?.disabled) next?.click();
333
+ });
334
+
335
+ const resizeObserver = new ResizeObserver(() => {
336
+ computePageSize();
337
+ renderPage();
338
+ });
339
+ if (viewport) resizeObserver.observe(viewport);
340
+
341
+ const stopActivityPolling = () => {
342
+ activityStopped = true;
343
+ if (activityTimer) clearTimeout(activityTimer);
344
+ activityRequest?.abort();
345
+ };
346
+ const scheduleActivityPoll = (delay = 5000) => {
347
+ if (activityStopped) return;
348
+ if (activityTimer) clearTimeout(activityTimer);
349
+ activityTimer = setTimeout(pollActivity, delay);
350
+ };
351
+ const pollActivity = async () => {
352
+ if (activityStopped) return;
353
+ if (document.hidden) {
354
+ scheduleActivityPoll();
355
+ return;
356
+ }
357
+ activityRequest?.abort();
358
+ activityRequest = new AbortController();
359
+ try {
360
+ const response = await fetch('/api/conversations/activity${langQuery}', {
361
+ cache: 'no-store',
362
+ signal: activityRequest.signal,
363
+ });
364
+ if (!response.ok) throw new Error('conversation activity unavailable');
365
+ const snapshot = await response.json();
366
+ if (snapshot.revision && snapshot.revision !== root?.dataset.activityRevision) {
367
+ window.location.reload();
368
+ return;
369
+ }
370
+ } catch (cause) {
371
+ if (cause?.name === 'AbortError') return;
372
+ }
373
+ scheduleActivityPoll();
374
+ };
375
+ document.addEventListener('visibilitychange', () => {
376
+ if (!document.hidden) scheduleActivityPoll(0);
377
+ });
378
+ window.addEventListener('pagehide', stopActivityPolling, { once: true });
379
+ computePageSize();
380
+ renderPage();
381
+ scheduleActivityPoll();
382
+ })();`;
383
+ }
384
+ const CSS = `
385
+ .conversation-page{width:min(1240px,calc(100% - 48px));margin:0 auto;padding:34px 0 64px}
386
+ .conversation-page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:22px}
387
+ .conversation-page-head h1{font-size:28px;line-height:1.25;margin:5px 0 8px;letter-spacing:0}.conversation-page-head p:last-child{max-width:820px;color:var(--text-secondary)}
388
+ .conversation-eyebrow{font:700 12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--accent);letter-spacing:0}.secondary-link,.back-link,.trajectory-link{color:var(--accent);text-decoration:none;font-weight:650}.secondary-link{padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface);white-space:nowrap}.back-link{display:inline-block;margin-bottom:12px;font-size:13px}
389
+ .task-list{border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);overflow:hidden}
390
+ .conversation-row{position:relative;display:grid;grid-template-columns:132px minmax(320px,1.35fr) minmax(210px,.8fr) 136px 24px;gap:20px;align-items:center;padding:8px 22px;color:inherit;border-bottom:1px solid var(--border);transition:background .14s,color .14s}.conversation-row:last-child{border-bottom:0}.conversation-row:hover{background:#f9faff}.conversation-row:has(.conversation-row-detail:focus-visible){z-index:1;outline:2px solid rgba(79,70,229,.46);outline-offset:-2px}.conversation-row[hidden]{display:none}.conversation-row-detail{position:absolute;inset:0;z-index:1}.conversation-row-detail:focus-visible{outline:0}.conversation-row>*:not(.conversation-row-detail){position:relative;z-index:2;pointer-events:none}
391
+ .conversation-activity{display:flex;flex-direction:column;min-width:0}.conversation-activity strong{font-size:12px;font-weight:650;white-space:nowrap}.conversation-activity span{color:var(--text-muted);font:500 11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.running-label{display:flex;align-items:center;gap:6px;color:var(--accent)}.running-label i,.live-task-link i{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px rgba(79,70,229,.1)}
392
+ .conversation-main{min-width:0}.conversation-title{display:block;font-size:14px;font-weight:650;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.conversation-main:has(.conversation-context) .conversation-title{margin-bottom:4px}.conversation-title code,.task-title code{padding:1px 3px;border-radius:3px;background:var(--bg-elevated);font:600 .92em ui-monospace,SFMono-Regular,Menlo,monospace}.inline-markdown-link{color:var(--accent);text-decoration:underline;text-decoration-color:rgba(79,70,229,.28);text-underline-offset:3px}.inline-markdown-link:hover{text-decoration-color:currentColor}.conversation-meta,.conversation-context,.detail-meta{display:flex;align-items:center;gap:8px;color:var(--text-muted);font-size:11px;min-width:0}.conversation-meta span+span:before,.conversation-context span+span:before,.detail-meta span+span:before{content:'·';margin-right:8px}.source-mark{color:var(--accent);font-weight:700}.conversation-workspace{min-width:0;color:var(--text-secondary);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
393
+ .conversation-counts{display:flex;flex-direction:column;justify-content:center;align-items:flex-end;gap:2px;color:var(--text-secondary);font-size:11px;white-space:nowrap}.conversation-counts span{display:flex;gap:4px;align-items:baseline}.conversation-counts b{font-size:14px;color:var(--text-primary);font-variant-numeric:tabular-nums}.live-task-link{z-index:3!important;display:flex;align-items:center;gap:6px;color:var(--accent);font-weight:650;text-decoration:none;pointer-events:auto!important}.live-task-link:hover{text-decoration:underline}.failure-count{color:var(--red)!important}.index-pending{color:var(--text-muted)}.row-arrow{display:flex;align-items:center;justify-content:center;color:var(--text-faint);transition:color .14s,transform .14s}.conversation-row:hover .row-arrow{color:var(--accent);transform:translateX(2px)}.empty-state{display:flex;flex-direction:column;gap:4px;padding:36px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);color:var(--text-secondary)}.empty-state strong{color:var(--text-primary)}
394
+ .conversation-detail-head{align-items:flex-start}.conversation-detail-head h1{max-width:920px;font-size:24px}.detail-meta{margin-top:10px}.task-list-head,.task-row{display:grid;grid-template-columns:minmax(0,1fr) 140px 160px 150px;gap:18px;align-items:center}.task-list-head{padding:10px 22px 10px 72px;color:var(--text-muted);font-size:12px;background:var(--bg-elevated);border-bottom:1px solid var(--border)}.task-row{position:relative;padding:17px 22px 17px 72px;border-bottom:1px solid var(--border)}.task-row.is-running{background:rgba(79,70,229,.035);box-shadow:inset 3px 0 0 var(--accent)}.task-row:last-child{border-bottom:0}.task-index{position:absolute;left:22px;top:19px;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)}.task-row.is-running .task-index{color:var(--accent)}.task-main{min-width:0}.task-title{display:block;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px}.task-context{color:var(--text-muted);font-size:12px}.task-time,.task-execution{display:flex;flex-direction:column;gap:3px}.task-time small,.task-execution small{color:var(--text-muted)}.task-status{font-weight:650}.status-aborted,.status-interrupted{color:var(--red)}.status-open{color:var(--accent)}.status-unknown{color:var(--yellow)}.status-completed{color:var(--green)}.trajectory-link{text-align:right;font-size:13px;white-space:nowrap}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
395
+
396
+ html:has(.conversation-index-app),body:has(.conversation-index-app){height:100%;overflow:hidden;scrollbar-gutter:auto}
397
+ body:has(.conversation-index-app) .app-bar{display:none}
398
+ body:has(.conversation-index-app) .app-main{width:100%;max-width:none;height:100dvh;margin:0;padding:0}
399
+ body:has(.conversation-index-app) .footer{display:none}
400
+ .conversation-index-app{width:100%;height:100%;margin:0;padding:0;display:grid;grid-template-rows:54px minmax(0,1fr);background:var(--bg-surface);overflow:hidden}
401
+ .conversation-app-head{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;min-width:0;border-bottom:1px solid var(--border);background:rgba(255,255,255,.94)}
402
+ .conversation-app-brand{height:100%;display:flex;align-items:center;gap:8px;padding:0 22px;color:var(--text-primary);text-decoration:none}.conversation-app-brand:hover{color:var(--text-primary);text-decoration:none}.conversation-app-brand>span{display:flex}.conversation-app-brand strong{font-size:14px;letter-spacing:.01em}.conversation-app-brand em{padding:1px 7px;border:1px solid var(--border);border-radius:4px;color:var(--text-muted);font-size:10px;font-style:normal;font-weight:650;letter-spacing:.03em}
403
+ .conversation-app-nav{height:100%;display:flex;align-items:center;gap:2px;padding-left:16px}.conversation-app-nav a{height:100%;display:flex;align-items:center;padding:0 14px;border-bottom:2px solid transparent;color:var(--text-secondary);font-size:13px;font-weight:600;text-decoration:none}.conversation-app-nav a:hover{color:var(--text-primary);text-decoration:none}.conversation-app-nav a.is-active{border-bottom-color:var(--accent);color:var(--text-primary)}
404
+ .conversation-app-body{display:block;min-width:0;min-height:0}
405
+ .conversation-browser{height:100%;display:grid;grid-template-rows:62px 32px minmax(0,1fr) 42px;min-width:0;min-height:0;background:var(--bg-surface)}.conversation-toolbar{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:0 22px;margin:0}.conversation-browser-title h1{font-size:18px;line-height:1.2;margin:0;font-weight:650;letter-spacing:0}.conversation-toolbar-actions{display:flex;align-items:center;gap:12px;min-width:0}.conversation-filters{display:flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--border);border-radius:6px;background:var(--bg-elevated)}.conversation-filters button{height:28px;padding:0 9px;border:0;border-radius:4px;background:transparent;color:var(--text-secondary);display:flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap}.conversation-filters button:hover{color:var(--text-primary)}.conversation-filters button.is-active{background:var(--bg-surface);color:var(--text-primary);box-shadow:0 1px 3px rgba(31,41,55,.08)}.conversation-filters b{color:var(--text-muted);font-size:10px;font-variant-numeric:tabular-nums}.conversation-filters button:disabled{cursor:default;opacity:.46}.conversation-search{position:relative;width:min(340px,34vw);color:var(--text-muted)}.conversation-search>svg{position:absolute;left:11px;top:50%;transform:translateY(-50%);pointer-events:none}.conversation-search input{display:block;width:100%;height:34px;padding:0 11px 0 34px;border:1px solid var(--border);border-radius:6px;background:var(--bg-elevated);color:var(--text-primary);font:inherit;outline:none}.conversation-search input:focus{border-color:rgba(79,70,229,.48);box-shadow:0 0 0 3px rgba(79,70,229,.07);background:var(--bg-surface)}
406
+ .conversation-columns{display:grid;grid-template-columns:132px minmax(320px,1.35fr) minmax(210px,.8fr) 136px 24px;gap:20px;align-items:center;padding:0 22px;border-top:1px solid var(--border);border-bottom:1px solid var(--border);background:#fafbfe;color:var(--text-muted);font-size:10px;font-weight:650;letter-spacing:.03em}.conversation-list-viewport{min-height:0;overflow:hidden}.conversation-list{height:100%;display:grid;grid-template-rows:repeat(var(--page-size,8),minmax(0,1fr));background:var(--bg-surface);overflow:hidden}.conversation-list-viewport>.empty-state{height:100%;justify-content:center;border:0;border-radius:0;text-align:center}.conversation-pager{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 22px;border-top:1px solid var(--border);background:#fafbfe;color:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}.pager-controls{display:flex;align-items:center;gap:8px}.pager-controls>span{min-width:54px;text-align:center;color:var(--text-secondary)}.pager-controls button{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid var(--border);border-radius:5px;background:var(--bg-surface);color:var(--text-secondary);font-size:15px;line-height:1}.pager-controls button:hover:not(:disabled){border-color:var(--border-hover);color:var(--accent)}.pager-controls button:disabled{cursor:default;opacity:.3}
407
+ @media(max-width:1100px){.conversation-row,.conversation-columns{grid-template-columns:118px minmax(260px,1fr) 124px 24px}.conversation-workspace,.conversation-columns span:nth-child(3){display:none}.conversation-app-brand{padding-left:16px}.conversation-search{width:min(300px,32vw)}}
408
+ @media(max-width:820px){.conversation-row,.conversation-columns{grid-template-columns:102px minmax(0,1fr) 112px 20px;gap:12px}.conversation-toolbar-actions{gap:8px}.conversation-filters b{display:none}.conversation-search{width:min(260px,34vw)}}
409
+ @media(max-width:720px){.conversation-page:not(.conversation-index-app){width:calc(100% - 24px);padding-top:24px}.conversation-index-app{grid-template-rows:50px minmax(0,1fr)}.conversation-app-brand{padding:0 12px}.conversation-app-brand strong{display:none}.conversation-app-nav{padding-left:2px}.conversation-app-nav a{padding:0 9px}.conversation-browser{grid-template-rows:82px 28px minmax(0,1fr) 40px}.conversation-toolbar{align-items:flex-start;gap:8px;padding:12px}.conversation-browser-title h1{font-size:16px}.conversation-toolbar-actions{align-items:flex-end;flex-direction:column-reverse;gap:6px}.conversation-filters button{height:24px;padding:0 7px}.conversation-search{width:min(250px,66vw)}.conversation-search input{height:30px}.conversation-columns,.conversation-row{grid-template-columns:86px minmax(0,1fr) 20px;gap:10px;padding-left:12px;padding-right:12px}.conversation-activity span{display:none}.conversation-title{font-size:13px}.conversation-context span:nth-child(n+2){display:none}.task-list-head{display:none}.task-row{grid-template-columns:minmax(0,1fr) auto;padding-left:52px}.task-time,.task-execution{display:none}}
410
+ `;
@@ -19,6 +19,7 @@ const PATHS = {
19
19
  'zoom-in': '<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.4-4.4M11 8v6M8 11h6"/>',
20
20
  'zoom-out': '<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.4-4.4M8 11h6"/>',
21
21
  'maximize-2': '<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>',
22
+ copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M15 9V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7a2 2 0 002 2h3"/>',
22
23
  // 报告类型(= 维度)
23
24
  doctor: '<path d="M3 12h4l2-6 4 14 2-8h6"/>',
24
25
  eval: '<rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 11l2 2 4-4"/>',
@@ -0,0 +1,6 @@
1
+ export interface InlineMarkdownOptions {
2
+ links?: 'anchors' | 'text';
3
+ maxLength?: number;
4
+ }
5
+ export declare function renderSafeInlineMarkdown(value: string, options?: InlineMarkdownOptions): string;
6
+ export declare function inlineMarkdownText(value: string): string;
@@ -0,0 +1,158 @@
1
+ import { e } from './layout.js';
2
+ export function renderSafeInlineMarkdown(value, options = {}) {
3
+ const nodes = parseInline(value);
4
+ const visibleLength = plainText(nodes).length;
5
+ const limitedNodes = options.maxLength !== undefined && visibleLength > options.maxLength
6
+ ? [...takeVisibleText(nodes, Math.max(0, options.maxLength - 1)), { nodeKind: 'text', value: '…' }]
7
+ : nodes;
8
+ return renderNodes(limitedNodes, options.links ?? 'anchors');
9
+ }
10
+ export function inlineMarkdownText(value) {
11
+ return plainText(parseInline(value));
12
+ }
13
+ function parseInline(value, depth = 0) {
14
+ if (depth >= 4)
15
+ return [{ nodeKind: 'text', value }];
16
+ const nodes = [];
17
+ let text = '';
18
+ let index = 0;
19
+ const flushText = () => {
20
+ if (!text)
21
+ return;
22
+ nodes.push({ nodeKind: 'text', value: text });
23
+ text = '';
24
+ };
25
+ while (index < value.length) {
26
+ if (value[index] === '\\' && index + 1 < value.length) {
27
+ text += value[index + 1];
28
+ index += 2;
29
+ continue;
30
+ }
31
+ if (value[index] === '`') {
32
+ const closing = value.indexOf('`', index + 1);
33
+ if (closing > index + 1) {
34
+ flushText();
35
+ nodes.push({ nodeKind: 'code', value: value.slice(index + 1, closing) });
36
+ index = closing + 1;
37
+ continue;
38
+ }
39
+ }
40
+ if (value[index] === '[') {
41
+ const labelEnd = value.indexOf('](', index + 1);
42
+ if (labelEnd > index + 1) {
43
+ const urlEnd = matchingParenthesis(value, labelEnd + 1);
44
+ if (urlEnd > labelEnd + 2) {
45
+ flushText();
46
+ nodes.push({
47
+ nodeKind: 'link',
48
+ href: value.slice(labelEnd + 2, urlEnd).trim(),
49
+ children: parseInline(value.slice(index + 1, labelEnd), depth + 1),
50
+ });
51
+ index = urlEnd + 1;
52
+ continue;
53
+ }
54
+ }
55
+ }
56
+ if (value.startsWith('**', index)) {
57
+ const closing = value.indexOf('**', index + 2);
58
+ if (closing > index + 2) {
59
+ flushText();
60
+ nodes.push({
61
+ nodeKind: 'strong',
62
+ children: parseInline(value.slice(index + 2, closing), depth + 1),
63
+ });
64
+ index = closing + 2;
65
+ continue;
66
+ }
67
+ }
68
+ if (value[index] === '*') {
69
+ const closing = value.indexOf('*', index + 1);
70
+ if (closing > index + 1) {
71
+ flushText();
72
+ nodes.push({
73
+ nodeKind: 'emphasis',
74
+ children: parseInline(value.slice(index + 1, closing), depth + 1),
75
+ });
76
+ index = closing + 1;
77
+ continue;
78
+ }
79
+ }
80
+ text += value[index];
81
+ index += 1;
82
+ }
83
+ flushText();
84
+ return nodes;
85
+ }
86
+ function matchingParenthesis(value, opening) {
87
+ if (value[opening] !== '(')
88
+ return -1;
89
+ let depth = 1;
90
+ for (let index = opening + 1; index < value.length; index += 1) {
91
+ if (value[index] === '\\') {
92
+ index += 1;
93
+ continue;
94
+ }
95
+ if (value[index] === '(')
96
+ depth += 1;
97
+ if (value[index] === ')')
98
+ depth -= 1;
99
+ if (depth === 0)
100
+ return index;
101
+ }
102
+ return -1;
103
+ }
104
+ function renderNodes(nodes, links) {
105
+ return nodes.map((node) => {
106
+ if (node.nodeKind === 'text')
107
+ return e(node.value);
108
+ if (node.nodeKind === 'code')
109
+ return `<code>${e(node.value)}</code>`;
110
+ if (node.nodeKind === 'strong')
111
+ return `<strong>${renderNodes(node.children, links)}</strong>`;
112
+ if (node.nodeKind === 'emphasis')
113
+ return `<em>${renderNodes(node.children, links)}</em>`;
114
+ const label = renderNodes(node.children, links);
115
+ const href = safeLinkHref(node.href);
116
+ if (links === 'text' || !href)
117
+ return label;
118
+ return `<a class="inline-markdown-link" href="${e(href)}" target="_blank" rel="noreferrer noopener">${label}</a>`;
119
+ }).join('');
120
+ }
121
+ function plainText(nodes) {
122
+ return nodes.map((node) => (node.nodeKind === 'text' || node.nodeKind === 'code'
123
+ ? node.value
124
+ : plainText(node.children))).join('');
125
+ }
126
+ function takeVisibleText(nodes, maxLength) {
127
+ let remaining = maxLength;
128
+ const take = (items) => {
129
+ const result = [];
130
+ for (const node of items) {
131
+ if (remaining <= 0)
132
+ break;
133
+ if (node.nodeKind === 'text' || node.nodeKind === 'code') {
134
+ const value = node.value.slice(0, remaining);
135
+ remaining -= value.length;
136
+ if (value)
137
+ result.push({ ...node, value });
138
+ continue;
139
+ }
140
+ const children = take(node.children);
141
+ if (children.length > 0)
142
+ result.push({ ...node, children });
143
+ }
144
+ return result;
145
+ };
146
+ return take(nodes);
147
+ }
148
+ function safeLinkHref(value) {
149
+ try {
150
+ const url = new URL(value);
151
+ return url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'mailto:'
152
+ ? url.href
153
+ : undefined;
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
@@ -0,0 +1,9 @@
1
+ import type { Lang } from '../types/index.js';
2
+ import type { KnowledgeDebuggerViewModel } from '../types/index.js';
3
+ export declare function renderKnowledgeDebuggerPage(model: KnowledgeDebuggerViewModel, lang?: Lang, options?: {
4
+ sourceRecordsEndpoint?: string;
5
+ live?: {
6
+ endpoint: string;
7
+ revision: string;
8
+ };
9
+ }): string;