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,315 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-contextos-devops',
3
+ factory(require) {
4
+ const React = require('react');
5
+ const h = React.createElement;
6
+ // 页面所在 host:cookie 只对同一 host 生效,localhost 与 127.0.0.1 不能混用。
7
+ // 网页 URL(只给浏览器打开):/s/<pod>/c/<portal>?debug=true&<place>_uid=<实例>#<实例>:<cmd>:<记录>
8
+ // 接口 URL 在宿主 lib/base/url.js;两者形状不同,见 docs/URL-SPEC.md
9
+ const cxPlaceKey = (place) => `${String(place || 'system').replace(/_(uid|name)$/, '') || 'system'}_uid`;
10
+ const cxWebUrl = ({ node, pod, command, place = 'system', placeUid = '', recordUid = '', uid = '', host = '' }) => {
11
+ if (!node || !pod || !command) return '';
12
+ const portal = /^web\./.test(String(command)) && String(command).split('.').length >= 3
13
+ ? String(command).split('.').slice(0, -1).join('.') + '.portal'
14
+ : String(command);
15
+ const q = new URLSearchParams({ debug: 'true', [cxPlaceKey(place)]: placeUid || '' });
16
+ if (uid) q.set('user.uid', uid);
17
+ const hash = recordUid ? `#${placeUid || ''}:${command}:${recordUid}` : '';
18
+ return `http://${host || CX_HOST}:${node}/s/${encodeURIComponent(pod)}/c/${portal}?${q}${hash}`;
19
+ };
20
+ const CX_HOST = (typeof location !== 'undefined' && /^(localhost|127\.0\.0\.1)$/.test(location.hostname)) ? location.hostname : '127.0.0.1';
21
+ const { useState, useEffect, useCallback, useRef } = React;
22
+ let CX_LAYOUT = null;
23
+ const API = '/dsh-contextos-devops/api';
24
+ const CLIENT_CSS = '/dsh-contextos-devops/assets/client.css?v=__CLIENT_CSS_VERSION__';
25
+ const CLIENT_BUILD = '__CLIENT_BUILD__';
26
+ // 界面版本徽标:让「你看到的到底是哪一版」不再靠猜
27
+ function cxClientTag() {
28
+ try {
29
+ const v = String(CLIENT_CSS).split('?v=')[1] || '';
30
+ return v ? 'v' + v : '';
31
+ } catch { return ''; }
32
+ }
33
+ const CRITICAL_CSS = '.cx-shell{height:100%;min-height:0;display:flex;flex-direction:column;background:#050a12;color:#d9f4ff;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}';
34
+ // 样式随版本走:旧 link(上次插件实例注入的 v=旧版本)必须被改写,
35
+ // 否则会出现「新 JS + 旧 CSS」——布局按旧规则算,看起来像回归。
36
+ if (typeof document !== 'undefined') {
37
+ const sel = 'link[data-cx-client-css]';
38
+ let link = document.querySelector(sel);
39
+ if (!link) {
40
+ link = document.createElement('link');
41
+ link.rel = 'stylesheet';
42
+ link.id = 'cx-client-css';
43
+ link.dataset.cxClientCss = '1';
44
+ link.href = CLIENT_CSS;
45
+ document.head.appendChild(link);
46
+ activity('加载插件样式 ' + CLIENT_CSS.split('?')[0], 'info');
47
+ } else if (link.getAttribute('href') !== CLIENT_CSS) {
48
+ const from = String(link.getAttribute('href') || '');
49
+ link.setAttribute('href', CLIENT_CSS);
50
+ activity('刷新插件样式 ' + from.split('?v=')[1] + ' → ' + CLIENT_CSS.split('?v=')[1], 'info');
51
+ }
52
+ }
53
+ // ACTIVITY 通道:模块级埋点,Page 挂载后把缓冲刷进 ACTIVITY 面板。
54
+ const ACTIVITY = { fn: null, buf: [] };
55
+ function activity(m, k = '') { try { if (ACTIVITY.fn) ACTIVITY.fn(m, k); else ACTIVITY.buf.push({ m, k }); } catch {} }
56
+ // 统一最大化:唯一实现。按插件壳矩形写内联 !important,压过所有 CSS 规则;
57
+ // 同时把祖先的 transform 置空(transform 会改变 fixed/absolute 的包含块)。
58
+ function cxMaxApply(el, opts = {}) {
59
+ if (!el) return () => {};
60
+ const saved = [];
61
+ const set = (k, v) => { saved.push([k, el.style.getPropertyValue(k), el.style.getPropertyPriority(k)]); el.style.setProperty(k, v, 'important'); };
62
+ const shell = document.querySelector('.cx-shell') || document.body;
63
+ const r = shell.getBoundingClientRect();
64
+ // 与「网页最大化」对齐:从顶部导航条(.cx-pipe)下沿开始,保留标题栏 + 导航条
65
+ const nav = document.querySelector('.cx-pipe');
66
+ const navRect = nav ? nav.getBoundingClientRect() : null;
67
+ const top = navRect && navRect.bottom > r.top ? navRect.bottom : r.top + (opts.offsetTop != null ? opts.offsetTop : 44);
68
+ set('position', 'fixed');
69
+ set('left', r.left + 'px');
70
+ set('top', top + 'px');
71
+ set('width', r.width + 'px');
72
+ set('height', Math.max(120, r.height - (top - r.top)) + 'px');
73
+ set('right', 'auto');
74
+ set('bottom', 'auto');
75
+ set('z-index', '950');
76
+ set('margin', '0px');
77
+ set('padding', '8px');
78
+ set('box-sizing', 'border-box');
79
+ set('overflow', 'auto');
80
+ set('border', '0px');
81
+ set('border-radius', '0px');
82
+ set('background', opts.bg || 'var(--cx-surface-0)');
83
+ const anc = [];
84
+ for (let n = el.parentElement; n && n !== document.body; n = n.parentElement) {
85
+ const cs = getComputedStyle(n);
86
+ if (cs.transform !== 'none') { anc.push([n, n.style.transform]); n.style.setProperty('transform', 'none', 'important'); }
87
+ }
88
+ return () => {
89
+ for (const [k, v, pr] of saved) { if (v) el.style.setProperty(k, v, pr); else el.style.removeProperty(k); }
90
+ anc.forEach(([n, t]) => { n.style.transform = t; });
91
+ };
92
+ }
93
+
94
+ // 控件定位:被点击元素分配稳定 data-cx-id,调试栏点条目前往对应控件。
95
+ const LOCATE = { last: null };
96
+ function locatable(el) {
97
+ if (!el || el.nodeType !== 1) return '';
98
+ let n = el.closest ? el.closest('[data-cx-id]') : null;
99
+ if (!n) n = el;
100
+ if (!n.dataset) return '';
101
+ if (!n.dataset.cxId) n.dataset.cxId = 'cx-' + Math.random().toString(36).slice(2, 9);
102
+ return n.dataset.cxId;
103
+ }
104
+ // ── 图标(取自 usr/macos-app/Resources/editor.html,同一套 SVG) ──────
105
+ const ICON_PAGE = "<path d='M3.3 1.7h5.5l4 3.9v8.7H3.3z'/><path d='M8.8 1.7v3.9h4'/>";
106
+ const ICON_DIR = "<path d='M2.4 4.7a1 1 0 0 1 1-1h3.05l1.4 1.75h5.75a1 1 0 0 1 1 1v5.75a1 1 0 0 1-1 1H3.4a1 1 0 0 1-1-1z'/>";
107
+ const ICON_DIRO = "<path d='M2.4 12.1V4.7a1 1 0 0 1 1-1h3.05l1.4 1.75h5.8a1 1 0 0 1 1 1v1.1'/><path d='M2.4 12.1l1.45-4.1a1 1 0 0 1 .95-.67h9.3a.9.9 0 0 1 .85 1.18l-1.35 3.85a1 1 0 0 1-.94.66H3.33a.93.93 0 0 1-.93-.92z'/>";
108
+ const ICON_NODE = "<rect x='2.5' y='2.8' width='11' height='4.35' rx='1.1'/><rect x='2.5' y='8.85' width='11' height='4.35' rx='1.1'/><path d='M5.1 4.97h2.2'/><path d='M5.1 11.02h2.2'/><circle cx='11.35' cy='4.97' r='.72' fill='currentColor' stroke='none'/><circle cx='11.35' cy='11.02' r='.72' fill='currentColor' stroke='none'/>";
109
+ const ICON_NODEO = "<rect x='2.5' y='2.8' width='11' height='4.35' rx='1.1'/><rect x='2.5' y='8.85' width='11' height='4.35' rx='1.1'/><path d='M5.1 4.97h2.2'/><path d='M5.1 11.02h2.2'/><circle cx='11.35' cy='4.97' r='.72'/><circle cx='11.35' cy='11.02' r='.72'/>";
110
+ const ICON_SPACE = "<path d='M8 2.35l5.1 2.85v5.6L8 13.65l-5.1-2.85v-5.6z'/><path d='M2.9 5.2L8 8.03l5.1-2.83'/><path d='M8 8.03v5.62'/>";
111
+ const ICON_MARKS = {
112
+ sh: "<path d='M5.7 8.1l1.5 1.3-1.5 1.3'/><path d='M8.6 10.7h2.4'/>",
113
+ go: "<circle cx='6.9' cy='9.3' r='.78' fill='currentColor' stroke='none'/><circle cx='9.1' cy='9.3' r='.78' fill='currentColor' stroke='none'/><path d='M6.8 10.9a1.75 1.75 0 0 0 2.4 0'/>",
114
+ json: "<path d='M7.4 6.9c-.95 0-.95.8-.95 1.3 0 .5-.2.9-.75 1.05.55.15.75.55.75 1.05 0 .5 0 1.3.95 1.3'/><path d='M8.6 6.9c.95 0 .95.8.95 1.3 0 .5.2.9.75 1.05-.55.15-.75.55-.75 1.05 0 .5 0 1.3-.95 1.3'/>",
115
+ js: "<path d='M6.9 6.9c-1.15.7-1.15 3.5 0 4.2'/><path d='M9.1 6.9c1.15.7 1.15 3.5 0 4.2'/><circle cx='8' cy='9' r='.62' fill='currentColor' stroke='none'/>",
116
+ css: "<path d='M7.1 6.9L6.6 11.5'/><path d='M9.6 6.9L9.1 11.5'/><path d='M5.9 8.6h5.2'/><path d='M5.55 10.2h5.2'/>",
117
+ html: "<path d='M6.3 7.3L4.9 9.2l1.4 1.9'/><path d='M9.7 7.3l1.4 1.9-1.4 1.9'/><path d='M8.75 6.6l-1.5 5.1'/>",
118
+ shy: "<path d='M8 6.4l2.25 2.65L8 11.7 5.75 9.05z'/>",
119
+ md: "<path d='M6.2 7.1v4.4'/><path d='M9.8 7.1v4.4'/><path d='M8 6.8v4.7'/><path d='M6.95 10.25L8 11.35l1.05-1.1'/>",
120
+ mod: "<path d='M6.2 7.1v4.4'/><path d='M9.8 7.1v4.4'/><path d='M8 6.8v4.7'/><path d='M6.95 10.25L8 11.35l1.05-1.1'/>",
121
+ sum: "<path d='M6.2 7.1v4.4'/><path d='M9.8 7.1v4.4'/><path d='M8 6.8v4.7'/><path d='M6.95 10.25L8 11.35l1.05-1.1'/>",
122
+ lic: "<path d='M6.2 6.9h3.6'/><path d='M6.2 8.4h3.6'/><path d='M6.2 9.9h2.4'/>",
123
+ mk: "<path d='M6.2 6.9h3.6'/><path d='M6.2 8.4h3.6'/><path d='M6.2 9.9h2.4'/>",
124
+ img: "<circle cx='6.4' cy='8.3' r='.85' fill='currentColor' stroke='none'/><path d='M4.9 11.4l2.2-2.4 1.8 1.9 1.3-1.4 1.4 1.9z'/>",
125
+ log: "<path d='M6.2 6.9h3.6'/><path d='M6.2 8.4h3.6'/><path d='M6.2 9.9h2.4'/>",
126
+ };
127
+ const EXTC = { go: '#1ed6f2', js: '#ffc93c', mjs: '#ffc93c', cjs: '#ffc93c', ts: '#3d8bff', tsx: '#3d8bff', json: '#ffa412', md: '#b45cff', markdown: '#b45cff', html: '#ff4d4d', htm: '#ff4d4d', css: '#2ba7ff', scss: '#ff5cb0', sh: '#1ed760', bash: '#1ed760', zsh: '#1ed760', py: '#45e87d', rb: '#ff6b83', java: '#ff8a1f', c: '#5aa8ff', h: '#5aa8ff', cpp: '#5aa8ff', cc: '#5aa8ff', hpp: '#5aa8ff', m: '#9fb2d0', mm: '#9fb2d0', rs: '#ff8a1f', php: '#a985ff', swift: '#ff9b2e', sql: '#ff5cb0', yaml: '#f06bff', yml: '#f06bff', xml: '#ff9b2e', toml: '#ffc31f', ini: '#ffc31f', conf: '#ffc31f', svg: '#ffd614', plist: '#9fb2d0', shy: '#b45cff', mod: '#12a8f0', sum: '#8fa1c4', log: '#9fb2d0', less: '#2ba7ff', png: '#5eead4', apng: '#5eead4', jpg: '#5eead4', jpeg: '#5eead4', jpe: '#5eead4', gif: '#5eead4', webp: '#5eead4', bmp: '#5eead4', ico: '#5eead4', avif: '#5eead4', heic: '#5eead4', heif: '#5eead4', tif: '#5eead4', tiff: '#5eead4' };
128
+ const iconKey = (name) => {
129
+ const low = String(name || '').toLowerCase();
130
+ if (low === 'makefile' || low === 'gnumakefile' || low === 'makefile.am' || low === 'makefile.in') return 'mk';
131
+ if (low === 'license' || low === 'licence' || low === 'copying' || low === 'notice') return 'lic';
132
+ const e = low.indexOf('.') >= 0 ? low.split('.').pop() : '';
133
+ const map = { sh: 'sh', bash: 'sh', zsh: 'sh', go: 'go', json: 'json', js: 'js', mjs: 'js', cjs: 'js', ts: 'js', tsx: 'js', css: 'css', scss: 'css', less: 'css', html: 'html', htm: 'html', shy: 'shy', md: 'md', markdown: 'md', mod: 'mod', sum: 'sum', log: 'log', png: 'img', apng: 'img', jpg: 'img', jpeg: 'img', jpe: 'img', gif: 'img', webp: 'img', bmp: 'img', ico: 'img', avif: 'img', heic: 'img', heif: 'img', tif: 'img', tiff: 'img' };
134
+ return map[e] || '';
135
+ };
136
+ const svgIcon = (inner, color, size) => {
137
+ const px = size || 14;
138
+ return { __html: `<svg width="${px}" height="${px}" viewBox="0 0 16 16" fill="none" stroke="${color || 'currentColor'}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>` };
139
+ };
140
+ const fileIconHtml = (name, size) => svgIcon(ICON_PAGE + (ICON_MARKS[iconKey(name)] || ''), EXTC[(String(name).split('.').pop() || '').toLowerCase()] || 'var(--cx-text-dim)', size);
141
+ const dirIconHtml = (open, size) => svgIcon(open ? ICON_DIRO : ICON_DIR, '#7b8cff', size);
142
+ const nodeIconHtml = (running, size) => svgIcon(running ? ICON_NODE : ICON_NODEO, running ? '#b06bff' : '#6b7391', size);
143
+ const spaceIconHtml = (size) => svgIcon(ICON_SPACE, '#7c8cff', size);
144
+ const chevHtml = (open, size) => ({ __html: `<svg width="${size || 10}" height="${size || 10}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="transform:rotate(${open ? 90 : 0}deg);transition:transform .12s">${'<path d="M5.5 3L10.5 8l-5 5"/>'}</svg>` });
145
+
146
+ // store-migration:旧键前缀链式迁移,需要枚举全部键,属唯一例外(一次性)
147
+ // contextos-devops: → contextos-devops-dsh: → dsh-contextos-devops:(插件改名的数据兼容)
148
+ try {
149
+ const LEGACY = ['contextos-devops:', 'contextos-devops-dsh:'];
150
+ const NEW = 'dsh-contextos-devops:';
151
+ const keys = [];
152
+ for (let i = 0; i < localStorage.length; i++) {
153
+ const k = localStorage.key(i);
154
+ if (k && LEGACY.some((p) => k.startsWith(p))) keys.push(k);
155
+ }
156
+ for (const k of keys) {
157
+ const p = LEGACY.find((x) => k.startsWith(x));
158
+ const nk = NEW + k.slice(p.length);
159
+ if (localStorage.getItem(nk) == null) localStorage.setItem(nk, localStorage.getItem(k));
160
+ }
161
+ } catch {}
162
+ let OPEN_LINK = null;
163
+ const WEBTABS_KEY = 'dsh-contextos-devops:webtabs';
164
+
165
+ /* ── 统一会话快照 ────────────────────────────────────────────────
166
+ 启动时一次性恢复「当前节点 / 空间 / 应用实例 / 工作模式 / 视图 /
167
+ 网页标签」,直接进入上次的界面,不再逐项读取、不再闪回默认界面。 */
168
+ const SESSION_KEY = 'dsh-contextos-devops:session';
169
+ let cxSessionMem = null, cxSessionTimer = 0;
170
+ const cxSessionGet = () => {
171
+ if (cxSessionMem) return cxSessionMem;
172
+ const s = cxRead(SESSION_KEY, null); cxSessionMem = s && s.v === 2 ? s : null;
173
+ return cxSessionMem;
174
+ };
175
+ const cxSessionFlush = () => { if (cxSessionMem) cxWrite(SESSION_KEY, cxSessionMem); };
176
+ const cxSessionSave = (patch) => {
177
+ const next = { ...(cxSessionGet() || {}), ...(patch || {}), v: 2, updatedAt: Date.now() };
178
+ cxSessionMem = next;
179
+ if (cxSessionTimer) return;
180
+ cxSessionTimer = setTimeout(() => { cxSessionTimer = 0; cxSessionFlush(); }, 400);
181
+ };
182
+ if (typeof window !== 'undefined') window.addEventListener('pagehide', cxSessionFlush);
183
+ const NS = 'contextosDevops';
184
+ const OFFICIAL = [['community','云社区','20240724-community','20240724'],['enterprise','云办公','20240724-enterprise','20240724'],['operation','云研发','20240903-operation','20240903'],['knowledge','云知识','20250520-knowledge','20250520'],['education','云教育','20240724-education','20240724'],['healthy','云健康','20250520-healthy','20250520'],['tourism','云文旅','20250520-tourism','20250520'],['charity','云公益','20250520-charity','20250520'],['industry','云工业','20250520-industry','20250520'],['agriculture','云农业','20250520-agriculture','20250520']];
185
+ const OFFICIAL_KEY = Object.fromEntries(OFFICIAL.flatMap(([k, n]) => [[k, k], [n, k]]));
186
+ const PRODUCT_NOTES = {
187
+ avatar: { tag: '身份层', title: '数字分身', text: '只做两件事:用当前分身(user.uid / 城市 / 公司)创建应用实例、添加数据。不部署节点、不建空间、不生成代码。' },
188
+ factory: { tag: '生产层', title: '软件工厂', text: '持续生产:部署服务节点、创建项目空间、生成应用模板与代码、编译上线、实例化。把从机器到应用的整条生产线跑起来。' },
189
+ iot: { tag: '世界层', title: '赛博世界', text: '发现周边、连接万物:蓝牙扫描附近设备、WiFi 扫描附近网络与同网设备,结合场景与背景,把数字分身放进真实环境。' },
190
+ };
191
+ const INIT_STEPS = [
192
+ { id: 'node', label: '部署服务节点' },
193
+ { id: 'spaces', label: '部署项目空间(云社区 / 云办公)' },
194
+ { id: 'avatar', label: '创建数字分身' },
195
+ { id: 'scene', label: '创建赛博世界' },
196
+ { id: 'project', label: '创建项目管理' },
197
+ { id: 'chat', label: '进入对话模式' },
198
+ ];
199
+ const TERMS = {
200
+ zh: {
201
+ cluster: '集群化', modules: '模块化', automation: '自动化',
202
+ serve: '服务节点', space: '项目空间', recent: '最近应用', web: '网页界面',
203
+ avatar: '数字分身', mode: '工作模式', view: '视图模式', factory: '软件工厂', iot: '赛博世界', scene: '赛博世界',
204
+ create: '创建', refresh: '刷新', maximize: '最大化', restore: '恢复', close: '关闭', initialize: '初始化',
205
+ deploy: '部署', devices: '设备', scan: '扫描', store: '应用商店', template: '模板',
206
+ target: '目标', plan: '计划', task: '任务', report: '报告', feedback: '反馈',
207
+ status: '状态', type: '类型', all: '全部', stopped: '已停止', missing: '缺失', starting: '启动中', search: '搜索', online: '上架', offline: '下架', code: '编程', config: '配置', data: '数据',
208
+ system: '跟随系统', light: '浅色', dark: '深色', chinese: '中文', english: 'English',
209
+ '目标描述': '目标描述', '计划描述': '计划描述', '探测端口': '探测端口', '探测中…': '探测中…', '重试当前步骤': '重试当前步骤', '执行过程': '执行过程', '时间': '时间', '耗时': '耗时', '操作': '操作', '影响 / 接口': '影响 / 接口', idle: '空闲', workspace: '项目空间', applications: '应用', running: '运行中', offline: '离线', chat: '对话', manage: '管理', dev: '开发',
210
+ },
211
+ en: {
212
+ cluster: 'CLUSTER', modules: 'MODULES', automation: 'AUTOMATION',
213
+ serve: 'SERVE', space: 'SPACE', recent: 'RECENT', web: 'WEB',
214
+ avatar: 'DIGITAL AVATAR', mode: 'MODE', view: 'VIEW', factory: 'SOFTWARE FACTORY', iot: 'CYBER WORLD', scene: 'CYBER WORLD',
215
+ create: 'Create', refresh: 'Refresh', maximize: 'Maximize', restore: 'Restore', close: 'Close', initialize: 'INIT',
216
+ deploy: 'Deploy', devices: 'Devices', scan: 'Scan', store: 'Store', template: 'Template',
217
+ target: 'Targets', plan: 'Plans', task: 'Tasks', report: 'Reports', feedback: 'Feedback',
218
+ status: 'Status', type: 'Type', all: 'All', stopped: 'Stopped', missing: 'Missing', starting: 'Starting', search: 'Search', online: 'Online', offline: 'Offline', code: 'Code', config: 'Config', data: 'Data',
219
+ system: 'System', light: 'Light', dark: 'Dark', chinese: '中文', english: 'English',
220
+ '目标描述': 'Goal description', '计划描述': 'Plan description', '创建目标': 'Create target', '先选择一个计划': 'Select a plan first', '先选择一个任务': 'Select a task first', '先选择一个报告': 'Select a report first', '该计划下还没有任务': 'No tasks under this plan', '该任务下还没有报告': 'No reports under this task', '该报告下还没有反馈': 'No feedback yet', '该目标下还没有计划': 'No plans under this target', '选择目标后显示计划': 'Select a target to see plans', '还没有目标': 'No targets yet', '请先初始化项目管理': 'Initialize project management first', '清除筛选': 'Clear filters', '清除': 'Clear', '状态': 'Status', '待开始': 'Not started', '进行中': 'In progress', '已完成': 'Finished', '已取消': 'Canceled', '已拒绝': 'Rejected', '已提交': 'Submitted', '已通过': 'Approved', '已创建': 'Created', '可完成': 'Can finish', '刷新': 'Refresh', '清屏': 'Clear', '重连': 'Reconnect', '分屏': 'Split', '待改进': 'Needs improvement', '日报': 'Daily', '周报': 'Weekly', '月报': 'Monthly', '深夜': 'Night', '深蓝': 'Deep blue', '浅色': 'Light', '柔和': 'Soft', 'CS 风': 'CS', '红警风': 'Red Alert', '魔兽风': 'Warcraft', '星际风': 'StarCraft', '8-bit': '8-bit', '静音': 'Mute', '动效·强': 'Motion · High', '动效·中': 'Motion · Mid', '动效·弱': 'Motion · Low', '动效·微': 'Motion · Micro', '动效·关': 'Motion · Off', '青': 'Cyan', '绿': 'Green', '紫': 'Violet', '琥珀': 'Amber', '玫红': 'Rose', '冰蓝': 'Ice', '暖橙': 'Warm', '科技风': 'Tech', '古典风': 'Classic', '沙漠风': 'Desert', '雨林风': 'Rainforest', '赛博风': 'Cyber', '蒸汽风': 'Steam', '极简风': 'Minimal', '创建计划': 'Create plan', '创建任务': 'Create task', '创建报告': 'Create report', '创建反馈': 'Create feedback', '正在初始化': 'Initializing', '初始化完成': 'Initialized', '初始化失败': 'Initialization failed', '失败': 'Failed', '完成': 'Done', '执行中': 'Running', '执行过程': 'Execution log', '标准空间': 'Standard spaces', '默认': 'Default', '探测端口': 'Probe port', '探测中…': 'Probing…', '重试当前步骤': 'Retry step', '展开日志': 'Expand log', '收起日志': 'Collapse log', '最大化': 'Maximize', '还原': 'Restore', '请求信息': 'Request', '基本信息': 'Basic info', '请求头': 'Request headers', '响应头': 'Response headers', '原始数据': 'Raw', '请求参数': 'Params', '解析数据': 'Parsed', '后端日志': 'Backend log', '重新加载': 'Reload', '日志': 'Log', '点击重新加载': 'Reload logs', '没有可解析的数据': 'No parsed data', '展开日志': 'Expand log', '收起日志': 'Collapse log', '最大化': 'Maximize', '还原': 'Restore', '请求信息': 'Request', '基本信息': 'Basic info', '请求头': 'Request headers', '响应头': 'Response headers', '原始数据': 'Raw', '请求参数': 'Params', '解析数据': 'Parsed', '后端日志': 'Backend log', '重新加载': 'Reload', '日志': 'Log', '点击重新加载': 'Reload logs', '没有可解析的数据': 'No parsed data', '执行过程': 'Execution log', '时间': 'Time', '耗时': 'Duration', '操作': 'Operation', '影响 / 接口': 'Impact / API', '数字分身 · 软件工厂 · 赛博世界': 'Digital Avatar · Software Factory · Cyber World', '创建数字分身': 'Create Digital Avatar', '创建赛博世界': 'Create Cyber World', '部署服务节点': 'Deploy node', '部署项目空间(云社区 / 云办公)': 'Deploy spaces (Community / Enterprise)', '创建项目管理': 'Create project management', '进入对话模式': 'Enter chat', '三步进入你的赛博世界:部署 → 数字分身与赛博世界 → 项目。': 'Three steps into your Cyber World: deploy → Digital Avatar & Cyber World → project.', '部署节点 → 部署空间 → 数字分身 → 赛博世界 → 项目管理 → 进入对话': 'Deploy node → Deploy spaces → Digital Avatar → Cyber World → Project → Enter chat', '将生成': 'Will create', '下一步': 'Next', '上一步': 'Back', '初始化': 'Initialize', '关闭': 'Close', '隐藏': 'Hide', '进入': 'Enter',
221
+ idle: 'IDLE', workspace: 'Workspace', applications: 'Applications', running: 'Running', offline: 'Offline', chat: 'CHAT', manage: 'MANAGE', dev: 'DEV',
222
+ },
223
+ };
224
+ // 语言在模块加载时就定下来,避免首帧(表单等 T() 文案)先按中文渲染
225
+ let CURRENT_LANG = (() => {
226
+ try {
227
+ // store-runtime:模块加载期就要定语言,早于 08-store 求值,只能直读
228
+ const m = localStorage.getItem('dsh-contextos-devops:lang') || 'system';
229
+ const nav = String((typeof navigator !== 'undefined' && navigator.language) || 'en').toLowerCase();
230
+ const v = m === 'system' ? (nav.startsWith('zh') ? 'zh' : 'en') : m;
231
+ return v === 'zh' ? 'zh' : 'en';
232
+ } catch { return 'zh'; }
233
+ })();
234
+ const EN_TEXT = {
235
+ '创建流程': 'Create flow', '创建流程申请': 'Create request', '修改流程': 'Modify flow',
236
+ '个字段': 'fields', '已提交': 'Submitted', '提交': 'Submit', '查看详情': 'View details',
237
+
238
+ '取消': 'Cancel', '保存': 'Save', '关闭': 'Close', '删除': 'Delete', '引用': 'Quote', '编辑': 'Edit', '停止': 'Stop',
239
+ '发送': 'Send', '保存并编译': 'Save + build', '创建中…': 'Creating…', '保存中…': 'Saving…', '复制': 'Copy', '新建': 'New', '界面出现错误': 'UI error', '重试': 'Retry', '刷新页面': 'Reload', '返回': 'Back', '更多': 'More', '收起': 'Less', '清除': 'Clear', '展开': 'Expand', '折叠': 'Collapse',
240
+ '上传头像照片': 'Upload avatar photo', '修改分身…': 'Edit avatar…', '切换分身': 'Switch avatar', '新建分身…': 'New avatar…',
241
+ '上传背景照片': 'Upload background photo', '赛博世界设置…': 'Cyber World settings…', '切换赛博世界': 'Switch Cyber World', '新建赛博世界': 'New Cyber World',
242
+ '初始化': 'Initialize', '进入': 'Enter', '稍后': 'Later', '添加文件': 'Attach file', '确认': 'Confirm',
243
+ '放大': 'Zoom in', '缩小': 'Zoom out', '定位': 'Locate', '全屏': 'Fullscreen', '退出全屏': 'Exit fullscreen',
244
+ '长期': 'Long', '年': 'Year', '月': 'Month', '周': 'Week', '全部日期': 'All dates', '今天': 'Today',
245
+ '提交': 'Submit', '通过': 'Approve', '驳回': 'Reject', '开始': 'Start', '完成': 'Finish', '交接': 'Handover',
246
+ '+ 计划': '+ Plan', '+ 任务': '+ Task', '+ 报告': '+ Report', '+ 反馈': '+ Feedback',
247
+ '赛博世界': 'Cyber World', '默认赛博世界': 'Default Cyber World', '默认': 'Default', '刷新': 'Refresh', '应用商店': 'Store', '部署': 'Deploy',
248
+ '隐藏左栏': 'Hide left panel', '显示左栏': 'Show left panel', '隐藏右栏': 'Hide right panel', '显示右栏': 'Show right panel',
249
+ '+ 技能': '+ Skill', '上传形象': 'Upload image', '重新开始': 'Restart', '生成': 'Generate', '隐藏': 'Hide', '后退': 'Back', '前进': 'Forward', '复制地址': 'Copy address', '在浏览器打开': 'Open in browser', '在管理中打开': 'Open in manage', '关闭标签': 'Close tab', '加载中…': 'Loading…', 'Copilot 正在执行…': 'Copilot is working…', '我的分身': 'My Digital Avatar', '我的赛博世界': 'My Cyber World',
250
+ '上一步': 'Back', '下一步': 'Next', '登录中…': 'Logging in…', '登录并获取 sessid': 'Login & get sessid',
251
+ '本机部署': 'Local', '容器部署': 'Docker', '远程部署': 'Remote deploy', '远程连接': 'Remote connect',
252
+ '部署': 'Deploy', '分身与场景': 'Digital Avatar & Cyber World', '项目': 'Project',
253
+ '源码地址': 'Repo URL', '部署目录': 'Install dir', '端口': 'Port', '镜像': 'Image', '宿主机目录(可空)': 'Host dir (optional)',
254
+ '远程主机': 'Remote host', 'SSH 用户': 'SSH user', 'SSH 端口': 'SSH port', '节点端口': 'Node port', '远程地址': 'Remote URL',
255
+ '用户名': 'Username', '密码': 'Password', '数字分身': 'Digital Avatar',
256
+ '上传头像': 'Upload avatar', '上传背景': 'Upload background', '名称': 'Name', '公司': 'Company', '城市': 'City',
257
+ '赛博世界名称': 'Cyber World name', '项目名称': 'Project name', '目标描述': 'Goal description', '计划描述': 'Plan description', '创建目标': 'Create target', '创建计划': 'Create plan', '创建任务': 'Create task', '创建报告': 'Create report', '创建反馈': 'Create feedback', '探测端口': 'Probe port', '探测中…': 'Probing…', '重试当前步骤': 'Retry step', '展开日志': 'Expand log', '收起日志': 'Collapse log', '最大化': 'Maximize', '还原': 'Restore', '请求信息': 'Request', '基本信息': 'Basic info', '请求头': 'Request headers', '响应头': 'Response headers', '原始数据': 'Raw', '请求参数': 'Params', '解析数据': 'Parsed', '后端日志': 'Backend log', '重新加载': 'Reload', '日志': 'Log', '点击重新加载': 'Reload logs', '没有可解析的数据': 'No parsed data', '展开日志': 'Expand log', '收起日志': 'Collapse log', '最大化': 'Maximize', '还原': 'Restore', '请求信息': 'Request', '基本信息': 'Basic info', '请求头': 'Request headers', '响应头': 'Response headers', '原始数据': 'Raw', '请求参数': 'Params', '解析数据': 'Parsed', '后端日志': 'Backend log', '重新加载': 'Reload', '日志': 'Log', '点击重新加载': 'Reload logs', '没有可解析的数据': 'No parsed data', '时间': 'Time', '耗时': 'Duration', '操作': 'Operation', '影响 / 接口': 'Impact / API', '正在初始化': 'Initializing', '初始化完成': 'Initialized', '初始化失败': 'Initialization failed', '失败': 'Failed', '完成': 'Done', '执行中': 'Running', '执行过程': 'Execution log', '标准空间': 'Standard spaces', '默认': 'Default',
258
+ '关闭其它标签': 'Close others', '关闭全部标签': 'Close all', '刷新': 'Refresh', '外部打开': 'Open externally',
259
+ '编辑代码': 'Edit code', '添加数据': 'Add data', '创建实例': 'Create instance', '创建应用实例': 'Create app instance', '将生成': 'Will create', '目标': 'target', '计划': 'plan', '任务': 'task',
260
+ '初始化 / INITIALIZE': 'INITIALIZE', '正在初始化 / INITIALIZING': 'INITIALIZING', '初始化完成 / INIT DONE': 'INIT DONE', '初始化失败 / INIT FAILED': 'INIT FAILED',
261
+ '三步进入你的赛博世界:部署 → 数字分身与赛博世界 → 项目。': 'Three steps into your Cyber World: deploy → Digital Avatar & Cyber World → project.',
262
+ '部署节点 → 部署空间 → 数字分身 → 赛博世界 → 项目管理 → 进入对话': 'Deploy node → Deploy spaces → Create Digital Avatar → Create Cyber World → Create project → Enter chat',
263
+ '本机直接部署节点': 'Deploy on this machine', 'docker run 一个节点': 'Run a node in Docker', 'ssh 到远端建节点': 'Deploy via SSH', '登录远端换 sessid': 'Login and get sessid',
264
+ '我的数字分身': 'My Digital Avatar', '云轩领航': 'Yunxuan', '深圳市': 'Shenzhen', '我的赛博世界': 'My Cyber World', '我的第一个项目': 'My first project',
265
+ '把想法变成可交付的软件。': 'Turn ideas into shippable software.', '先梳理需求,再搭建原型,最后验收演示。': 'Clarify requirements, build a prototype, then demo and accept.', '梳理需求': 'Collect requirements', '搭建原型': 'Build prototype', '验收演示': 'Demo & accept',
266
+ '点击选择图片': 'Click to choose image', '数字分身 · 软件工厂 · 赛博世界': 'Digital Avatar · Software Factory · Cyber World',
267
+ '用一个对话,部署节点、创建空间、生成应用、管理数字分身与日常任务。': 'One conversation to deploy nodes, create spaces, generate apps, and manage avatars and tasks.',
268
+ '消息还是空白 · 输入问题,或从下面的建议开始': 'Empty message · type a question or pick a suggestion below',
269
+ '身份层': 'Identity layer', '生产层': 'Production layer', '世界层': 'World layer',
270
+ '一个用户可以有多个分身;每个分身独立 user.uid、城市、公司、人格、喜好、风格。创建应用实例、添加数据、项目任务都归属当前分身。': 'One user can have multiple avatars; each with its own user.uid, city, company, persona, preferences and style. Instances, data and tasks belong to the active avatar.',
271
+ '从服务节点到项目空间,再到应用模板、应用代码、应用实例。把部署、生成、编译、实例化串成一条可运行的软件生产线。': 'From nodes to spaces, app templates, app code and instances — deployment, generation, build and instantiation form one runnable software line.',
272
+ '把数字分身放进可切换的赛博世界:上传背景、设置场景参数(主题/语言/时区/地点),让对话发生在你选择的世界里。': 'Put your avatar into switchable Cyber Worlds: upload backgrounds and set scene params (theme/language/timezone/location).',
273
+ '云社区 · 云办公': 'Community · Enterprise', '云社区 + 云办公已就绪': 'Community + Enterprise ready', '云社区': 'Community', '云办公': 'Enterprise', '云研发': 'R&D', '云知识': 'Knowledge', '云教育': 'Education', '云健康': 'Health', '云文旅': 'Tourism', '云公益': 'Charity', '云工业': 'Industry', '云农业': 'Agriculture',
274
+
275
+ '跳过:背景服务未就绪': 'Skipped: scene service not ready', '远程连接:跳过本地步骤': 'Remote connect: local steps skipped',
276
+ '远程会话已建立': 'Remote session established', '对话已就绪': 'Chat ready', '已上传头像': 'avatar uploaded', '已上传背景': 'background uploaded',
277
+ '部署服务节点': 'Deploy node', '部署项目空间(云社区 / 云办公)': 'Deploy spaces', '创建数字分身': 'Create avatar',
278
+ '创建赛博世界': 'Create Cyber World', '创建项目管理': 'Create project', '进入对话模式': 'Enter chat', '已上传照片 · 生成赛博世界': 'Photo · Cyber World', '默认背景': 'Default scene',
279
+ };
280
+ const T = (zh) => (CURRENT_LANG === 'zh' ? zh : (EN_TEXT[zh] || zh));
281
+ const PM_STATUS_ZH = { created: '已创建', submited: '已提交', approved: '已通过', process: '进行中', finished: '已完成', canceled: '已取消', rejected: '已拒绝' };
282
+ const PM_STATUS_EN = { created: 'Created', submited: 'Submitted', approved: 'Approved', process: 'In progress', finished: 'Finished', canceled: 'Canceled', rejected: 'Rejected' };
283
+ const pmStatusText = (v) => (CURRENT_LANG === 'zh' ? (PM_STATUS_ZH[v] || v) : (PM_STATUS_EN[v] || EN_TEXT[v] || v));
284
+ const term = (lang, key) => (TERMS[lang] && TERMS[lang][key]) || TERMS.en[key] || key;
285
+ const browserLang = () => (typeof navigator !== 'undefined' && String(navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en');
286
+
287
+ const CHAT_MODES = [
288
+ { id: 'avatar', name: '数字分身', intro: '数字分身模式:只用当前分身(user.uid / 城市 / 公司)创建应用实例、添加数据、查看应用数据;不会部署节点、创建空间或生成代码。',
289
+ quick: ['创建应用实例「员工管理」', '给「员工管理」添加一条数据', '查看「员工管理」的应用数据', '把目标拆解成计划和任务'],
290
+ introEn: 'Digital avatar mode: the active avatar user.uid/company/city are used when creating instances. Say "create employee management", "add data to recent app", "check project progress".',
291
+ quickEn: ['Create instance "Employee"', 'Add one record to "Employee"', 'View app data of "Employee"', 'Break a goal into plans and tasks'] },
292
+ { id: 'factory', name: '软件工厂', intro: '软件工厂模式:持续部署服务节点、创建项目空间、生成应用代码并编译上线;说「部署 9188 节点」「创建空间 demo」「生成员工管理」「修改应用代码」。',
293
+ quick: ['创建项目空间 demo', '生成应用模板「员工管理」', '添加应用代码「员工管理」'],
294
+ introEn: 'Software factory mode: say "deploy node 9188", "create space demo", "generate employee management", "edit app code".',
295
+ quickEn: ['Create space demo', 'Generate app template "Employee"', 'Add app code "Employee"'] },
296
+ { id: 'iot', name: '赛博世界', intro: '赛博世界模式:发现周边设备——蓝牙扫描附近设备、WiFi 扫描附近网络与同网设备,实现万物互联;也可切换场景与背景。',
297
+ quick: ['扫描周边设备', '连接周边设备', '控制周边设备「灯光」', '切换赛博世界场景'],
298
+ introEn: 'Cyber World mode: scan, connect and control nearby devices over Bluetooth/WiFi, and switch the cyber scene.',
299
+ quickEn: ['Scan nearby devices', 'Connect a nearby device', 'Control device "light"', 'Switch cyber scene'] },
300
+ ];
301
+
302
+ const reportError = (msg) => {
303
+ try { fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ op: 'clientError', error: msg }), keepalive: true }); } catch {}
304
+ };
305
+ if (typeof window !== 'undefined') {
306
+ window.addEventListener('error', (e) => reportError(String(e?.message || e?.error || '')));
307
+ window.addEventListener('unhandledrejection', (e) => reportError(String(e?.reason?.message || e?.reason || '')));
308
+ }
309
+
310
+ /** The single identity used for every request: the active avatar, else the last
311
+ * one we saw (survives reloads before `avatars` finishes loading). */
312
+ function cxActorUid(avatar) {
313
+ if (avatar && avatar.uid) return avatar.uid;
314
+ return cxReadText('actor', '');
315
+ }
@@ -0,0 +1,61 @@
1
+ const emitEvent = (detail) => { try { window.dispatchEvent(new CustomEvent('cx-event', { detail })); } catch {} };
2
+ async function api(op, params) {
3
+ const body = { op, ...(params || {}) };
4
+ const started = (typeof performance !== 'undefined' ? performance.now() : Date.now());
5
+ const brief = (() => {
6
+ try {
7
+ const raw = JSON.stringify(body);
8
+ return raw.length > 220 ? raw.slice(0, 220) + '…' : raw;
9
+ } catch { return op; }
10
+ })();
11
+ const QUERY_OPS = new Set(['health', 'nodes', 'spaces', 'apps', 'storeApps', 'avatars', 'backgrounds', 'devices', 'sshHosts', 'pmList', 'pmTree', 'projectStatus', 'fieldOptions', 'listDir', 'tree', 'readFile', 'readData', 'git', 'origin', 'stateGet', 'llmInfo', 'files', 'file']);
12
+ const method = QUERY_OPS.has(op) ? 'GET' : (op === 'pmCreate' ? 'PUT' : 'POST');
13
+ // GET 一律不走浏览器缓存:否则 /api?op=nodes 这类地址会被缓存在磁盘上,
14
+ // 插件重启、页面刷新都还是旧数据(曾导致节点编译时间永远显示 —)
15
+ const qs = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v ?? '')]));
16
+ if (method === 'GET') qs.set('_', String(Date.now()));
17
+ const query = method === 'GET' ? '?' + qs.toString() : '';
18
+ const endpoint = method === 'GET' ? `${API}${query}` : API;
19
+
20
+ const longOps = new Set(['deploy', 'dockerDeploy', 'remoteDeploy', 'remoteLogin', 'pipeline', 'buildSpace', 'initSpace', 'compile', 'chat']);
21
+ const controller = new AbortController();
22
+ const timer = setTimeout(() => controller.abort(), longOps.has(op) ? 900000 : 120000);
23
+ let res, text = '';
24
+ try {
25
+ res = await fetch(endpoint, method === 'GET'
26
+ ? { method, signal: controller.signal, cache: 'no-store' }
27
+ : { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal });
28
+ text = await res.text();
29
+ } catch (e) {
30
+ const error = e?.name === 'AbortError' ? `请求超时:${op}` : String(e?.message || e);
31
+ emitEvent({ kind: 'api', ok: false, op, text: brief, ms: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - started), error });
32
+ throw new Error(error);
33
+ } finally { clearTimeout(timer); }
34
+ const responseHeaders = (() => { try { return Object.fromEntries(res.headers.entries()); } catch { return {}; } })();
35
+ const logId = responseHeaders['log-id'] || `local-${Date.now().toString(36)}`;
36
+ const elapsed = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - started);
37
+ const bytes = (() => { try { return new TextEncoder().encode(text).length; } catch { return text.length; } })();
38
+ let payload;
39
+ try { payload = JSON.parse(text); } catch {
40
+ emitEvent({ kind: 'api', ok: false, op, text: brief, ms: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - started), error: `HTTP ${res.status}` });
41
+ throw new Error(`HTTP ${res.status} from ${API} (${op}): ${text.slice(0, 160)}`);
42
+ }
43
+
44
+ if (Array.isArray(payload.debug)) {
45
+ for (const call of payload.debug) {
46
+ try { window.dispatchEvent(new CustomEvent('cx-debug', { detail: { text: ` ↳ ${call.method} ${call.url}`, method: call.method, url: call.url, requestHeaders: call.requestHeaders || {}, responseHeaders: call.responseHeaders || {}, status: call.status || 0, size: call.size || 0, ms: call.ms || 0, logId: call.logId || '', response: call.body || null } })); } catch {}
47
+ }
48
+ }
49
+ if (!payload.ok) {
50
+ emitEvent({ kind: 'api', ok: false, op, text: brief, ms: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - started), error: String(payload.error || 'request failed') });
51
+ throw new Error(payload.error || 'request failed');
52
+ }
53
+ emitEvent({ kind: 'api', ok: true, op, text: brief, ms: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - started) });
54
+ const dataKind = /^(create|save|write|mkdir|deploy|dockerDeploy|remoteDeploy|initSpace|deployOfficialSpaces|createApp|createInstance|autogen|pmCreate|seed|createAvatar|createBackground|saveUserMedia)/.test(op) ? 'create'
55
+ : /^(update|modify|rename|set|saveAvatar|saveBackground|updateAvatar|updateBackground|pmModify|pmAct|stateSet)/.test(op) ? 'update'
56
+ : /^(remove|delete|rm|pmRemove|trash|removePath|deleteNode|deleteSpace|deleteAvatar|deleteBackground)/.test(op) ? 'delete'
57
+ : /^(pmList|pmTree|nodes|spaces|apps|storeApps|avatars|backgrounds|devices|sshHosts|listDir|tree|readFile|readData|git|health|files|file|stateGet|logs|refresh)/.test(op) ? 'refresh'
58
+ : null;
59
+ if (dataKind) { try { window.dispatchEvent(new CustomEvent('cx-data-op', { detail: { kind: dataKind, op } })); } catch {} }
60
+ return payload.data;
61
+ }
@@ -0,0 +1,85 @@
1
+ // 08-store.js — 客户端持久化唯一出口。
2
+ // 约定:localStorage 只能在这里出现(scripts/store-check.mjs 棘轮门禁);
3
+ // 新状态一律走 cxRead/cxWrite(JSON)或 cxReadText/cxWriteText(原样字符串),
4
+ // 键名先进 CX_STORE_SCHEMA,顺带成为 docs/STATE.md 与「控制台 → STATE」页的索引。
5
+ const CX_STORE_PREFIX = 'dsh-contextos-devops:';
6
+ const CX_STORE_SCHEMA = {
7
+ theme: { kind: 'text', def: 'system', zh: '主题', note: 'system / dark / light' },
8
+ lang: { kind: 'text', def: 'system', zh: '语言', note: 'system / zh / en' },
9
+ view: { kind: 'text', def: 'chat', zh: '视图模式', note: 'chat / manage / dev' },
10
+ 'work-mode': { kind: 'text', def: 'avatar', zh: '工作模式', note: 'avatar / factory / iot' },
11
+ chatfull: { kind: 'text', def: '1', zh: '对话全屏', note: '1 全屏 / 0 分栏' },
12
+ actor: { kind: 'text', def: '', zh: '当前分身', note: 'user.uid' },
13
+ nodes: { kind: 'json', def: [], zh: '节点缓存', note: '进入插件先渲染,↻ 才刷新' },
14
+ sel: { kind: 'json', def: {}, zh: '本次选择', note: '节点/空间/应用启动引导' },
15
+ 'sel-map': { kind: 'json', def: {}, zh: '按节点记忆选择', note: '{ node: [space, app] }' },
16
+ console: { kind: 'json', def: {}, zh: '控制台', note: 'open / tab / height / sort' },
17
+ reqlog: { kind: 'json', def: [], zh: '请求记录', note: '最近 50 条,服务端另有 state.reqlog' },
18
+ fx: { kind: 'json', def: { sound: 'soft', light: 'high', color: 'cyan', style: 'tech' }, zh: '特效', note: '声音/光效/配色/风格' },
19
+ webtabs: { kind: 'json', def: null, zh: '网页页签', note: '{ tabs, active }' },
20
+ panels: { kind: 'json', def: {}, zh: '面板宽度', note: '左右栏拖拽结果' },
21
+ folds: { kind: 'json', def: {}, zh: '面板折叠', note: 'PM 卡片/控制台折叠' },
22
+ 'tree-folds': { kind: 'json', def: {}, zh: '编辑器折叠', note: '按项目记 WORKSPACE/GIT 分区' },
23
+ ide: { kind: 'json', def: null, zh: '编辑器页签', note: '{ tabs, active }' },
24
+ 'ide-open': { kind: 'json', def: {}, zh: '编辑器已开文件', note: '按项目记文件树展开' },
25
+ 'ide-active': { kind: 'text', def: '', zh: '编辑器当前文件', note: '按项目记 active' },
26
+ 'ide-panes': { kind: 'json', def: {}, zh: '编辑器分栏', note: '分栏与比例' },
27
+ 'preview-w': { kind: 'text', def: '360', zh: '预览宽度', note: '网页预览分栏' },
28
+ pm: { kind: 'json', def: {}, zh: '项目管理选择', note: 'target/plan/task 选中态' },
29
+ cal: { kind: 'json', def: {}, zh: '日历', note: '周期/锚点日期' },
30
+ map: { kind: 'json', def: {}, zh: '地图', note: '中心点/缩放' },
31
+ init: { kind: 'text', def: '', zh: '初始化向导', note: '完成标记 1;细节在服务端' },
32
+ 'term-buf': { kind: 'json', def: {}, zh: '终端缓冲', note: '与服务端 state.termBuf 同步' },
33
+ draft: { kind: 'text', def: '', zh: '输入草稿', note: '未发送的输入框文本' },
34
+ scroll: { kind: 'json', def: {}, zh: '滚动位置', note: '按视图记位置' },
35
+ formstate: { kind: 'json', def: {}, zh: '表单状态', note: '按表单记未提交字段' },
36
+ calls: { kind: 'json', def: [], zh: '调用记录', note: '工具调用摘要' },
37
+ cfg: { kind: 'json', def: {}, zh: '配置草稿', note: '配置视图未保存项' },
38
+ 'filter-node': { kind: 'json', def: null, zh: '节点过滤', note: '状态筛选' },
39
+ 'filter-space': { kind: 'json', def: null, zh: '空间过滤', note: '状态筛选' },
40
+ 'filter-app': { kind: 'json', def: null, zh: '应用过滤', note: '按空间筛' },
41
+ 'filter-store': { kind: 'json', def: null, zh: '商店过滤', note: '按空间筛' },
42
+ 'monitor-panels': { kind: 'json', def: {}, zh: '监控面板', note: '面板开关/顺序' },
43
+ session: { kind: 'json', def: null, zh: '会话', note: '{ v, 会话键: 消息 }' },
44
+ chat: { kind: 'json', def: [], zh: '消息缓存', note: '旧版单键;现按 chat:<分身>:<计划> 分键' },
45
+ };
46
+
47
+ // 允许调用方传完整键(旧常量带前缀)或裸键,统一归一。
48
+ const cxKey = (key) => (String(key).startsWith(CX_STORE_PREFIX) ? String(key) : CX_STORE_PREFIX + String(key));
49
+
50
+ // 读 JSON 值;缺失/解析失败/显式 null 都回落到 def。第三个参数 truthy=true 时,解析出的假值也回落。
51
+ function cxRead(key, def, truthy = false) {
52
+ try {
53
+ const raw = localStorage.getItem(cxKey(key));
54
+ if (raw == null) return def;
55
+ const v = JSON.parse(raw);
56
+ if (v == null || (truthy && !v)) return def;
57
+ return v;
58
+ } catch { return def; }
59
+ }
60
+ function cxWrite(key, val) {
61
+ try { localStorage.setItem(cxKey(key), JSON.stringify(val)); } catch {}
62
+ }
63
+ function cxReadText(key, def) {
64
+ try { const raw = localStorage.getItem(cxKey(key)); return raw == null ? def : raw; } catch { return def; }
65
+ }
66
+ function cxWriteText(key, val) {
67
+ try { localStorage.setItem(cxKey(key), String(val == null ? '' : val)); } catch {}
68
+ }
69
+ function cxRemove(key) { try { localStorage.removeItem(cxKey(key)); } catch {} }
70
+ function cxStoreDef(key) { return (CX_STORE_SCHEMA[key] || { kind: 'json', def: null }).def; }
71
+ // 一个键 = 一个持久状态:读取用 schema 默认值,写入由 effect 自动落盘。
72
+ function cxPref(key) {
73
+ const spec = CX_STORE_SCHEMA[key] || { kind: 'json', def: null };
74
+ const [value, setValue] = useState(() => (spec.kind === 'text' ? cxReadText(key, spec.def) : cxRead(key, spec.def)));
75
+ useEffect(() => { if (spec.kind === 'text') cxWriteText(key, value); else cxWrite(key, value); }, [value]);
76
+ return [value, setValue];
77
+ }
78
+ // 控制台 STATE 页与文档索引共用:schema + 当前值 + 占用字符数。
79
+ function cxStoreDump() {
80
+ return Object.keys(CX_STORE_SCHEMA).map((key) => {
81
+ const spec = CX_STORE_SCHEMA[key];
82
+ const raw = (() => { try { return localStorage.getItem(cxKey(key)); } catch { return null; } })();
83
+ return { key, kind: spec.kind, zh: spec.zh, note: spec.note, size: raw == null ? 0 : raw.length, value: raw };
84
+ });
85
+ }