dsh-contextos-devops 0.0.0-stage → 1.48.32

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/assets/avatar.jpg +0 -0
  4. package/assets/client.css +3316 -0
  5. package/assets/icon-180.png +0 -0
  6. package/build/client.mjs +64 -0
  7. package/client.js +7622 -0
  8. package/cordis.patch.yml +3 -0
  9. package/icon.svg +15 -0
  10. package/index.js +345 -0
  11. package/lib/base/env.js +22 -0
  12. package/lib/base/field.js +25 -0
  13. package/lib/base/http.js +140 -0
  14. package/lib/base/platform.js +33 -0
  15. package/lib/base/registry.js +186 -0
  16. package/lib/base/state.js +66 -0
  17. package/lib/base/table.js +47 -0
  18. package/lib/base/texttable.js +26 -0
  19. package/lib/base/url.js +178 -0
  20. package/lib/core/README.md +17 -0
  21. package/lib/core/apps.js +675 -0
  22. package/lib/core/auth.js +110 -0
  23. package/lib/core/avatar.js +289 -0
  24. package/lib/core/backup.js +105 -0
  25. package/lib/core/codegen.js +401 -0
  26. package/lib/core/copilot.js +419 -0
  27. package/lib/core/data.js +157 -0
  28. package/lib/core/device.js +148 -0
  29. package/lib/core/files.js +152 -0
  30. package/lib/core/gobuild.js +63 -0
  31. package/lib/core/iam.js +122 -0
  32. package/lib/core/identity.js +22 -0
  33. package/lib/core/nodes.js +627 -0
  34. package/lib/core/ops.js +306 -0
  35. package/lib/core/project.js +393 -0
  36. package/lib/core/spaces.js +406 -0
  37. package/lib/core/terminal.js +137 -0
  38. package/lib/core/terminal2.js +141 -0
  39. package/lib/core/users.js +51 -0
  40. package/lib/core/vocab.js +158 -0
  41. package/lib/misc/async.js +15 -0
  42. package/lib/misc/shell.js +27 -0
  43. package/lib/misc/static.js +33 -0
  44. package/locale/en.json +10 -0
  45. package/locale/zh.json +10 -0
  46. package/package.json +92 -4
  47. package/src/client/README.md +37 -0
  48. package/src/client/parts/00-prelude.js +315 -0
  49. package/src/client/parts/05-bootstrap.js +61 -0
  50. package/src/client/parts/08-store.js +85 -0
  51. package/src/client/parts/10-ui.js +188 -0
  52. package/src/client/parts/12-debug.js +361 -0
  53. package/src/client/parts/15-layout.js +86 -0
  54. package/src/client/parts/20-devworkspace.js +789 -0
  55. package/src/client/parts/25-editor-ui.js +28 -0
  56. package/src/client/parts/30-modals.js +85 -0
  57. package/src/client/parts/32-cards.js +176 -0
  58. package/src/client/parts/34-forms.js +176 -0
  59. package/src/client/parts/36-data.js +267 -0
  60. package/src/client/parts/40-project.js +430 -0
  61. package/src/client/parts/45-map.js +109 -0
  62. package/src/client/parts/46-pm.js +158 -0
  63. package/src/client/parts/47-three.js +324 -0
  64. package/src/client/parts/49-msg.js +167 -0
  65. package/src/client/parts/50-chat.js +1422 -0
  66. package/src/client/parts/51-pmselection.js +31 -0
  67. package/src/client/parts/52-pmdata.js +29 -0
  68. package/src/client/parts/55-pm.js +146 -0
  69. package/src/client/parts/58-config.js +381 -0
  70. package/src/client/parts/59-monitor.js +262 -0
  71. package/src/client/parts/60-page.js +1518 -0
  72. package/src/client/styles/kit.css +20 -0
  73. package/src/client/styles/main.css +3294 -0
  74. package/src/client/styles/tokens.css +2 -0
  75. package/vendor/leaflet/leaflet.css +661 -0
  76. package/vendor/leaflet/leaflet.js +6 -0
  77. package/vendor/three/three.module.js +53044 -0
  78. package/vendor/xterm/addon-fit.js +2 -0
  79. package/vendor/xterm/xterm.css +218 -0
  80. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,188 @@
