dsh-contextos-devops 0.0.0-stage → 1.48.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -2
- package/assets/avatar.jpg +0 -0
- package/assets/client.css +3316 -0
- package/assets/icon-180.png +0 -0
- package/build/client.mjs +64 -0
- package/client.js +7622 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +15 -0
- package/index.js +345 -0
- package/lib/base/env.js +22 -0
- package/lib/base/field.js +25 -0
- package/lib/base/http.js +140 -0
- package/lib/base/platform.js +33 -0
- package/lib/base/registry.js +186 -0
- package/lib/base/state.js +66 -0
- package/lib/base/table.js +47 -0
- package/lib/base/texttable.js +26 -0
- package/lib/base/url.js +178 -0
- package/lib/core/README.md +17 -0
- package/lib/core/apps.js +675 -0
- package/lib/core/auth.js +110 -0
- package/lib/core/avatar.js +289 -0
- package/lib/core/backup.js +105 -0
- package/lib/core/codegen.js +401 -0
- package/lib/core/copilot.js +419 -0
- package/lib/core/data.js +157 -0
- package/lib/core/device.js +148 -0
- package/lib/core/files.js +152 -0
- package/lib/core/gobuild.js +63 -0
- package/lib/core/iam.js +122 -0
- package/lib/core/identity.js +22 -0
- package/lib/core/nodes.js +627 -0
- package/lib/core/ops.js +306 -0
- package/lib/core/project.js +393 -0
- package/lib/core/spaces.js +406 -0
- package/lib/core/terminal.js +137 -0
- package/lib/core/terminal2.js +141 -0
- package/lib/core/users.js +51 -0
- package/lib/core/vocab.js +158 -0
- package/lib/misc/async.js +15 -0
- package/lib/misc/shell.js +27 -0
- package/lib/misc/static.js +33 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +91 -4
- package/src/client/README.md +37 -0
- package/src/client/parts/00-prelude.js +315 -0
- package/src/client/parts/05-bootstrap.js +61 -0
- package/src/client/parts/08-store.js +85 -0
- package/src/client/parts/10-ui.js +188 -0
- package/src/client/parts/12-debug.js +361 -0
- package/src/client/parts/15-layout.js +86 -0
- package/src/client/parts/20-devworkspace.js +789 -0
- package/src/client/parts/25-editor-ui.js +28 -0
- package/src/client/parts/30-modals.js +85 -0
- package/src/client/parts/32-cards.js +176 -0
- package/src/client/parts/34-forms.js +176 -0
- package/src/client/parts/36-data.js +267 -0
- package/src/client/parts/40-project.js +430 -0
- package/src/client/parts/45-map.js +109 -0
- package/src/client/parts/46-pm.js +158 -0
- package/src/client/parts/47-three.js +324 -0
- package/src/client/parts/49-msg.js +167 -0
- package/src/client/parts/50-chat.js +1422 -0
- package/src/client/parts/51-pmselection.js +31 -0
- package/src/client/parts/52-pmdata.js +29 -0
- package/src/client/parts/55-pm.js +146 -0
- package/src/client/parts/58-config.js +381 -0
- package/src/client/parts/59-monitor.js +262 -0
- package/src/client/parts/60-page.js +1518 -0
- package/src/client/styles/kit.css +20 -0
- package/src/client/styles/main.css +3294 -0
- package/src/client/styles/tokens.css +2 -0
- package/vendor/leaflet/leaflet.css +661 -0
- package/vendor/leaflet/leaflet.js +6 -0
- package/vendor/three/three.module.js +53044 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +218 -0
- 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
|
+
}
|