dsh-compact-relay 0.3.2 → 0.3.4

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 (43) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.en.md +8 -4
  3. package/README.md +8 -4
  4. package/ROADMAP.md +30 -3
  5. package/TUTORIAL.md +3 -2
  6. package/lib/actions/better-compact.js +24 -72
  7. package/lib/actions/compact-run.js +76 -0
  8. package/lib/actions/compress-hint.js +16 -7
  9. package/lib/actions/handoff-docs.js +9 -3
  10. package/lib/actions/handoff.js +5 -22
  11. package/lib/actions/settings.js +1 -4
  12. package/lib/actions/state.js +2 -1
  13. package/lib/actions/support.js +76 -0
  14. package/lib/actions/test-compact.js +12 -41
  15. package/lib/adapters/http.js +2 -1
  16. package/lib/adapters/prompt.js +72 -0
  17. package/lib/adapters/tool.js +12 -8
  18. package/lib/client.js +424 -96
  19. package/lib/config.js +1 -2
  20. package/lib/core/compress-cue.js +96 -0
  21. package/lib/core/compress-hint.js +1 -1
  22. package/lib/core/errors.js +0 -1
  23. package/lib/core/handoff-spec.js +1 -1
  24. package/lib/core/markdown-lite.js +178 -0
  25. package/lib/core/object.js +19 -0
  26. package/lib/core/ranges.js +1 -2
  27. package/lib/core/relay.js +2 -2
  28. package/lib/core/tokens.js +34 -0
  29. package/lib/dsh/compaction-log.js +3 -19
  30. package/lib/dsh/compaction-pair.js +48 -0
  31. package/lib/dsh/engine.js +2 -2
  32. package/lib/dsh/handoff-files.js +28 -0
  33. package/lib/dsh/handoff-writer.js +16 -21
  34. package/lib/dsh/model-task.js +112 -0
  35. package/lib/dsh/relay.js +1 -1
  36. package/lib/dsh/session-view.js +5 -17
  37. package/lib/dsh/settings-file.js +1 -4
  38. package/lib/dsh/state-hub.js +95 -19
  39. package/lib/dsh/summarizer.js +16 -25
  40. package/lib/index.js +28 -6
  41. package/package.json +2 -2
  42. package/lib/actions/stage.js +0 -36
  43. package/lib/core/stage.js +0 -96