1
+ // 统一品牌标识(科技风):devops = 六边集群,copilot = 神经 C 核心
2
+ function CtxMark({ variant = 'devops', size = 22 }) {
3
+ if (variant === 'copilot') {
4
+ return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true },
5
+ h('path', { d: 'M17.2 7.1a6.5 6.5 0 1 0 0 9.8', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' }),
6
+ h('circle', { cx: 11.2, cy: 12, r: 1.7, fill: 'currentColor' }),
7
+ h('path', { d: 'M17.6 5.4 21 2.4', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round' }),
8
+ h('path', { d: 'M19.4 9.2h2.9', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round' }),
9
+ h('path', { d: 'M18.9 15.1l2.2 2.2', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round' }));
10
+ }
11
+ const pts = [[12, 2.8], [19.9, 7.4], [19.9, 16.6], [12, 21.2], [4.1, 16.6], [4.1, 7.4]];
12
+ return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true },
13
+ h('path', { d: 'M12 2.8 19.9 7.4v9.2L12 21.2 4.1 16.6V7.4Z', stroke: 'currentColor', strokeWidth: 1.5, strokeLinejoin: 'round' }),
14
+ h('path', { d: 'M12 6.9 16.4 9.5v4.9L12 17 7.6 14.4V9.5Z', stroke: 'currentColor', strokeWidth: 1.1, opacity: '.55', strokeLinejoin: 'round' }),
15
+ h('circle', { cx: 12, cy: 12, r: 1.7, fill: 'currentColor' }),
16
+ ...pts.map(([x, y], i) => h('circle', { key: i, cx: x, cy: y, r: i % 2 ? 1 : 1.2, fill: 'currentColor' })));
17
+ }
18
+
19
+ function Ico({ name, size = 12 }) {
20
+ const d = {
21
+ target: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z M12 11.2a.8.8 0 1 0 0 1.6.8.8 0 0 0 0-1.6Z',
22
+ plan: 'M4 6h3v3H4z M10 7.5h10 M4 15h3v3H4z M10 16.5h10',
23
+ task: 'M4 6h6 M4 12h6 M4 18h6 M13 7l2 2 4-4 M13 15l2 2 4-4',
24
+ report: 'M6 3h8l4 4v14H6z M14 3v4h4 M9 12h6 M9 16h4',
25
+ feedback: 'M4 5h16v10H9l-5 4z M8 9h8 M8 12h5',
26
+ clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z M12 7v5l3 2',
27
+ map: 'M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2Z M9 4v14 M15 6v14',
28
+ }[name];
29
+ return h('span', { className: `cx-ico cx-ico-${name || 'dot'}` },
30
+ h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { display: 'block' } }, h('path', { d: d || '' })));
31
+ }
32
+
33
+ function Icon({ size = 15 }) {
34
+ return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true },
35
+ h('path', { d: 'M12 2 3 7v10l9 5 9-5V7l-9-5Z', stroke: 'currentColor', strokeWidth: 1.5 }),
36
+ h('path', { d: 'M12 12 3 7M12 12l9-5M12 12v10', stroke: 'currentColor', strokeWidth: 1.1, opacity: .7 }),
37
+ h('circle', { cx: 12, cy: 12, r: 1.7, fill: 'currentColor' }));
38
+ }
39
+
40
+ function Panel({ index, icon, title, count, tools, children }) {
41
+ const [zh, en] = String(title || '').split(' / ');
42
+ return h('section', { className: 'cx-panel' },
43
+ h('div', { className: 'cx-ph' }, h(UiAffordance, { icon, zh, en, mode: 'full', iconSize: 18, className: 'cx-panel-title' }), h('span', { className: 'cnt' }, count || '')),
44
+ tools ? h('div', { className: 'cx-tools' }, tools) : null,
45
+ h('div', { className: 'cx-list' }, children));
46
+ }
47
+
48
+ // ==== UI KIT:全局通用组件。新界面只允许组合这里的组件 + cx-* token,禁止再手写 cx-empty/cx-err。 ====
49
+ const TOKENS = { space: [4, 8, 12, 16, 24], radius: [6, 8, 12], font: [10, 11, 12, 13, 15] };
50
+ function Btn({ kind = 'default', size = 'md', icon, children, ...rest }) {
51
+ const cls = `cx-btn${kind !== 'default' ? ` cx-${kind}` : ''}${size === 'sm' ? ' cx-sm' : ''}`;
52
+ return h('button', { type: 'button', className: cls, ...rest }, icon ? h(Ico, { name: icon }) : null, children);
53
+ }
54
+ function Toolbar({ children, className = '' }) { return h('div', { className: `cx-tools ${className}`.trim() }, children); }
55
+ function Section({ title, tools, children }) { return h('div', { className: 'cx-section' }, title ? h('div', { className: 'cx-section-h' }, title) : null, tools ? h(Toolbar, null, tools) : null, children); }
56
+ function Field({ label, children }) { return h('label', { className: 'cx-field' }, h('span', null, label), children); }
57
+ function EmptyState({ text, style }) { return h('div', { className: 'cx-empty', style }, text); }
58
+ function ErrorBox({ text }) { return h('div', { className: 'cx-err' }, text); }
59
+ function Hint({ text, className = '' }) { return h('div', { className: `cx-hint ${className}`.trim() }, text); }
60
+ function Card({ title, children, ...rest }) { return h('div', { className: 'cx-card', ...rest }, title ? h('div', { className: 'cx-card-head' }, title) : null, children); }
61
+ function Tabs({ items, value, onChange }) { return h('div', { className: 'cx-tabs' }, items.map((it) => h('button', { key: it.value, type: 'button', className: `cx-tab${value === it.value ? ' on' : ''}`, onClick: () => onChange(it.value) }, it.label))); }
62
+
63
+ const UID_RE = /^[0-9a-f]{32}$/i;
64
+ const UID_ALIASES = { targetUID: 'target_uid', targetId: 'target_uid', target_uid: 'target_uid', planUID: 'plan_uid', planId: 'plan_uid', plan_uid: 'plan_uid', taskUID: 'task_uid', taskId: 'task_uid', task_uid: 'task_uid', reportUID: 'report_uid', reportId: 'report_uid', report_uid: 'report_uid', feedbackUID: 'feedback_uid', feedbackId: 'feedback_uid', feedback_uid: 'feedback_uid' };
65
+ function uidFieldMap(input = {}) {
66
+ const out = {};
67
+ for (const [key, value] of Object.entries(input || {})) {
68
+ const canonical = UID_ALIASES[key];
69
+ if (canonical && value && UID_RE.test(String(value))) out[canonical] = String(value);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ const UI_ICONS = {
75
+ monitor: ['M2.2 3.2h11.6v7.2H2.2z', 'M5.4 13.2h5.2', 'M8 10.4v2.8'],
76
+ chevronRight: ['M6 4l4 4-4 4'],
77
+ chevronDown: ['M4 6l4 4 4-4'],
78
+ refresh: ['M13.5 8a5.5 5.5 0 1 1-1.7-3.9', 'M13.5 2.8v3.4h-3.4'],
79
+ plus: ['M8 3v10', 'M3 8h10'],
80
+ more: ['M3.5 8h.01', 'M8 8h.01', 'M12.5 8h.01'],
81
+ close: ['M4 4l8 8', 'M12 4l-8 8'],
82
+ chat: ['M3 4.5h10v6H8l-3 2v-2H3z'],
83
+ manage: ['M3 4h10', 'M3 8h10', 'M3 12h10', 'M6 2.8v2.4', 'M10 6.8v2.4', 'M7 10.8v2.4'],
84
+ dev: ['M6 4L3 8l3 4', 'M10 4l3 4-3 4'],
85
+ chevronUp: ['M4 10l4-4 4 4'],
86
+ arrowLeft: ['M13 8H3', 'M7 4L3 8l4 4'],
87
+ arrowRight: ['M3 8h10', 'M9 4l4 4-4 4'],
88
+ fullscreen: ['M6 3H3v3', 'M10 3h3v3', 'M6 13H3v-3', 'M10 13h3v-3'],
89
+ restore: ['M3 6h3V3', 'M13 6h-3V3', 'M3 10h3v3', 'M13 10h-3v3'],
90
+ panelLeft: ['M3 3h10v10H3z', 'M6 3v10'],
91
+ panelRight: ['M3 3h10v10H3z', 'M10 3v10'],
92
+ panelBottom: ['M3 3h10v10H3z', 'M3 10h10'],
93
+ save: ['M8 3v7', 'M5 7l3 3 3-3', 'M3 13h10'],
94
+ copy: ['M6 5h7v8H6z', 'M3 3h7v2', 'M3 3v8h2'],
95
+ check: ['M3 8l3 3 7-7'],
96
+ send: ['M8 13V3', 'M4 7l4-4 4 4'],
97
+ bolt: ['M8 2L4 9h3l-1 5 4-7H7z'],
98
+ external: ['M9 3h4v4', 'M13 3L7 9', 'M5 3H3v10h10v-2'],
99
+ minus: ['M3 8h10'],
100
+ locate: ['M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2z', 'M8 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4z', 'M8 1v2', 'M8 13v2', 'M1 8h2', 'M13 8h2'],
101
+ server: ['M3 3h10v4H3z', 'M3 9h10v4H3z', 'M5 5h.01', 'M5 11h.01'],
102
+ space: ['M3 5h4l1 2h5v6H3z'],
103
+ app: ['M3 3h4v4H3z', 'M9 3h4v4H9z', 'M3 9h4v4H3z', 'M9 9h4v4H9z'],
104
+ web: ['M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2z', 'M2 8h12', 'M8 2c2 2 2 10 0 12', 'M8 2c-2 2-2 10 0 12'],
105
+ avatar: ['M8 3a2.2 2.2 0 1 0 0 4.4A2.2 2.2 0 0 0 8 3z', 'M3.5 13c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4'],
106
+ image: ['M3 4h10v8H3z', 'M5 9l2-2 2 2 2-2 2 2', 'M5 6.5h.01'],
107
+ factory: ['M3 13V7l3 2V7l3 2V5h3v8z', 'M5 13v-2h2v2', 'M9 13v-2h2v2'],
108
+ layers: ['M8 2l5 3-5 3-5-3z', 'M3 8l5 3 5-3', 'M3 11l5 3 5-3'],
109
+ home: ['M3 7l5-4 5 4v6H3z', 'M6.5 13V9h3v4'],
110
+ pulse: ['M2 8.5h3l1.8-4.2 2.4 8 1.8-4.6h2.4'],
111
+ lock: ['M4.6 7.2V5.4a3.4 3.4 0 0 1 6.8 0v1.8', 'M3.4 7.2h9.2v6.2H3.4z', 'M8 9.6v1.8'],
112
+ setting: ['M8 5.6a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8', 'M8 1.6v1.8', 'M8 12.6v1.8', 'M1.6 8h1.8', 'M12.6 8h1.8'],
113
+ terminal: ['M3 4h10v8H3z', 'M5.5 7l2 2-2 2', 'M9 11h2'],
114
+ stop: ['M4.5 4.5h7v7h-7z'],
115
+ trash: ['M4 5h8', 'M6.2 5V3.2h3.6V5', 'M5.2 5l.5 8h4.6l.5-8'],
116
+ };
117
+ function UiIcon({ name, size = 16 }) {
118
+ const paths = UI_ICONS[name] || UI_ICONS.chevronRight;
119
+ return h('svg', { viewBox: '0 0 16 16', width: size, height: size, fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false' },
120
+ paths.map((d, i) => h('path', { key: i, d })));
121
+ }
122
+ function IconButton({ name, title, onClick, className = '', size = 16, disabled = false, ...rest }) {
123
+ return h(UiAffordance, { as: 'button', icon: name, iconSize: size, mode: 'icon', className: `cx-icon ${className}`.trim(), title, disabled, onClick, ...rest });
124
+ }
125
+ function EntityTitle({ image, icon = 'app', type, title, status, statusTone = '', meta, after, onClick, active = false, className = '', children }) {
126
+ const cls = `cx-entity${active ? ' on' : ''}${className ? ' ' + className : ''}`;
127
+ return h('div', { className: cls, onClick, role: onClick ? 'button' : null },
128
+ h('div', { className: 'cx-entity-pic' }, image
129
+ ? h('img', { src: image, alt: title || '', loading: 'lazy', onError: (e) => { e.currentTarget.style.display = 'none'; } })
130
+ : (React.isValidElement(icon) ? h('span', { className: 'cx-entity-ico' }, icon) : h(UiIcon, { name: icon, size: 28 }))),
131
+ h('div', { className: 'cx-entity-copy' },
132
+ h('div', { className: 'cx-entity-head' },
133
+ type ? h('span', { className: 'cx-entity-type' }, type) : null,
134
+ h('span', { className: 'cx-entity-name', title: title || '' }, title || ''),
135
+ after ? h('span', { className: 'cx-entity-time' }, after) : null,
136
+ status ? h('span', { className: `cx-entity-status ${statusTone}` }, status) : null),
137
+ h('div', { className: 'cx-entity-meta', title: typeof meta === 'string' ? meta : '' }, meta || ''),
138
+ children || null));
139
+ }
140
+ function UiAffordance({ icon, zh, en, mode = 'full', active = false, onClick, className = '', title, disabled = false, children, as = 'span', iconSize = 14, ...rest }) {
141
+ const content = [
142
+ icon ? h(UiIcon, { name: icon, size: iconSize }) : null,
143
+ zh ? h('span', { className: 'cx-aff-zh' }, zh) : null,
144
+ en ? h('span', { className: 'cx-aff-en' }, en) : null,
145
+ children || null,
146
+ ];
147
+ const cls = `cx-aff cx-aff--${mode}${active ? ' on' : ''}${onClick ? ' action' : ''}${className ? ' ' + className : ''}`;
148
+ if (onClick || as === 'button') return h('button', { type: 'button', className: cls, title, disabled, onClick, ...rest }, ...content);
149
+ return h('span', { className: cls, title, ...rest }, ...content);
150
+ }
151
+ function StandardSpaces({ value, onChange }) {
152
+ const selected = new Set(value || []);
153
+ const toggle = (key) => {
154
+ if (key === 'community' || key === 'enterprise') return;
155
+ const next = new Set(selected);
156
+ next.has(key) ? next.delete(key) : next.add(key);
157
+ onChange([...next]);
158
+ };
159
+ return h('div', { className: 'cx-space-picker' }, OFFICIAL.map(([key, name, en]) => {
160
+ const forced = key === 'community' || key === 'enterprise';
161
+ const on = forced || selected.has(key);
162
+ return h('label', { key, className: `cx-space-check${on ? ' on' : ''}${forced ? ' forced' : ''}` },
163
+ h('input', { type: 'checkbox', checked: on, disabled: forced, onChange: () => toggle(key) }),
164
+ h('span', { className: 'cx-space-ico', dangerouslySetInnerHTML: spaceIconHtml(name, 14) }),
165
+ h('span', { className: 'cx-space-name' }, T(name)),
166
+ h('em', { className: 'cx-space-en' }, en || ''),
167
+ forced ? h('i', null, T('默认')) : null);
168
+ }));
169
+ }
170
+ function KeyValueTable({ rows, className = '' }) {
171
+ return h('table', { className: `cx-console-table cx-detail-head-table ${className}`.trim() },
172
+ h('thead', null, h('tr', null, h('th', null, 'KEY'), h('th', null, 'VALUE'))),
173
+ h('tbody', null, (rows || []).map(([k, v], i) => h('tr', { key: i }, h('td', null, String(k)), h('td', null, React.isValidElement(v) ? v : String(v ?? ''))))));
174
+ }
175
+ function DataTable({ cols, rows }) {
176
+ return h('div', { className: 'cx-console-detail-table' },
177
+ h('table', { className: 'cx-console-table' },
178
+ h('thead', null, h('tr', null, (cols || []).map((c) => h('th', { key: c.k }, c.label)))),
179
+ h('tbody', null, (rows || []).map((row, i) => h('tr', { key: i }, (cols || []).map((c) => h('td', { key: c.k }, String(row[c.k] ?? ''))))))));
180
+ }
181
+ function DetailTabs({ items, value, onChange, onClose }) {
182
+ return h('div', { className: 'cx-detail-tabs', style: { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 6 } },
183
+ (items || []).map((it) => h('button', { key: it.value, type: 'button', className: `cx-console-tab${value === it.value ? ' on' : ''}`, style: { display: 'inline-flex', width: 'auto', whiteSpace: 'nowrap', flex: '0 0 auto' }, onClick: () => onChange(it.value) }, it.label)),
184
+ onClose ? h(IconButton, { name: 'close', className: 'cx-detail-close', onClick: onClose, title: T('关闭') }) : null);
185
+ }
186
+
187
+
188
+ // 开发模式:VSCode 风格 IDE 的骨架(编辑器、终端、预览后续接入)
@@ -0,0 +1,361 @@
1
+ const debugResponseTable = (row) => {
2
+ let payload = row?.response;
3
+ if (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = null; } }
4
+ const env = payload?.data !== undefined ? payload.data : payload;
5
+ if (!env) return null;
6
+ if (env.columns && Array.isArray(env.rows)) return { cols: env.columns.map((k) => ({ k, label: k })), rows: env.rows.map((v, i) => ({ ...v, _i: i + 1 })) };
7
+ if (Array.isArray(env.append)) {
8
+ const height = env[env.append[0]]?.length || 0;
9
+ const rows = Array.from({ length: height }, (_, i) => Object.fromEntries(env.append.map((k) => [k, env[k]?.[i] ?? ''])));
10
+ return { cols: env.append.map((k) => ({ k, label: k })), rows };
11
+ }
12
+ if (Array.isArray(env)) {
13
+ const keys = [...new Set(env.flatMap((x) => x && typeof x === 'object' ? Object.keys(x) : []))].slice(0, 8);
14
+ return { cols: keys.map((k) => ({ k, label: k })), rows: env.map((x, i) => x && typeof x === 'object' ? { ...x, _i: i + 1 } : { value: String(x), _i: i + 1 }) };
15
+ }
16
+ const result = env && typeof env === 'object' ? env.result : null;
17
+ if (result && typeof result === 'object') {
18
+ if (Array.isArray(result)) {
19
+ const keys = [...new Set(result.flatMap((x) => x && typeof x === 'object' ? Object.keys(x) : []))].slice(0, 8);
20
+ return { cols: keys.map((k) => ({ k, label: k })), rows: result.map((x) => x && typeof x === 'object' ? x : { value: String(x) }) };
21
+ }
22
+ const keys = Object.keys(result).slice(0, 12);
23
+ return { cols: keys.map((k) => ({ k, label: k })), rows: [result] };
24
+ }
25
+ if (env && typeof env === 'object') {
26
+ const lists = Object.entries(env).filter(([, v]) => Array.isArray(v) && v.length && v[0] && typeof v[0] === 'object');
27
+ if (lists.length) {
28
+ const rows = [];
29
+ for (const [kind, list] of lists) for (const item of list) rows.push({ kind, ...item });
30
+ const keys = [...new Set(rows.flatMap((x) => Object.keys(x)).filter((k) => k !== 'kind'))].slice(0, 8);
31
+ return { cols: [{ k: 'kind', label: 'kind' }, ...keys.map((k) => ({ k, label: k }))], rows };
32
+ }
33
+ return { cols: [{ k: 'key', label: 'KEY' }, { k: 'value', label: 'VALUE' }], rows: Object.entries(env).map(([key, value]) => ({ key, value: typeof value === 'object' ? JSON.stringify(value).slice(0, 220) : String(value) })) };
34
+ }
35
+ return { cols: [{ k: 'value', label: 'VALUE' }], rows: [{ value: String(env) }] };
36
+ };
37
+ const debugMetaFields = (row) => {
38
+ let env = row?.response;
39
+ if (typeof env === 'string') { try { env = JSON.parse(env); } catch { env = null; } }
40
+ if (env?.data !== undefined && env.data && typeof env.data === 'object') env = env.data;
41
+ if (!env || typeof env !== 'object') return [];
42
+ const out = [];
43
+ for (const key of ['detail', 'option', 'append', 'result']) {
44
+ if (env[key] === undefined) continue;
45
+ const value = env[key];
46
+ out.push([key.toUpperCase(), Array.isArray(value) ? value.join(' · ') : (typeof value === 'object' ? JSON.stringify(value).slice(0, 260) : String(value))]);
47
+ }
48
+ return out;
49
+ };
50
+ const debugActionOf = (url) => {
51
+ const text = String(url || '');
52
+ const runMarker = text.lastIndexOf('/action/run/');
53
+ const base = runMarker >= 0 ? text.slice(runMarker + 12) : text;
54
+ const marker = base.lastIndexOf('/action/');
55
+ if (marker >= 0) {
56
+ const tail = base.slice(marker + 8).split('?')[0];
57
+ const parts = tail.split('/').filter(Boolean);
58
+ return parts[0] || 'list';
59
+ }
60
+ return 'list';
61
+ };
62
+ const debugCmdOf = (url) => {
63
+ const text = String(url || '');
64
+ const runMarker = text.lastIndexOf('/action/run/');
65
+ let command = '';
66
+ if (runMarker >= 0) command = text.slice(runMarker + 12).split(/[?/]/)[0];
67
+ else {
68
+ const m = text.match(/\/c\/([^/?]+)/);
69
+ command = m ? m[1] : '';
70
+ }
71
+ const parts = command.split('.').filter(Boolean);
72
+ return parts[parts.length - 1] || '—';
73
+ };
74
+ const traceDebugRow = (row) => {
75
+ const text = JSON.stringify(row?.response ?? row?.raw ?? '') + ' ' + String(row?.url || '');
76
+ const uids = [...new Set(Array.from(text.matchAll(/"uid"\s*:\s*"([0-9a-f]{32})"/gi)).map((m) => m[1]))].slice(0, 40);
77
+ try { window.dispatchEvent(new CustomEvent('cx-trace', { detail: { logId: row?.logId || '', uids } })); } catch {}
78
+ };
79
+ const debugParamsOf = (row) => {
80
+ let env = row?.response;
81
+ if (typeof env === 'string') { try { env = JSON.parse(env); } catch { env = null; } }
82
+ const out = [];
83
+ try {
84
+ const u = new URL(String(row?.url || ''));
85
+ for (const [k, v] of u.searchParams.entries()) if (!out.some((x) => x[0] === k)) out.push([k, v]);
86
+ } catch {}
87
+ if (env && typeof env === 'object') {
88
+ const keys = Array.isArray(env.option) ? env.option : [];
89
+ for (const k of keys) out.push([k, Array.isArray(env[k]) ? env[k].join(' · ') : (env[k] == null ? '' : String(env[k]))]);
90
+ }
91
+ return out;
92
+ };
93
+
94
+ function debugRequestUrl(detail) {
95
+ return String(detail?.row?.req || detail?.row?.url || detail?.row?.request?.url || (detail?.fields || []).find(([k]) => k === 'URL')?.[1] || '');
96
+ }
97
+ function debugLogTarget(detail) {
98
+ let node = Number(detail?.node || detail?.row?.node || 0) || 0;
99
+ let space = String(detail?.space || detail?.row?.space || '');
100
+ const raw = debugRequestUrl(detail);
101
+ let parsed = null;
102
+ try { parsed = new URL(raw, typeof location !== 'undefined' ? location.origin : 'http://127.0.0.1'); } catch {}
103
+ if (parsed) {
104
+ if (/^https?:/i.test(raw)) node = Number(parsed.port) || node;
105
+ const m = parsed.pathname.match(/\/s\/([^/]+)/);
106
+ if (m) space = decodeURIComponent(m[1]);
107
+ }
108
+ if (!space) { const m = raw.match(/\/s\/([^/?#]+)/); if (m) space = decodeURIComponent(m[1]); }
109
+ if (!node) { const m = raw.match(/(?:127\.0\.0\.1|localhost):(\d+)/); if (m) node = Number(m[1]); }
110
+ return { node, space, url: raw };
111
+ }
112
+ function colorizeLogLine(line) {
113
+ const text = String(line || '');
114
+ const out = [];
115
+ const re = /(\b[0-9a-f]{8}-[0-9a-f]{4}\b|https?:\/\/\S+|\b(?:warn|error|panic|fatal)\b)/gi;
116
+ let last = 0; let m;
117
+ while ((m = re.exec(text))) {
118
+ if (m.index > last) out.push(text.slice(last, m.index));
119
+ const cls = /^https?:/i.test(m[0]) ? 'link' : /^(warn|error|panic|fatal)$/i.test(m[0]) ? 'mark' : 'id';
120
+ out.push(h('span', { key: `${m.index}-${m[0]}`, className: cls }, m[0]));
121
+ last = m.index + m[0].length;
122
+ }
123
+ if (last < text.length) out.push(text.slice(last));
124
+ return out;
125
+ }
126
+ const ANSI_COLORS = ['#000000','#cc0000','#4e9a06','#c4a000','#3465a4','#75507b','#06989a','#d3d7cf','#555753','#ef2929','#8ae234','#fce94f','#729fcf','#ad7fa8','#34e2e2','#eeeeec'];
127
+ const ANSI_NAMES = { black:'#000000', red:'#cc0000', green:'#4e9a06', yellow:'#c4a000', blue:'#3465a4', magenta:'#75507b', cyan:'#06989a', white:'#d3d7cf', silver:'#c0c0c0', gray:'#888888', grey:'#888888', darkgrey:'#555753', brightred:'#ef2929', brightgreen:'#8ae234', brightyellow:'#fce94f', brightblue:'#729fcf', brightmagenta:'#ad7fa8', brightcyan:'#34e2e2', brightwhite:'#eeeeec' };
128
+ const ansiColor = (name, fallback) => {
129
+ const v = String(name || '').trim().toLowerCase();
130
+ if (/^#[0-9a-f]{3,8}$/.test(v)) return v;
131
+ return ANSI_NAMES[v] || fallback;
132
+ };
133
+ const ansi256 = (n) => {
134
+ n = Number(n) || 0;
135
+ if (n < 16) return ANSI_COLORS[n] || null;
136
+ if (n < 232) {
137
+ const i = n - 16, r = Math.floor(i / 36), g = Math.floor((i % 36) / 6), b = i % 6;
138
+ const c = (x) => x === 0 ? 0 : 55 + x * 40;
139
+ return `rgb(${c(r)},${c(g)},${c(b)})`;
140
+ }
141
+ const v = 8 + (n - 232) * 10;
142
+ return `rgb(${v},${v},${v})`;
143
+ };
144
+ function ansiRuns(text) {
145
+ const src = String(text || '');
146
+ const out = [];
147
+ const state = { fg: null, bg: null, bold: false, dim: false, italic: false, underline: false, inverse: false };
148
+ const push = (value) => {
149
+ if (!value) return;
150
+ let fg = state.fg, bg = state.bg;
151
+ if (state.inverse) { const f = fg; fg = bg; bg = f; }
152
+ out.push({ text: value, style: {
153
+ color: fg || undefined, background: bg || undefined,
154
+ fontWeight: state.bold ? 700 : undefined,
155
+ opacity: state.dim ? 0.65 : undefined,
156
+ fontStyle: state.italic ? 'italic' : undefined,
157
+ textDecoration: state.underline ? 'underline' : undefined,
158
+ } });
159
+ };
160
+ const re = /\x1b\[([0-9;]*)m/g;
161
+ let last = 0, m;
162
+ while ((m = re.exec(src))) {
163
+ push(src.slice(last, m.index));
164
+ const codes = (m[1] || '0').split(';').map((x) => Number(x || 0));
165
+ for (let i = 0; i < codes.length; i++) {
166
+ const c = codes[i];
167
+ if (c === 0) { state.fg = state.bg = null; state.bold = state.dim = state.italic = state.underline = state.inverse = false; }
168
+ else if (c === 1) state.bold = true;
169
+ else if (c === 2) state.dim = true;
170
+ else if (c === 3) state.italic = true;
171
+ else if (c === 4) state.underline = true;
172
+ else if (c === 7) state.inverse = true;
173
+ else if (c === 22) { state.bold = false; state.dim = false; }
174
+ else if (c === 23) state.italic = false;
175
+ else if (c === 24) state.underline = false;
176
+ else if (c === 27) state.inverse = false;
177
+ else if ((c >= 30 && c <= 37) || (c >= 90 && c <= 97)) state.fg = ANSI_COLORS[c <= 37 ? c - 30 : c - 90 + 8];
178
+ else if ((c >= 40 && c <= 47) || (c >= 100 && c <= 107)) state.bg = ANSI_COLORS[c <= 47 ? c - 40 : c - 100 + 8];
179
+ else if (c === 39) state.fg = null;
180
+ else if (c === 49) state.bg = null;
181
+ else if (c === 38 || c === 48) {
182
+ const target = c === 38 ? 'fg' : 'bg';
183
+ if (codes[i + 1] === 5) { state[target] = ansi256(codes[i + 2]); i += 2; }
184
+ else if (codes[i + 1] === 2) { state[target] = `rgb(${codes[i + 2] || 0},${codes[i + 3] || 0},${codes[i + 4] || 0})`; i += 4; }
185
+ }
186
+ }
187
+ last = m.index + m[0].length;
188
+ }
189
+ push(src.slice(last));
190
+ return out;
191
+ }
192
+ function AnsiText({ text }) {
193
+ return h(React.Fragment, null, ansiRuns(text).map((r, i) => h('span', { key: i, style: r.style }, colorizeLogLine(r.text))));
194
+ }
195
+ function terminalTheme(text) {
196
+ const src = String(text || '');
197
+ const bg = src.match(/"sess\.bg":\["([^"]+)"/);
198
+ const fg = src.match(/"sess\.fg":\["([^"]+)"/);
199
+ return { background: ansiColor(bg?.[1], '#020a10'), color: ansiColor(fg?.[1], '#b9d7e8') };
200
+ }
201
+ function TerminalLog({ text }) {
202
+ const lines = String(text || '').split('\n');
203
+ const theme = terminalTheme(text);
204
+ return h('pre', { className: 'cx-term-log', style: theme }, lines.map((line, i) => {
205
+ const low = line.toLowerCase();
206
+ const sev = /panic|fatal|error|✗/.test(low) ? 'err' : /warn/.test(low) ? 'warn' : '';
207
+ return h('div', { key: i, className: `cx-term-line ${sev}` }, h(AnsiText, { text: line }));
208
+ }));
209
+ }
210
+ // Reusable debug inspector. Keep request/response presentation here so
211
+ // page.js only builds the data model and opens/closes the panel.
212
+ /** 调试栏终端:xterm + PTY(bash,家目录),单例复用,避免重复挂载起两个 shell。 */
213
+ const CONSOLE_TERMS = {};
214
+ function consoleTermDo(node, space, kind, slot = '') {
215
+ const key = `${node || ''}:${space || ''}:v2${slot}`;
216
+ const rec = CONSOLE_TERMS[key];
217
+ if (!rec) return;
218
+ if (kind === 'clear') rec.clear && rec.clear();
219
+ if (kind === 'refresh') rec.reset && rec.reset();
220
+ if (kind === 'reconnect') rec.reconnect && rec.reconnect();
221
+ if (kind === 'fit') rec.fitNow && rec.fitNow();
222
+ }
223
+ function ConsoleTerminal({ node, space, slot = '' }) {
224
+ const hostRef = useRef(null);
225
+ useEffect(() => {
226
+ const key = `${node || ''}:${space || ''}:v2${slot}`;
227
+ const host = hostRef.current;
228
+ if (!host) return;
229
+ const existing = CONSOLE_TERMS[key];
230
+ if (existing) {
231
+ try {
232
+ Array.from(host.children).forEach((c) => { if (c !== existing.wrap) c.remove(); });
233
+ if (existing.wrap.parentNode !== host) host.appendChild(existing.wrap);
234
+ existing.fit && existing.fit.fit();
235
+ } catch {}
236
+ return () => { try { if (existing.wrap.parentNode) existing.wrap.parentNode.removeChild(existing.wrap); } catch {} };
237
+ }
238
+ let cancelled = false;
239
+ Array.from(host.children).forEach((c) => c.remove());
240
+ const wrap = document.createElement('div');
241
+ wrap.className = 'cx-console-term-inner';
242
+ host.appendChild(wrap);
243
+ const rec = CONSOLE_TERMS[key] = { wrap, term: null, fit: null, ws: null };
244
+ (async () => {
245
+ const load = async (src) => { const code = await (await fetch(src)).text(); new Function('module', 'exports', 'define', 'require', 'window', 'self', 'globalThis', code)(undefined, undefined, undefined, undefined, window, window, window); };
246
+ if (!window.Terminal) await load('/dsh-contextos-devops/xterm/xterm.js');
247
+ if (!(window.FitAddon && window.FitAddon.FitAddon)) await load('/dsh-contextos-devops/xterm/addon-fit.js').catch(() => {});
248
+ if (cancelled || !window.Terminal) return;
249
+ const lightNow = (document.querySelector('.cx-shell') || {}).dataset?.theme === 'light';
250
+ const termTheme = lightNow
251
+ ? { background: '#ffffff', foreground: '#1d3a4a', cursor: '#0b7f9f', selectionBackground: 'rgba(11,127,159,.22)' }
252
+ : { background: '#04101a', foreground: '#cfeff9', cursor: '#42f5a7' };
253
+ const term = new window.Terminal({ fontSize: 12, fontFamily: 'Menlo, monospace', cursorBlink: true, scrollback: 2000, cols: 100, rows: 20, theme: termTheme });
254
+ // 吞掉 DSR/DA 查询:别让 xterm 自动回 \x1b[1;<col>R,回包会被 shell 回显成数字
255
+ try { term.parser.registerCsiHandler({ final: 'n' }, () => true); term.parser.registerCsiHandler({ final: 'c' }, () => true); } catch {}
256
+ const fit = window.FitAddon && window.FitAddon.FitAddon ? new window.FitAddon.FitAddon() : null;
257
+ if (fit) term.loadAddon(fit);
258
+ term.open(wrap);
259
+ rec.term = term; rec.fit = fit;
260
+ try { fit && fit.fit(); } catch {}
261
+ wrap.addEventListener('mousedown', () => { try { term.focus(); } catch {} });
262
+ try { document.activeElement && document.activeElement !== document.body && document.activeElement.blur(); } catch {}
263
+ try { term.focus(); } catch {}
264
+ let wsHost = location.host, wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
265
+ if (!/^https?:$/.test(location.protocol)) { try { const o = await api('origin'); if (o && o.host) { wsHost = o.host; wsProto = 'ws'; } } catch {} }
266
+ if (cancelled) return;
267
+ let pend = '';
268
+ const feed = (chunk) => {
269
+ let t = pend + String(chunk || ''); pend = '';
270
+ if (/\x1b\[1m\x1b\[7m%[\s\S]*\r \r/.test(t)) t = t.replace(/\x1b\[1m\x1b\[7m%\x1b\[27m\x1b\[1m\x1b\[0m *\r \r/g, '');
271
+ else { const i = t.lastIndexOf('\x1b[1m\x1b[7m%'); if (i >= 0) { pend = t.slice(i); t = t.slice(0, i); } }
272
+ if (t) { try { term.write(t); } catch {} }
273
+ };
274
+ const connect = () => {
275
+ if (cancelled) return;
276
+ let ws;
277
+ try { ws = new WebSocket(`${wsProto}://${wsHost}/dsh-contextos-devops/terminal?shell=bash`); } catch { return; }
278
+ rec.ws = ws;
279
+ ws.onmessage = (e) => { if (ws === rec.ws) { try { feed(e.data); } catch {} } };
280
+ ws.onclose = () => { if (ws !== rec.ws) return; try { term.write('\r\n[closed]\r\n'); } catch {} };
281
+ rec.send = (d) => { const clean = String(d).replace(/\x1b\[[0-9;?]*[Rc]/g, ''); if (clean && ws.readyState === 1) ws.send(clean); };
282
+ };
283
+ connect();
284
+ rec.reconnect = () => { try { rec.ws && rec.ws.close(); } catch {}; connect(); };
285
+ rec.clear = () => { try { term.clear(); } catch {} };
286
+ rec.reset = () => { try { rec.ws && rec.ws.readyState === 1 && rec.ws.send('clear\r'); } catch {} };
287
+ rec.fitNow = () => { try { fit && fit.fit(); } catch {} };
288
+ term.onData((d) => { rec.send && rec.send(d); });
289
+ [200, 600, 1200].forEach((ms) => setTimeout(() => { try { fit && fit.fit(); } catch {} }, ms));
290
+ })().catch(() => {});
291
+ return () => { cancelled = true; try { rec.term && rec.term.blur(); } catch {}; try { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } catch {} };
292
+ }, [node, space, slot]);
293
+ return h('div', { className: 'cx-console-term', ref: hostRef });
294
+ }
295
+
296
+ /** 解析后端日志行:时间 / log.id / order / 模块 / 类型 / 内容 / SQL / 源码文件。 */
297
+ function parseBackendLog(text) {
298
+ const out = [];
299
+ for (const line of String(text || '').split('\n')) {
300
+ const t = line.trim();
301
+ if (!t) continue;
302
+ const m = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+id:\s*(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(t);
303
+ if (!m) continue;
304
+ const time = m[1], id = m[2], order = m[3], module = m[4], type = m[5];
305
+ let rest = m[6];
306
+ const srcM = /\s(\S+\.go:\d+)\s*$/.exec(rest);
307
+ const file = srcM ? srcM[1] : '';
308
+ if (srcM) rest = rest.slice(0, srcM.index);
309
+ const costM = /\s(\d+(?:\.\d+)?(?:ns|us|ms|s))\s/.exec(rest);
310
+ const cost = costM ? costM[1] : '';
311
+ if (costM) rest = rest.slice(0, costM.index) + rest.slice(costM.index + costM[0].length);
312
+ const sqlM = /\b(SELECT|UPDATE|INSERT|DELETE)\b[\s\S]*/i.exec(rest);
313
+ const sql = sqlM ? sqlM[0].replace(/\s+rows;\s*[-\d]+[\s\S]*$/i, '').trim() : '';
314
+ const content = (sqlM ? rest.slice(0, sqlM.index) : rest).replace(/\s+rows;[\s\S]*$/, '').trim();
315
+ out.push({ time, id, order, module, type, content, cost, sql, file });
316
+ }
317
+ return out;
318
+ }
319
+
320
+ function DebugDetail({ detail, onClose }) {
321
+ const [tab, setTab] = React.useState('info');
322
+ const [logText, setLogText] = React.useState('');
323
+ const [logFile, setLogFile] = React.useState('');
324
+ const [logLoading, setLogLoading] = React.useState(false);
325
+ React.useEffect(() => { setTab('info'); setLogText(''); setLogFile(''); setLogLoading(false); }, [detail?.key]);
326
+ const logId = detail?.row?.logId || detail?.responseHeaders?.['log-id'] || detail?.responseHeaders?.['Log-Id'] || (detail?.fields || []).find(([k]) => k === 'LOG.ID')?.[1] || '';
327
+ const loadLog = async () => {
328
+ if (!detail || !logId) return;
329
+ setLogLoading(true);
330
+ try {
331
+ const target = debugLogTarget(detail);
332
+ if (!target.node || !target.space) throw new Error(`无法从 URL 解析节点/空间:${target.url || '(空 URL)'}`);
333
+ const d = await api('pmLog', { node: target.node, space: target.space, log_id: logId });
334
+ setLogText(d?.text || ''); setLogFile(d?.file || '');
335
+ } catch (e) { setLogText(String(e?.message || e)); }
336
+ finally { setLogLoading(false); }
337
+ };
338
+ React.useEffect(() => { if (['backend', 'sql', 'parsedlog'].includes(tab) && !logText && logId) loadLog(); }, [tab, detail?.key]);
339
+ if (!detail) return null;
340
+ const info = h(React.Fragment, null,
341
+ h('div', { className: 'cx-detail-section' }, T('基本信息')),
342
+ h(KeyValueTable, { rows: (detail.fields || []).map(([k, v]) => [k, k === 'LOG.ID' ? h(React.Fragment, null, String(v ?? ''), h('button', { className: 'cx-log-btn', onClick: () => { setTab('backend'); loadLog(); } }, T('日志'))) : String(v ?? '')]) }),
343
+ h('div', { className: 'cx-detail-section' }, T('请求头')),
344
+ h(KeyValueTable, { rows: Object.entries(detail.requestHeaders || {}) }),
345
+ h('div', { className: 'cx-detail-section' }, T('响应头')),
346
+ h(KeyValueTable, { rows: Object.entries(detail.responseHeaders || {}) }),
347
+ logText ? h('div', { className: 'cx-detail-section' }, `${T('后端日志')} ${logFile}`) : null,
348
+ logText ? h(TerminalLog, { text: logText }) : null);
349
+ return h('div', { className: 'cx-console-detail', style: { height: 'auto', alignSelf: 'flex-start', maxHeight: '100%' } },
350
+ h(DetailTabs, {
351
+ items: [{ value: 'info', label: T('请求信息') }, { value: 'raw', label: T('原始数据') }, { value: 'params', label: T('请求参数') }, { value: 'parsed', label: T('解析数据') }, { value: 'sql', label: T('数据读写') }, { value: 'parsedlog', label: T('解析日志') }, { value: 'backend', label: T('后端日志') }],
352
+ value: tab, onChange: setTab, onClose,
353
+ }),
354
+ tab === 'info' ? info : null,
355
+ tab === 'raw' ? h('pre', { className: 'cx-console-detail-raw' }, detail.body || '') : null,
356
+ tab === 'params' ? h(KeyValueTable, { rows: detail.params || [] }) : null,
357
+ tab === 'parsed' ? h(UiTableList, { cols: detail.table?.cols || [], rows: detail.table?.rows?.slice(0, 100) || [], empty: h('div', { className: 'cx-console-empty' }, T('没有可解析的数据')) }) : null,
358
+ tab === 'sql' ? h(UiTableList, { cols: [{ k: 'time', label: T('时间') }, { k: 'id', label: 'log.id' }, { k: 'sql', label: 'SQL' }, { k: 'cost', label: T('耗时') }, { k: 'file', label: T('源码文件') }], rows: parseBackendLog(logText).filter((r) => r.sql), empty: h('div', { className: 'cx-console-empty' }, T('没有可解析的 SQL')) }) : null,
359
+ tab === 'parsedlog' ? h(UiTableList, { cols: [{ k: 'time', label: T('时间') }, { k: 'id', label: 'log.id' }, { k: 'order', label: 'log.order' }, { k: 'module', label: T('模块') }, { k: 'type', label: T('类型') }, { k: 'content', label: T('内容') }, { k: 'file', label: T('源码文件') }], rows: parseBackendLog(logText), empty: h('div', { className: 'cx-console-empty' }, T('没有可解析的日志')) }) : null,
360
+ tab === 'backend' ? h('div', { className: 'cx-backend-log' }, h('div', { className: 'cx-backend-log-h' }, h('span', null, `${T('后端日志')} · ${logFile || logId || '—'}`), h('button', { className: 'cx-btn xs', disabled: logLoading, onClick: loadLog }, logLoading ? T('加载中…') : T('重新加载'))), logText ? h(TerminalLog, { text: logText }) : h('div', { className: 'cx-console-empty' }, logId ? T('点击重新加载') : 'no log.id')) : null);
361
+ }
@@ -0,0 +1,86 @@
1
+ // Design-system layout primitives. Every page is composed from these slots
2
+ // so the chrome, spacing and behavior stay identical across product areas.
3
+ const SingleLineTitle = UiAffordance;
4
+ const DoubleLineTitle = EntityTitle;
5
+
6
+ function UiBar({ kind = 'toolbar', children, className = '', ...rest }) {
7
+ return h('div', { className: `cx-bar cx-bar--${kind}${className ? ' ' + className : ''}`, ...rest }, children);
8
+ }
9
+ function UiWorkspace({ header, tabs, toolbar, left, content, preview, output, right, status, className = '' }) {
10
+ return h('div', { className: `cx-workspace${className ? ' ' + className : ''}` },
11
+ header ? h('div', { className: 'cx-workspace-header' }, header) : null,
12
+ tabs ? h('div', { className: 'cx-workspace-tabs' }, tabs) : null,
13
+ toolbar ? h('div', { className: 'cx-workspace-toolbar' }, toolbar) : null,
14
+ h('div', { className: 'cx-workspace-body' },
15
+ left ? h('aside', { className: 'cx-workspace-left' }, left) : null,
16
+ h('main', { className: 'cx-workspace-main' },
17
+ h('div', { className: 'cx-workspace-canvas' },
18
+ content ? h('section', { className: 'cx-workspace-content' }, content) : null,
19
+ preview ? h('section', { className: 'cx-workspace-preview' }, preview) : null),
20
+ output ? h('section', { className: 'cx-workspace-output' }, output) : null),
21
+ right ? h('aside', { className: 'cx-workspace-right' }, right) : null),
22
+ status ? h('div', { className: 'cx-workspace-status' }, status) : null);
23
+ }
24
+ function UiAddressBar({ segments = [], actions, children, className = '' }) {
25
+ return h('div', { className: `cx-addressbar${className ? ' ' + className : ''}` },
26
+ h('div', { className: 'cx-addressbar-segments' }, segments.map((s, i) => h(React.Fragment, { key: i },
27
+ i ? h(UiIcon, { name: 'chevronRight', size: 12 }) : null,
28
+ h('button', { type: 'button', className: `cx-addressbar-seg${s.active ? ' on' : ''}`, title: s.title || s.label, onClick: s.onClick },
29
+ s.icon ? h(UiIcon, { name: s.icon, size: 13 }) : null,
30
+ h('span', null, s.label || ''))))),
31
+ children ? h('div', { className: 'cx-addressbar-input' }, children) : null,
32
+ actions ? h('div', { className: 'cx-addressbar-actions' }, actions) : null);
33
+ }
34
+ // 全局统一下拉列表:三列(图标/文字/选中),可滚动时上下显示箭头
35
+ function MenuList({ items, onPick }) {
36
+ const ref = useRef(null);
37
+ const [hint, setHint] = useState({ top: false, bottom: false });
38
+ const update = () => {
39
+ const el = ref.current;
40
+ if (!el) return;
41
+ const top = el.scrollTop > 2;
42
+ const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 2;
43
+ setHint((h) => (h.top === top && h.bottom === bottom ? h : { top, bottom }));
44
+ };
45
+ useEffect(() => {
46
+ update();
47
+ const el = ref.current;
48
+ if (!el) return;
49
+ el.addEventListener('scroll', update, { passive: true });
50
+ const ro = new ResizeObserver(update); ro.observe(el);
51
+ return () => { el.removeEventListener('scroll', update); ro.disconnect(); };
52
+ }, [items]);
53
+ const list = items || [];
54
+ return h('div', { className: 'cx-menu-list' },
55
+ hint.top ? h('div', { className: 'cx-menu-hint top', 'aria-hidden': 'true' }, '▲') : null,
56
+ h('div', { className: 'cx-menu-scroll', ref, onScroll: update },
57
+ list.length ? list.map((it, i) => (it.sep ? h('div', { key: `sep${i}`, className: 'cx-menu-sep' })
58
+ : h('button', {
59
+ key: i, type: 'button',
60
+ className: `cx-menu-item cx-menu-3col${it.on ? ' on' : ''}${it.danger ? ' danger' : ''}`,
61
+ onClick: () => { try { it.go(); } catch {} onPick && onPick(it); },
62
+ },
63
+ h(UiIcon, { name: it.icon || 'dot', size: 14 }),
64
+ h('span', { className: 'cx-menu-lbl' }, it.label),
65
+ h('span', { className: 'cx-menu-chk' },
66
+ it.on ? h(UiIcon, { name: 'check', size: 12 })
67
+ : it.open ? h('i', { className: 'cx-menu-dot open', title: '已打开' })
68
+ : it.openable ? h('i', { className: 'cx-menu-dot' }) : ''))))
69
+ : h('div', { className: 'cx-menu-item empty' }, '—')),
70
+ hint.bottom ? h('div', { className: 'cx-menu-hint bottom', 'aria-hidden': 'true' }, '▼') : null);
71
+ }
72
+
73
+ function UiCardList({ items, render, empty, className = '' }) {
74
+ const list = items || [];
75
+ if (!list.length) return empty || null;
76
+ return h('div', { className: `cx-card-list${className ? ' ' + className : ''}` }, list.map((item, i) => render(item, i)));
77
+ }
78
+ function UiTableList({ cols, rows, empty, className = '' }) {
79
+ if (!rows || !rows.length) return empty || null;
80
+ return h('div', { className: `cx-table-list${className ? ' ' + className : ''}` }, h(DataTable, { cols, rows }));
81
+ }
82
+ function UiTreeList({ children, empty, className = '' }) {
83
+ const has = Array.isArray(children) ? children.length > 0 : !!children;
84
+ if (!has) return empty || null;
85
+ return h('div', { className: `cx-tree-list${className ? ' ' + className : ''}`, role: 'tree' }, children);
86
+ }