package/lib/client.js CHANGED
@@ -376,6 +376,355 @@ window.__ModuleLoader__.load({
376
376
  return theme;
377
377
  }
378
378
 
379
+ /**
380
+ * 跨片段共享的浮层 / 指示灯环小部件(30-indicator 与 50-settings 共用)。
381
+ *
382
+ * 为什么单独一个片段:这些几何/材质在「指示灯」和「设置页教学卡片」里各画过一遍,
383
+ * 以前是两份点坐标/弧线 path/原生线刻度/闪烁 keyframes。放在 15 号片段(按文件名
384
+ * 排序在 10-runtime 之后、20-format/30-indicator/50-settings 之前),函数声明会
385
+ * 提升,后面的片段可直接调用;常量在使用点(组件渲染时)早已初始化。
386
+ */
387
+
388
+ /**
389
+ * 环上比例 → SVG 坐标(viewBox 0 0 20 20,圆心 10,10,半径用 10-runtime 的 RADIUS)。
390
+ * @param {number} value 0–1 的比例
391
+ * @returns {[number, number]}
392
+ */
393
+ function ringPoint(value) {
394
+ const angle = value * Math.PI * 2 - Math.PI / 2;
395
+ return [10 + RADIUS * Math.cos(angle), 10 + RADIUS * Math.sin(angle)];
396
+ }
397
+
398
+ /**
399
+ * 环上两比例之间的弧线 path(0.5 以上用大弧标志)。
400
+ * @param {number} from
401
+ * @param {number} to
402
+ * @returns {string}
403
+ */
404
+ function ringArcPath(from, to) {
405
+ const [x1, y1] = ringPoint(from);
406
+ const [x2, y2] = ringPoint(to);
407
+ const large = to - from > 0.5 ? 1 : 0;
408
+ return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A ${RADIUS} ${RADIUS} 0 ${large} 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
409
+ }
410
+
411
+ /**
412
+ * DSH 原生自动压缩线的「径向刻度」:穿过环带的一条短线(内 5.6 → 外 9.6)。
413
+ * 指示灯与设置页教学图用同一套几何,只是描边/属性不同。
414
+ * @param {number} value 合法的原生阈值比例(0.02–1 之间,调用方先校验)
415
+ * @returns {{x1:number, y1:number, x2:number, y2:number}}
416
+ */
417
+ function nativeTickAt(value) {
418
+ const angle = value * Math.PI * 2 - Math.PI / 2;
419
+ const cos = Math.cos(angle);
420
+ const sin = Math.sin(angle);
421
+ return { x1: 10 + 5.6 * cos, y1: 10 + 5.6 * sin, x2: 10 + 9.6 * cos, y2: 10 + 9.6 * sin };
422
+ }
423
+
424
+ /** 越过原生自动压缩线后「整环红色闪烁」的 keyframes(指示灯/教学图共用一份)。 */
425
+ const NATIVE_ALERT_CSS = '@keyframes cr-native-alert { 0%,100% { opacity: 1 } 50% { opacity: 0.15 } } @media (prefers-reduced-motion: reduce) { [data-cr-native-alert] { animation: none !important; opacity: 0.85 !important; } }';
426
+
427
+ /**
428
+ * 浮层外观(菜单 / 悬停卡 / 确认卡 / 交接单面板 / 历史面板 / toast 共用)。
429
+ * 主题色由 10-runtime 的 detectTheme() 给出(BUG-001:优先 DSH 主题 token)。
430
+ * @param {object} theme detectTheme() 的返回值
431
+ * @returns {{background:string, backdropFilter:string, WebkitBackdropFilter:string, border:string, boxShadow:string}}
432
+ */
433
+ function panelChrome(theme) {
434
+ return {
435
+ background: theme.panelBg,
436
+ backdropFilter: theme.panelBackdrop,
437
+ WebkitBackdropFilter: theme.panelBackdrop,
438
+ border: `1px solid ${theme.panelBorder}`,
439
+ boxShadow: theme.panelShadow,
440
+ };
441
+ }
442
+
443
+ /**
444
+ * 轻量 Markdown 解析(0.3.4 BUG-005)—— 纯函数,零依赖。
445
+ *
446
+ * 为什么自己写:交接单详情面板以前把 markdown 原样塞进 `<pre>`,用户在面板里看到的是
447
+ * `## 目标`/`- xxx`/`**粗体**` 源码。引第三方 markdown 渲染器会增加运行时依赖
448
+ * (插件只有 peerDependencies、零运行时依赖是硬约束),而 `dangerouslySetInnerHTML`
449
+ * 又引入 XSS 面。所以这里只解析交接单实际用到的子集,产出**结构化数据**,由客户端
450
+ * 用 React 文本节点渲染 —— 从不生成 HTML,任何 `<script>` 都只会显示成文字。
451
+ *
452
+ * 支持(够用即可,不做完整 CommonMark):
453
+ * - 标题 `#`–`######`(段落里的行首)
454
+ * - 段落(连续非空行;保留原始换行)
455
+ * - 无序列表 `-` / `*` / `+`
456
+ * - 有序列表 `1.` / `1)`(保留起始编号)
457
+ * - 围栏代码块 ```(保留语言标记;块内不做行内解析)
458
+ * - 分隔线 `---` / `***` / `___`
459
+ * - 行内:`**粗体**`、`` `行内代码` ``、反斜杠转义
460
+ *
461
+ * 返回值约定(客户端渲染器按此映射 React 节点):
462
+ * blocks: Array<
463
+ * | {type:'heading', level:number, segments:Segment[]}
464
+ * | {type:'paragraph', segments:Segment[]}
465
+ * | {type:'list', ordered:boolean, start:number, items:Segment[][]}
466
+ * | {type:'code', lang:string, text:string}
467
+ * | {type:'hr'}
468
+ * >
469
+ * Segment = {type:'text'|'strong'|'code', text:string}
470
+ */
471
+
472
+ const FENCE_RE = /^\s*```\s*([^\s`]*)\s*$/;
473
+ const HEADING_RE = /^(#{1,6})\s+(.*)$/;
474
+ const HR_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
475
+ const UL_RE = /^\s*[-*+]\s+(.*)$/;
476
+ const OL_RE = /^\s*(\d+)[.)]\s+(.*)$/;
477
+
478
+ /**
479
+ * 解析行内标记:`**粗体**`、`` `代码` ``、反斜杠转义。
480
+ * 未闭合的标记按普通文本处理(不吞字符)。
481
+ * @param {string} text
482
+ * @returns {{type:'text'|'strong'|'code', text:string}[]}
483
+ */
484
+ function parseInlineLite(text) {
485
+ const source = typeof text === 'string' ? text : String(text ?? '');
486
+ const segments = [];
487
+ let buffer = '';
488
+ const flush = () => {
489
+ if (buffer.length > 0) {
490
+ segments.push({ type: 'text', text: buffer });
491
+ buffer = '';
492
+ }
493
+ };
494
+ let index = 0;
495
+ while (index < source.length) {
496
+ const char = source[index];
497
+ // 反斜杠转义:下一个字符按字面量处理(\* 不开启粗体)。
498
+ if (char === '\\' && index + 1 < source.length) {
499
+ buffer += source[index + 1];
500
+ index += 2;
501
+ continue;
502
+ }
503
+ if (char === '`') {
504
+ const end = source.indexOf('`', index + 1);
505
+ if (end === -1) {
506
+ buffer += char;
507
+ index += 1;
508
+ continue;
509
+ }
510
+ flush();
511
+ segments.push({ type: 'code', text: source.slice(index + 1, end) });
512
+ index = end + 1;
513
+ continue;
514
+ }
515
+ if (char === '*' && source[index + 1] === '*') {
516
+ const end = source.indexOf('**', index + 2);
517
+ if (end === -1) {
518
+ buffer += '**';
519
+ index += 2;
520
+ continue;
521
+ }
522
+ flush();
523
+ segments.push({ type: 'strong', text: source.slice(index + 2, end) });
524
+ index = end + 2;
525
+ continue;
526
+ }
527
+ buffer += char;
528
+ index += 1;
529
+ }
530
+ flush();
531
+ return segments;
532
+ }
533
+
534
+ /**
535
+ * 把 markdown 文本解析成块数组;空文本返回 []。
536
+ * @param {string} text
537
+ * @returns {object[]}
538
+ */
539
+ function parseMarkdownLite(text) {
540
+ const source = typeof text === 'string' ? text : String(text ?? '');
541
+ const lines = source.replace(/\r\n?/g, '\n').split('\n');
542
+ const blocks = [];
543
+ let index = 0;
544
+
545
+ // 连续非空行攒成段落;遇到块级语法先 flush。
546
+ let paragraph = [];
547
+ const flushParagraph = () => {
548
+ if (paragraph.length === 0) return;
549
+ blocks.push({ type: 'paragraph', segments: parseInlineLite(paragraph.join('\n')) });
550
+ paragraph = [];
551
+ };
552
+
553
+ while (index < lines.length) {
554
+ const line = lines[index];
555
+
556
+ // 围栏代码块
557
+ const fence = FENCE_RE.exec(line);
558
+ if (fence !== null) {
559
+ flushParagraph();
560
+ const codeLines = [];
561
+ index += 1;
562
+ while (index < lines.length && FENCE_RE.exec(lines[index]) === null) {
563
+ codeLines.push(lines[index]);
564
+ index += 1;
565
+ }
566
+ // 吃掉收尾围栏(未闭合时到文件尾)
567
+ if (index < lines.length) index += 1;
568
+ blocks.push({ type: 'code', lang: fence[1] ?? '', text: codeLines.join('\n') });
569
+ continue;
570
+ }
571
+
572
+ // 空行:结束段落
573
+ if (line.trim().length === 0) {
574
+ flushParagraph();
575
+ index += 1;
576
+ continue;
577
+ }
578
+
579
+ // 标题
580
+ const heading = HEADING_RE.exec(line);
581
+ if (heading !== null) {
582
+ flushParagraph();
583
+ blocks.push({ type: 'heading', level: heading[1].length, segments: parseInlineLite(heading[2]) });
584
+ index += 1;
585
+ continue;
586
+ }
587
+
588
+ // 分隔线(注意:`---` 也匹配无序列表语法,必须先判 hr)
589
+ if (HR_RE.test(line)) {
590
+ flushParagraph();
591
+ blocks.push({ type: 'hr' });
592
+ index += 1;
593
+ continue;
594
+ }
595
+
596
+ const unordered = UL_RE.exec(line);
597
+ const ordered = unordered === null ? OL_RE.exec(line) : null;
598
+ if (unordered !== null || ordered !== null) {
599
+ flushParagraph();
600
+ const isOrdered = ordered !== null;
601
+ const start = isOrdered ? Number(ordered[1]) : 1;
602
+ const items = [];
603
+ const itemRe = isOrdered ? OL_RE : UL_RE;
604
+ while (index < lines.length) {
605
+ const matched = itemRe.exec(lines[index]);
606
+ if (matched === null) break;
607
+ items.push(parseInlineLite(isOrdered ? matched[2] : matched[1]));
608
+ index += 1;
609
+ }
610
+ blocks.push({ type: 'list', ordered: isOrdered, start, items });
611
+ continue;
612
+ }
613
+
614
+ // 普通段落行(保留换行)
615
+ paragraph.push(line);
616
+ index += 1;
617
+ }
618
+ flushParagraph();
619
+ return blocks;
620
+ }
621
+
622
+ /**
623
+ * 交接单详情面板的轻量 Markdown 渲染(0.3.4 BUG-005)。
624
+ *
625
+ * 解析在 core/markdown-lite.js(构建时内联,同一份源码有 node 单测),这里只把
626
+ * 结构化块映射成 React 节点。全部是 React 文本节点,**从不生成 HTML**,
627
+ * 因此交接单内容里的 `<script>` 只会显示成文字,没有 XSS 面。
628
+ */
629
+ const MARKDOWN_MONO_STYLE = {
630
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
631
+ };
632
+ const MARKDOWN_INLINE_CODE_STYLE = {
633
+ ...MARKDOWN_MONO_STYLE,
634
+ fontSize: 10.5,
635
+ background: 'rgba(127,127,127,0.16)',
636
+ borderRadius: 4,
637
+ padding: '0 3px',
638
+ };
639
+ const MARKDOWN_CODE_BLOCK_STYLE = {
640
+ ...MARKDOWN_MONO_STYLE,
641
+ margin: '6px 0',
642
+ padding: '6px 8px',
643
+ borderRadius: 6,
644
+ background: 'rgba(127,127,127,0.16)',
645
+ overflowX: 'auto',
646
+ whiteSpace: 'pre-wrap',
647
+ wordBreak: 'break-word',
648
+ fontSize: 10.5,
649
+ lineHeight: '15px',
650
+ };
651
+
652
+ /** 行内片段 → React 子节点(文本/粗体/行内代码)。 */
653
+ function renderInlineLite(segments, keyPrefix) {
654
+ return segments.map((segment, index) => {
655
+ const key = `${keyPrefix}-${index}`;
656
+ if (segment.type === 'strong') return h('strong', { key }, segment.text);
657
+ if (segment.type === 'code') return h('code', { key, style: MARKDOWN_INLINE_CODE_STYLE }, segment.text);
658
+ return segment.text;
659
+ });
660
+ }
661
+
662
+ /**
663
+ * 把 markdown 文本渲染成 React 节点数组(标题/段落/列表/代码块/分隔线)。
664
+ * @param {string} text
665
+ * @returns {object[]}
666
+ */
667
+ function renderMarkdownLite(text) {
668
+ const blocks = parseMarkdownLite(typeof text === 'string' ? text : '');
669
+ return blocks
670
+ .map((block, index) => {
671
+ const key = `md-${index}`;
672
+ if (block.type === 'heading') {
673
+ return h(
674
+ 'div',
675
+ {
676
+ key,
677
+ style: {
678
+ fontWeight: 600,
679
+ fontSize: block.level <= 2 ? 13 : 12,
680
+ lineHeight: '17px',
681
+ marginTop: index === 0 ? 0 : 10,
682
+ marginBottom: 4,
683
+ },
684
+ },
685
+ renderInlineLite(block.segments, `${key}-h`),
686
+ );
687
+ }
688
+ if (block.type === 'paragraph') {
689
+ return h(
690
+ 'div',
691
+ { key, style: { margin: '4px 0', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
692
+ renderInlineLite(block.segments, `${key}-p`),
693
+ );
694
+ }
695
+ if (block.type === 'list') {
696
+ return h(
697
+ 'div',
698
+ { key, style: { margin: '2px 0 6px' } },
699
+ block.items.map((item, itemIndex) =>
700
+ h(
701
+ 'div',
702
+ { key: `${key}-i${itemIndex}`, style: { display: 'flex', gap: 6, alignItems: 'flex-start' } },
703
+ h(
704
+ 'span',
705
+ { style: { flex: 'none', minWidth: block.ordered ? 18 : 8, opacity: 0.7 } },
706
+ block.ordered ? `${block.start + itemIndex}.` : '•',
707
+ ),
708
+ h(
709
+ 'span',
710
+ { style: { flex: 1, minWidth: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
711
+ renderInlineLite(item, `${key}-i${itemIndex}-s`),
712
+ ),
713
+ ),
714
+ ),
715
+ );
716
+ }
717
+ if (block.type === 'code') {
718
+ return h('pre', { key, style: MARKDOWN_CODE_BLOCK_STYLE }, block.text);
719
+ }
720
+ if (block.type === 'hr') {
721
+ return h('div', { key, style: { borderTop: '1px solid rgba(127,127,127,0.35)', margin: '8px 0' } });
722
+ }
723
+ return null;
724
+ })
725
+ .filter((node) => node !== null);
726
+ }
727
+
379
728
  const fmt = (n) => {
380
729
  if (n == null) return '—';
381
730
  if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
@@ -841,16 +1190,7 @@ window.__ModuleLoader__.load({
841
1190
  const nativeCrossed = nativeValid && ratio > nativeAuto;
842
1191
  // 断点缺口:0.9 SVG 单位(≈6.4°),折算成比例;段太短时缺口会吃掉整段,就跳过该段。
843
1192
  const ringGapRatio = 0.9 / CIRC;
844
- const ringPoint = (value) => {
845
- const angle = value * Math.PI * 2 - Math.PI / 2;
846
- return [10 + RADIUS * Math.cos(angle), 10 + RADIUS * Math.sin(angle)];
847
- };
848
- const ringArcPath = (from, to) => {
849
- const [x1, y1] = ringPoint(from);
850
- const [x2, y2] = ringPoint(to);
851
- const largeArc = to - from > 0.5 ? 1 : 0;
852
- return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A ${RADIUS} ${RADIUS} 0 ${largeArc} 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
853
- };
1193
+ // 点坐标/弧线 path 是共享几何(15-shared.js 的 ringPoint / ringArcPath)。
854
1194
  const ringNodes = ring.segments
855
1195
  .map((segment, index) => {
856
1196
  const trimmed = trimSegment(segment, ringGapRatio, ring.ratio);
@@ -871,14 +1211,11 @@ window.__ModuleLoader__.load({
871
1211
  // 刻度用一条「穿过环带」的短线(内 5.6 → 外 9.6)+ 一圈主题底色描边,避免和红色分段糊在一起。
872
1212
  const nativeNodes = [];
873
1213
  if (nativeValid) {
874
- const nativeAngle = nativeAuto * Math.PI * 2 - Math.PI / 2;
875
- const nativeCos = Math.cos(nativeAngle);
876
- const nativeSin = Math.sin(nativeAngle);
877
- const tick = { x1: 10 + 5.6 * nativeCos, y1: 10 + 5.6 * nativeSin, x2: 10 + 9.6 * nativeCos, y2: 10 + 9.6 * nativeSin };
1214
+ const tick = nativeTickAt(nativeAuto);
878
1215
  // 越过原生压缩线:整环红色闪烁(1s 呼吸);prefers-reduced-motion 下改为常亮红色(不闪)。
879
1216
  if (nativeCrossed) {
880
1217
  nativeNodes.push(
881
- h('style', { key: 'native-alert-style' }, '@keyframes cr-native-alert { 0%,100% { opacity: 1 } 50% { opacity: 0.15 } } @media (prefers-reduced-motion: reduce) { [data-cr-native-alert] { animation: none !important; opacity: 0.85 !important; } }'),
1218
+ h('style', { key: 'native-alert-style' }, NATIVE_ALERT_CSS),
882
1219
  h('circle', {
883
1220
  key: 'native-alert',
884
1221
  cx: 10,
@@ -1125,7 +1462,9 @@ window.__ModuleLoader__.load({
1125
1462
 
1126
1463
  // M6:交接单列表(菜单入口)——只读接口,点开可看全文/复制路径。
1127
1464
  // P2-C3/C4:请求带序号 + AbortController;只接受「仍然是当前选中文件」的响应。
1128
- const loadDocs = useCallback(async () => {
1465
+ // 0.3.4(BUG-006):列表先取最近 20 份,「加载更多」再把 limit 提到 50;
1466
+ // 端点返回 total,界面据此显示「共 N 份(显示最近 M)」并在无更多时隐藏按钮。
1467
+ const loadDocs = useCallback(async (limit = 20) => {
1129
1468
  setOpen(false);
1130
1469
  setPreview(null);
1131
1470
  closeDiff();
@@ -1141,7 +1480,7 @@ window.__ModuleLoader__.load({
1141
1480
  const reqId = ++docsRequestRef.current;
1142
1481
  setDocs({ loading: true });
1143
1482
  try {
1144
- const res = await fetch('/api/compact-relay-handoffs?limit=20', {
1483
+ const res = await fetch(`/api/compact-relay-handoffs?limit=${limit}`, {
1145
1484
  cache: 'no-store',
1146
1485
  ...(controller === null ? {} : { signal: controller.signal }),
1147
1486
  });
@@ -1151,7 +1490,9 @@ window.__ModuleLoader__.load({
1151
1490
  setDocs({ error: data && data.message ? String(data.message) : '读取交接单失败' });
1152
1491
  return;
1153
1492
  }
1154
- setDocs({ items: Array.isArray(data.items) ? data.items : [], selected: null });
1493
+ const items = Array.isArray(data.items) ? data.items : [];
1494
+ const total = typeof data.total === 'number' && Number.isFinite(data.total) ? data.total : items.length;
1495
+ setDocs({ items, total, limit, selected: null });
1155
1496
  } catch (error) {
1156
1497
  if (reqId !== docsRequestRef.current || (error && error.name === 'AbortError')) return;
1157
1498
  setDocs({ error: '读取交接单失败:' + (error && error.message ? error.message : '未知错误') });
@@ -1399,11 +1740,7 @@ window.__ModuleLoader__.load({
1399
1740
  wordBreak: 'break-word',
1400
1741
  padding: '6px 9px',
1401
1742
  borderRadius: 8,
1402
- background: theme.panelBg,
1403
- backdropFilter: theme.panelBackdrop,
1404
- WebkitBackdropFilter: theme.panelBackdrop,
1405
- border: `1px solid ${theme.panelBorder}`,
1406
- boxShadow: theme.panelShadow,
1743
+ ...panelChrome(theme),
1407
1744
  color: theme.panelFg,
1408
1745
  fontSize: 11,
1409
1746
  lineHeight: '16px',
@@ -1591,11 +1928,7 @@ window.__ModuleLoader__.load({
1591
1928
  overflowY: 'auto',
1592
1929
  padding: 6,
1593
1930
  borderRadius: 12,
1594
- background: theme.panelBg,
1595
- backdropFilter: theme.panelBackdrop,
1596
- WebkitBackdropFilter: theme.panelBackdrop,
1597
- border: `1px solid ${theme.panelBorder}`,
1598
- boxShadow: theme.panelShadow,
1931
+ ...panelChrome(theme),
1599
1932
  zIndex: 40,
1600
1933
  },
1601
1934
  },
@@ -1702,11 +2035,7 @@ window.__ModuleLoader__.load({
1702
2035
  maxWidth: 'calc(100vw - 16px)',
1703
2036
  padding: '10px 12px',
1704
2037
  borderRadius: 12,
1705
- background: theme.panelBg,
1706
- backdropFilter: theme.panelBackdrop,
1707
- WebkitBackdropFilter: theme.panelBackdrop,
1708
- border: `1px solid ${theme.panelBorder}`,
1709
- boxShadow: theme.panelShadow,
2038
+ ...panelChrome(theme),
1710
2039
  color: theme.panelFg,
1711
2040
  zIndex: 42,
1712
2041
  },
@@ -1821,12 +2150,8 @@ window.__ModuleLoader__.load({
1821
2150
  gap: 8,
1822
2151
  padding: '10px 12px',
1823
2152
  borderRadius: 12,
1824
- background: theme.panelBg,
1825
- backdropFilter: theme.panelBackdrop,
1826
- WebkitBackdropFilter: theme.panelBackdrop,
2153
+ ...panelChrome(theme),
1827
2154
  color: theme.panelFg,
1828
- border: `1px solid ${theme.panelBorder}`,
1829
- boxShadow: theme.panelShadow,
1830
2155
  zIndex: 44,
1831
2156
  },
1832
2157
  },
@@ -1917,6 +2242,12 @@ window.__ModuleLoader__.load({
1917
2242
  ),
1918
2243
  );
1919
2244
 
2245
+ // 0.3.4(BUG-006):total 与当前窗口(20/50),用于列表底部与「加载更多」。
2246
+ const docsItems = docs !== null && Array.isArray(docs.items) ? docs.items : [];
2247
+ const docsTotal =
2248
+ docs !== null && typeof docs.total === 'number' && Number.isFinite(docs.total)
2249
+ ? docs.total
2250
+ : docsItems.length;
1920
2251
  const docsPanel =
1921
2252
  docs === null
1922
2253
  ? null
@@ -1937,12 +2268,8 @@ window.__ModuleLoader__.load({
1937
2268
  flexDirection: 'column',
1938
2269
  overflow: 'hidden',
1939
2270
  borderRadius: 12,
1940
- background: theme.panelBg,
1941
- backdropFilter: theme.panelBackdrop,
1942
- WebkitBackdropFilter: theme.panelBackdrop,
2271
+ ...panelChrome(theme),
1943
2272
  color: theme.panelFg,
1944
- border: `1px solid ${theme.panelBorder}`,
1945
- boxShadow: theme.panelShadow,
1946
2273
  zIndex: 43,
1947
2274
  },
1948
2275
  },
@@ -1964,7 +2291,7 @@ window.__ModuleLoader__.load({
1964
2291
  { style: { flex: 1, minWidth: 0 } },
1965
2292
  docs.selected !== undefined && docs.selected !== null
1966
2293
  ? '交接单详情'
1967
- : `历史交接单(${Array.isArray(docs.items) ? docs.items.length : 0})`,
2294
+ : `历史交接单(${docsTotal})`,
1968
2295
  ),
1969
2296
  h('button', { type: 'button', 'data-cr-docs-close': '1', onClick: closeDocs, style: smallBtnStyle }, '关闭'),
1970
2297
  ),
@@ -1981,7 +2308,7 @@ window.__ModuleLoader__.load({
1981
2308
  'button',
1982
2309
  {
1983
2310
  type: 'button',
1984
- onClick: () => setDocs({ items: docs.items, selected: null }),
2311
+ onClick: () => setDocs((prev) => (prev === null ? null : { ...prev, selected: null })),
1985
2312
  style: smallBtnStyle,
1986
2313
  },
1987
2314
  '← 列表',
@@ -2004,21 +2331,28 @@ window.__ModuleLoader__.load({
2004
2331
  ),
2005
2332
  ),
2006
2333
  h(
2007
- 'pre',
2334
+ 'div',
2008
2335
  {
2336
+ // 0.3.4(BUG-005):交接单详情从 <pre> 纯文本改成轻量 markdown 渲染。
2337
+ // 解析器是 core/markdown-lite.js(构建时内联),渲染只产生 React
2338
+ // 文本节点,不生成 HTML、不引第三方依赖。
2009
2339
  'data-cr-doc-content': '1',
2010
2340
  style: {
2011
2341
  margin: 0,
2012
2342
  padding: '8px 12px',
2013
2343
  overflowY: 'auto',
2014
- whiteSpace: 'pre-wrap',
2344
+ overflowX: 'hidden',
2015
2345
  wordBreak: 'break-word',
2016
2346
  fontSize: 11,
2017
2347
  lineHeight: '16px',
2018
2348
  maxHeight: 'min(50vh, 320px)',
2019
2349
  },
2020
2350
  },
2021
- docs.contentLoading === true ? '读取中…' : (docs.contentError ?? docs.content ?? ''),
2351
+ docs.contentLoading === true
2352
+ ? '读取中…'
2353
+ : docs.contentError !== undefined && docs.contentError !== null
2354
+ ? h('div', { style: { color: COLORS.error } }, String(docs.contentError))
2355
+ : renderMarkdownLite(docs.content ?? ''),
2022
2356
  ),
2023
2357
  )
2024
2358
  : h(
@@ -2026,8 +2360,8 @@ window.__ModuleLoader__.load({
2026
2360
  { style: { overflowY: 'auto', padding: 4 } },
2027
2361
  docs.error !== undefined && docs.error !== null
2028
2362
  ? h('div', { style: { padding: '8px 10px', fontSize: 11, color: COLORS.error } }, String(docs.error))
2029
- : Array.isArray(docs.items) && docs.items.length > 0
2030
- ? docs.items.map((entry) =>
2363
+ : docsItems.length > 0
2364
+ ? [...docsItems.map((entry) =>
2031
2365
  h(
2032
2366
  'button',
2033
2367
  {
@@ -2070,7 +2404,38 @@ window.__ModuleLoader__.load({
2070
2404
  `${fmtTime(entry.mtime)} · ${Math.max(1, Math.round((Number(entry.size) || 0) / 1024))}KB · ${entry.name}`,
2071
2405
  ),
2072
2406
  ),
2073
- )
2407
+ ),
2408
+ h(
2409
+ 'div',
2410
+ {
2411
+ key: 'docs-footer',
2412
+ style: {
2413
+ display: 'flex',
2414
+ alignItems: 'center',
2415
+ gap: 8,
2416
+ padding: '6px 10px',
2417
+ borderTop: `1px solid ${theme.panelBorder}`,
2418
+ fontSize: 10,
2419
+ color: theme.subText,
2420
+ },
2421
+ },
2422
+ h('span', { style: { flex: 1, minWidth: 0 } }, `共 ${docsTotal} 份(显示最近 ${docsItems.length})`),
2423
+ docsItems.length < docsTotal && docsItems.length < 50
2424
+ ? h(
2425
+ 'button',
2426
+ {
2427
+ type: 'button',
2428
+ 'data-cr-docs-more': '1',
2429
+ onClick: () => {
2430
+ void loadDocs(50);
2431
+ },
2432
+ style: smallBtnStyle,
2433
+ },
2434
+ '加载更多',
2435
+ )
2436
+ : null,
2437
+ ),
2438
+ ]
2074
2439
  : h(
2075
2440
  'div',
2076
2441
  { style: { padding: '10px 12px', fontSize: 11, color: theme.subText } },
@@ -2106,12 +2471,8 @@ window.__ModuleLoader__.load({
2106
2471
  flexDirection: 'column',
2107
2472
  overflow: 'hidden',
2108
2473
  borderRadius: 12,
2109
- background: theme.panelBg,
2110
- backdropFilter: theme.panelBackdrop,
2111
- WebkitBackdropFilter: theme.panelBackdrop,
2474
+ ...panelChrome(theme),
2112
2475
  color: theme.panelFg,
2113
- border: `1px solid ${theme.panelBorder}`,
2114
- boxShadow: theme.panelShadow,
2115
2476
  zIndex: 43,
2116
2477
  },
2117
2478
  },
@@ -2208,16 +2569,12 @@ window.__ModuleLoader__.load({
2208
2569
  textAlign: 'left',
2209
2570
  padding: '8px 12px',
2210
2571
  borderRadius: 10,
2211
- background: theme.panelBg,
2212
- backdropFilter: theme.panelBackdrop,
2213
- WebkitBackdropFilter: theme.panelBackdrop,
2572
+ ...panelChrome(theme),
2214
2573
  color: theme.panelFg,
2215
- border: `1px solid ${theme.panelBorder}`,
2216
2574
  borderLeft: `3px solid ${/失败|错误|无法|没有可|不满足|拒绝|未知/.test(toastText) ? COLORS.ringAdviseHandoff : '#2fb344'}`,
2217
2575
  fontSize: 12,
2218
2576
  lineHeight: '18px',
2219
2577
  zIndex: 41,
2220
- boxShadow: theme.panelShadow,
2221
2578
  display: 'flex',
2222
2579
  alignItems: 'center',
2223
2580
  gap: 8,
@@ -2232,16 +2589,7 @@ window.__ModuleLoader__.load({
2232
2589
  type: 'button',
2233
2590
  'data-cr-toast-cancel': '1',
2234
2591
  onClick: cancelRun,
2235
- style: {
2236
- flex: 'none',
2237
- border: `1px solid ${theme.panelBorder}`,
2238
- borderRadius: 6,
2239
- padding: '2px 8px',
2240
- fontSize: 11,
2241
- cursor: 'pointer',
2242
- background: 'transparent',
2243
- color: theme.panelFg,
2244
- },
2592
+ style: { ...smallBtnStyle, flex: 'none' },
2245
2593
  },
2246
2594
  '取消',
2247
2595
  )
@@ -2958,16 +3306,7 @@ window.__ModuleLoader__.load({
2958
3306
  // BUG-009(2026-10-20):每个示例环单独构造参数——比例按各自语义取、hint 用 null;
2959
3307
  // 不再共用 minRatio + 0.12 的假断点(那会让 5 个环的断点全挤在同一处、颜色也对不上)。
2960
3308
  const demoRatio = (ratio, hintRatio) => ringSegments(ratio, form.thresholds.compactRatio, form.thresholds.handoffRatio, hintRatio ?? null);
2961
- const point = (value) => {
2962
- const angle = value * Math.PI * 2 - Math.PI / 2;
2963
- return [10 + 8 * Math.cos(angle), 10 + 8 * Math.sin(angle)];
2964
- };
2965
- const arcPath = (from, to) => {
2966
- const [x1, y1] = point(from);
2967
- const [x2, y2] = point(to);
2968
- const large = to - from > 0.5 ? 1 : 0;
2969
- return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A 8 8 0 ${large} 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
2970
- };
3309
+ // 点坐标/弧线 path 是共享几何(15-shared.js 的 ringPoint / ringArcPath)。
2971
3310
  const colorOf = (key) =>
2972
3311
  key === 'stage' ? previewLight.stage : key === 'compact' ? previewLight.compact : key === 'handoff' ? previewLight.handoff : previewLight.idle;
2973
3312
  const demoRing = (ring, options = {}) => {
@@ -2977,18 +3316,7 @@ window.__ModuleLoader__.load({
2977
3316
  // 原生自动压缩线:和指示灯一样用「径向紫色刻度 + 底色描边」;越过线后整环红色闪烁。
2978
3317
  const nativeAt =
2979
3318
  typeof options.nativeAt === 'number' && options.nativeAt > 0.02 && options.nativeAt < 1 ? options.nativeAt : null;
2980
- const pointAt = (value, radius) => {
2981
- const angle = value * Math.PI * 2 - Math.PI / 2;
2982
- return [10 + radius * Math.cos(angle), 10 + radius * Math.sin(angle)];
2983
- };
2984
- const nativeTick =
2985
- nativeAt === null
2986
- ? null
2987
- : (() => {
2988
- const [x1, y1] = pointAt(nativeAt, 5.6);
2989
- const [x2, y2] = pointAt(nativeAt, 9.6);
2990
- return { x1, y1, x2, y2 };
2991
- })();
3319
+ const nativeTick = nativeAt === null ? null : nativeTickAt(nativeAt);
2992
3320
  const nativeCrossed = nativeAt !== null && ring.ratio > nativeAt + 1e-9;
2993
3321
  return h(
2994
3322
  'svg',
@@ -3001,17 +3329,17 @@ window.__ModuleLoader__.load({
3001
3329
  const from = segment.from <= 1e-9 ? segment.from : segment.from + gap;
3002
3330
  const to = Math.abs(segment.to - ring.ratio) < 1e-9 ? segment.to : segment.to - gap;
3003
3331
  if (to - from <= 0.001) return null;
3004
- return h('path', { key: index, d: arcPath(from, to), fill: 'none', stroke: colorOf(segment.color), strokeWidth: 2, strokeLinecap: 'round', 'data-cr-demo': segment.color });
3332
+ return h('path', { key: index, d: ringArcPath(from, to), fill: 'none', stroke: colorOf(segment.color), strokeWidth: 2, strokeLinecap: 'round', 'data-cr-demo': segment.color });
3005
3333
  })
3006
3334
  .filter((node) => node !== null),
3007
3335
  // 断点缺口用「背景色小段」强调一下(教学图里看得清)
3008
3336
  ...gaps.map((at, index) =>
3009
- h('path', { key: `b${index}`, d: arcPath(at - 0.01, at + 0.01), fill: 'none', stroke: 'var(--dsw-alias-bg-base, #fff)', strokeWidth: 3, 'data-cr-demo-gap': String(at) }),
3337
+ h('path', { key: `b${index}`, d: ringArcPath(at - 0.01, at + 0.01), fill: 'none', stroke: 'var(--dsw-alias-bg-base, #fff)', strokeWidth: 3, 'data-cr-demo-gap': String(at) }),
3010
3338
  ),
3011
3339
  // 越过原生压缩线:整环红色闪烁(教学图里也演示出来)。
3012
3340
  ...(nativeCrossed
3013
3341
  ? [
3014
- h('style', { key: 'native-alert-style' }, '@keyframes cr-native-alert { 0%,100% { opacity: 1 } 50% { opacity: 0.15 } } @media (prefers-reduced-motion: reduce) { [data-cr-native-alert] { animation: none !important; opacity: 0.85 !important; } }'),
3342
+ h('style', { key: 'native-alert-style' }, NATIVE_ALERT_CSS),
3015
3343
  h('circle', {
3016
3344
  key: 'native-alert',
3017
3345
  cx: 10,