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,28 @@
1
+ function gitClass(x) { return (x === 'A' || x === '?' || x === '??' ? 'add' : x === 'D' ? 'del' : x === 'U' || x === 'UU' ? 'unt' : 'mod'); }
2
+ // Shared editor chrome. Keep the explorer's Git groups and file tabs out of
3
+ // the page component so new editor features compose these primitives.
4
+ function GitSections({ git, gitOpen, setGitOpen, gitHistOpen, setGitHistOpen, activePath, openFile, setStatus, zh }) {
5
+ if (!git) return null;
6
+ return h('div', { className: 'cx-git-sections' },
7
+ git.changed ? h('div', { className: 'cx-tree-recent' },
8
+ h('div', { className: 'cx-tree-sect cx-tree-sect-top', onClick: () => setGitOpen((v) => !v) }, h('span', { className: 'chev', dangerouslySetInnerHTML: chevHtml(gitOpen) }), zh ? 'GIT 变更' : 'GIT CHANGES',
9
+ h('span', { className: 'cx-git-count' }, String(git.changed))),
10
+ gitOpen ? Object.keys(git.files).slice(0, 60).map((fp) => h('div', { key: 'g:' + fp, className: `cx-tree-item${activePath === fp ? ' on' : ''}`, style: { paddingLeft: 6 }, onClick: () => openFile(fp) },
11
+ h('span', { className: 'chev' }),
12
+ h('span', { className: 'fic', dangerouslySetInnerHTML: fileIconHtml(fp) }),
13
+ h('span', { className: 'nm' }, fp.split('/').pop()),
14
+ h('span', { className: `cx-git-badge ${gitClass(git.files[fp])}` }, git.files[fp] === '??' ? 'U' : git.files[fp]))) : null) : null,
15
+ git.history && git.history.length ? h('div', { className: 'cx-tree-recent' },
16
+ h('div', { className: 'cx-tree-sect cx-tree-sect-top cx-git-history-head', onClick: () => setGitHistOpen((v) => !v) }, h('span', { className: 'chev', dangerouslySetInnerHTML: chevHtml(gitHistOpen) }), zh ? 'GIT 历史' : 'GIT HISTORY',
17
+ h('span', { className: 'cx-git-count' }, String(git.history.length))),
18
+ gitHistOpen ? git.history.slice(0, 30).map((c) => h('div', { key: 'h:' + c.hash, className: 'cx-tree-item cx-git-log', style: { paddingLeft: 6 }, title: `${c.hash} ${c.subject} · ${c.author} · ${c.when}`, onClick: () => { try { navigator.clipboard.writeText(c.hash); } catch {} setStatus(`${c.hash} ${c.subject}`); } },
19
+ h('span', { className: 'h' }, c.hash),
20
+ h('span', { className: 'nm' }, c.subject),
21
+ h('span', { className: 'when' }, c.when))) : null) : null);
22
+ }
23
+ function EditorTabs({ tabs, activePath, setActivePath, closeTab }) {
24
+ return h('div', { className: 'cx-tabs' }, (tabs || []).map((t) => h('div', { key: t.path, className: `cx-tab${t.path === activePath ? ' on' : ''}`, onClick: () => setActivePath(t.path), title: t.path },
25
+ h('span', { className: 'ico', dangerouslySetInnerHTML: fileIconHtml(t.name, 12) }),
26
+ h('span', { className: 'nm' }, t.name + (t.dirty ? ' ●' : '')),
27
+ h(IconButton, { name: 'close', className: 'x', onClick: (e) => { e.stopPropagation(); closeTab(t.path); } }))));
28
+ }
@@ -0,0 +1,85 @@
1
+ function DropSelect({ value, onChange, options, title, className }) {
2
+ const [open, setOpen] = useState(false);
3
+ const [pos, setPos] = useState(null);
4
+ const btnRef = useRef(null);
5
+ const lbRef = useRef(null);
6
+ const openMenu = (e) => { e.stopPropagation(); setOpen((v) => !v); };
7
+ useEffect(() => {
8
+ if (!open) return;
9
+ const close = () => setOpen(false);
10
+ window.addEventListener('click', close);
11
+ window.addEventListener('scroll', close, true);
12
+ return () => { window.removeEventListener('click', close); window.removeEventListener('scroll', close, true); };
13
+ }, [open]);
14
+ const cur = (options || []).find((o) => o.value === value);
15
+ return h('span', { className: `cx-drop${className ? ' ' + className : ''}` },
16
+ h('button', { ref: btnRef, type: 'button', className: `cx-drop-btn${open ? ' on' : ''}`, title, onClick: openMenu },
17
+ h('span', { className: 'lb' }, cur ? cur.label : ''),
18
+ h('span', { className: 'cv' }, h(UiIcon, { name: 'chevronDown', size: 12 }))),
19
+ open ? h('div', { className: 'cx-drop-pop', onClick: (e) => e.stopPropagation() },
20
+ h(MenuList, { items: (options || []).map((o) => ({ label: o.label, icon: o.icon, on: o.value === value, go: () => { setOpen(false); onChange(o.value); } })), onPick: () => setOpen(false) })) : null);
21
+ }
22
+
23
+ function MenuButton({ items, disabled }) {
24
+ const [open, setOpen] = useState(false);
25
+ const fire = (e) => {
26
+ e.stopPropagation();
27
+ const r = e.currentTarget.getBoundingClientRect();
28
+ try {
29
+ window.dispatchEvent(new CustomEvent('cx-float-menu', { detail: {
30
+ x: Math.round(r.left),
31
+ right: Math.round((window.innerWidth || 1200) - r.right),
32
+ y: Math.round(Math.min(r.bottom + 4, (window.innerHeight || 800) - 300)),
33
+ title: '更多',
34
+ icon: 'more',
35
+ items: (items || []).map((it) => (it.sep ? { sep: true } : { label: T(it.label), icon: it.icon || 'chevronRight', url: it.url || '', danger: !!it.danger, open: !!it.open, openable: !!it.openable, go: () => it.action() })),
36
+ } }));
37
+ } catch {}
38
+ };
39
+ return h('span', { className: 'cx-menu-wrap' },
40
+ h(IconButton, { name: 'more', className: 'cx-btn xs', title: '⋯', disabled, onClick: fire }));
41
+ }
42
+
43
+ function Modal({ title, desc, onClose, children }) {
44
+ return h('div', { className: 'cx-modal', onMouseDown: (e) => { if (e.target === e.currentTarget) onClose(); } },
45
+ h('div', { className: 'cx-card' }, h('h2', null, title), desc ? h('div', { className: 'desc' }, desc) : null, children));
46
+ }
47
+
48
+ function SceneModal({ scene, onClose, onSave }) {
49
+ const p = scene?.params || {};
50
+ const tzDefault = (typeof Intl !== 'undefined' && Intl.DateTimeFormat().resolvedOptions().timeZone) || 'Asia/Shanghai';
51
+ const [name, setName] = useState(scene?.name || '');
52
+ const [theme, setTheme] = useState(p.theme || 'system');
53
+ const [lang, setLang] = useState(p.lang || 'system');
54
+ const [timezone, setTimezone] = useState(p.timezone || tzDefault);
55
+ const [location, setLocation] = useState(p.location || '');
56
+ return h(Modal, { title: '赛博世界设置 / CYBER WORLD', desc: '这些参数属于当前赛博世界:切换赛博世界会自动跟随;没设置的用系统默认。', onClose },
57
+ h('div', { className: 'cx-form' },
58
+ h('label', { className: 'cx-field full' }, '名称', h('input', { className: 'cx-input', value: name, placeholder: '例如 我的赛博世界', onChange: (e) => setName(e.target.value) })),
59
+ h('label', { className: 'cx-field full' }, '主题', h(DropSelect, { value: theme, onChange: setTheme, options: [{ value: 'system', label: '跟随系统' }, { value: 'light', label: '浅色' }, { value: 'dark', label: '深色' }] })),
60
+ h('label', { className: 'cx-field full' }, '语言', h(DropSelect, { value: lang, onChange: setLang, options: [{ value: 'system', label: '跟随系统' }, { value: 'zh', label: '中文' }, { value: 'en', label: 'English' }] })),
61
+ h('label', { className: 'cx-field full' }, '时区', h('input', { className: 'cx-input', value: timezone, placeholder: tzDefault, onChange: (e) => setTimezone(e.target.value) })),
62
+ h('label', { className: 'cx-field full' }, '地点', h('input', { className: 'cx-input', value: location, placeholder: '例如 深圳市', onChange: (e) => setLocation(e.target.value) }))),
63
+ h('div', { className: 'cx-foot' },
64
+ h('button', { className: 'cx-btn', onClick: onClose }, T('取消')),
65
+ h('button', { className: 'cx-btn pri', onClick: () => onSave({ name: name.trim(), theme, lang, timezone, location }) }, T('保存'))));
66
+ }
67
+
68
+ function AvatarEditModal({ avatar, onClose, onSave }) {
69
+ const [form, setForm] = useState({
70
+ name: avatar?.name || '', city: avatar?.city || '', company: avatar?.company || '',
71
+ persona: avatar?.persona || '', preferences: avatar?.preferences || '', style: avatar?.style || '',
72
+ });
73
+ const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
74
+ return h(Modal, { title: '修改分身 / AVATAR', desc: '这些信息属于当前数字分身,创建实例与对话上下文都会带上。', onClose },
75
+ h('div', { className: 'cx-form' },
76
+ h('label', { className: 'cx-field full' }, '名称', h('input', { className: 'cx-input', value: form.name, onChange: set('name') })),
77
+ h('label', { className: 'cx-field full' }, '城市', h('input', { className: 'cx-input', value: form.city, onChange: set('city') })),
78
+ h('label', { className: 'cx-field full' }, '公司', h('input', { className: 'cx-input', value: form.company, onChange: set('company') })),
79
+ h('label', { className: 'cx-field full' }, '人格', h('input', { className: 'cx-input', value: form.persona, onChange: set('persona'), placeholder: '专业、亲和、直接' })),
80
+ h('label', { className: 'cx-field full' }, '偏好', h('input', { className: 'cx-input', value: form.preferences, onChange: set('preferences'), placeholder: '技术、产品、效率' })),
81
+ h('label', { className: 'cx-field full' }, '风格', h('input', { className: 'cx-input', value: form.style, onChange: set('style'), placeholder: '简洁、结构化' }))),
82
+ h('div', { className: 'cx-foot' },
83
+ h('button', { className: 'cx-btn', onClick: onClose }, T('取消')),
84
+ h('button', { className: 'cx-btn pri', onClick: () => onSave(form) }, T('保存'))));
85
+ }
@@ -0,0 +1,176 @@
1
+ // 节点/空间通用的常用工具(都是宿主自带页面)
2
+ // key, 中文, 英文, 尾段, 图标
3
+ const CARD_TOOLS = [
4
+ ['desktop', '应用桌面', 'App Desktop', '', 'monitor'],
5
+ ['portal', '官方网站', 'Website', '', 'web'],
6
+ ['admin', '管理后台', 'Admin', '', 'manage'],
7
+ ['vimer', '编程工具', 'Code', '', 'dev'],
8
+ ['runtime', '运行环境', 'Runtime', '', 'pulse'], // /c/runtime
9
+ ['xterm', '命令终端', 'Terminal', '/type/sh', 'terminal'], // 直接进 shell,不停在会话列表
10
+ ];
11
+ // 首页由节点/空间自己的代码决定,插件只负责「打开首页」;这里只列固定工具
12
+ // 副标题用的时间:MM-DD HH:mm
13
+ const cardTime = (v) => {
14
+ const s = String(v || '').replace('T', ' ').replace(/\..*$/, '');
15
+ const m = s.match(/^(?:\d{4}-)?(\d{2}-\d{2})[ T](\d{2}:\d{2})/);
16
+ return m ? `${m[1]} ${m[2]}` : (s || '');
17
+ };
18
+ // 版本号兜底:老接口会把整张 /c/version 表塞进 version,这里只取 vX.Y.Z-xxx
19
+ const cardVer = (v) => {
20
+ const raw = String(v || '').trim();
21
+ if (!raw) return '';
22
+ if (!/\s/.test(raw)) return raw;
23
+ const m = raw.match(/v\d+\.\d+\.\d+(?:-[0-9a-z.]+)+/i);
24
+ return m ? m[0] : '';
25
+ };
26
+ const toolItems = (onTool, urlOf, locale, isOpen = null) => (onTool ? CARD_TOOLS.map(([key, zh, en, tail, icon]) => {
27
+ const url = urlOf(key, tail || '');
28
+ return {
29
+ label: locale === 'zh' ? zh : en,
30
+ icon,
31
+ url,
32
+ openable: !!isOpen,
33
+ open: isOpen ? !!isOpen(url) : false,
34
+ action: () => onTool(url, locale === 'zh' ? zh : en),
35
+ };
36
+ }) : []);
37
+
38
+ function NodeCard({ n, active, busy, onSelect, onStart, onStop, onDelete, onOpen, onIde, onTool, onHome, onAuth, isOpen, tone = () => '', locale }) {
39
+ const on = n.state === 'running';
40
+ return h('div', { className: `cx-node${active ? ' on' : ''}`, onClick: () => onSelect(n.port) },
41
+ h(EntityTitle, {
42
+ image: n.image,
43
+ icon: 'server',
44
+ title: n.name || `node-${n.port}`,
45
+ status: on ? (locale === 'zh' ? '运行中' : 'RUNNING') : (locale === 'zh' ? '离线' : 'OFFLINE'),
46
+ statusTone: on ? 'ok' : 'off',
47
+ // 端口 · 代码版本 · 编译时间 · 程序大小
48
+ // 端口 · 版本 · 编译时间 · 大小 · 进程 · 部署方式
49
+ meta: [`:${n.port}`, cardVer(n.version) || '—',
50
+ cardTime(n.builtAt || (n.runtime && n.runtime.builtAt)) || '—',
51
+ n.sizeText || (n.runtime && n.runtime.sizeText) || (n.size ? Math.round(n.size / 1048576) + 'M' : '—'),
52
+ n.pid ? `pid ${n.pid}` : '',
53
+ n.source || ''].filter(Boolean).join(' · '),
54
+ }),
55
+ h('div', { className: 'cx-node-actions' },
56
+ on
57
+ ? h('button', { className: 'cx-btn xs', onClick: (e) => { e.stopPropagation(); onOpen(n.port); } }, locale === 'zh' ? '首页' : 'HOME')
58
+ : h('button', { className: 'cx-btn xs pri', onClick: (e) => { e.stopPropagation(); onStart(n.port); } }, locale === 'zh' ? '启动' : 'START'),
59
+ h('button', { className: 'cx-btn xs', disabled: busy, onClick: (e) => { e.stopPropagation(); onIde && onIde(n.port, ''); } }, locale === 'zh' ? '编程' : 'CODE'),
60
+ on
61
+ ? h(MenuButton, { disabled: busy, items: [
62
+ ...(onHome ? [{ label: locale === 'zh' ? '打开首页' : 'Open home', icon: 'home', action: () => onHome() }] : []),
63
+ ...toolItems(onTool, (k, tail = '') => `http://127.0.0.1:${n.port}/c/${k}${tail}`, locale, isOpen),
64
+ ...(onAuth ? [{ label: locale === 'zh' ? '登录凭证' : 'Credentials', icon: 'lock', action: () => onAuth(n.port) }] : []),
65
+ { sep: true }, { label: '停止', icon: 'stop', danger: tone('stop') === 'danger', action: () => onStop(n.port) }, { label: '删除', icon: 'trash', danger: true, action: () => onDelete(n.port) }] })
66
+ : h(MenuButton, { disabled: busy, items: [
67
+ ...(onHome ? [{ label: locale === 'zh' ? '打开首页' : 'Open home', icon: 'home', action: () => onHome() }] : []),
68
+ ...toolItems(onTool, (k, tail = '') => `http://127.0.0.1:${n.port}/c/${k}${tail}`, locale, isOpen),
69
+ ...(onAuth ? [{ label: locale === 'zh' ? '登录凭证' : 'Credentials', icon: 'lock', action: () => onAuth(n.port) }] : []),
70
+ { sep: true },
71
+ { label: '删除', icon: 'trash', danger: tone('remove') === 'danger', action: () => onDelete(n.port) },
72
+ ] })));
73
+ }
74
+
75
+ function SpaceCard({ s, active, busy, node, onSelect, onOpen, onStart, onStop, onDelete, onIde, onTool, onHome, isOpen, tone = () => '', locale }) {
76
+ const on = String(s.status || '').trim() === 'start';
77
+ const iconUrl = (() => {
78
+ const raw = String(s.icons || '').trim();
79
+ if (!raw) return '';
80
+ if (/^https?:\/\//i.test(raw)) return raw;
81
+ const p = raw.startsWith('/') ? raw : `/${raw}`;
82
+ return node ? `http://127.0.0.1:${node}${p}` : p;
83
+ })();
84
+ return h('div', { className: `cx-space${active ? ' on' : ''}`, onClick: () => onSelect(s.name) },
85
+ h(EntityTitle, {
86
+ image: iconUrl,
87
+ icon: 'space',
88
+ title: [s.help, s.name].filter(Boolean).join(' '),
89
+ status: on ? (locale === 'zh' ? '运行中' : 'start') : (locale === 'zh' ? '已停止' : 'stop'),
90
+ statusTone: on ? 'ok' : 'off',
91
+ // 代码版本 · 编译时间 · 程序大小
92
+ meta: [cardVer(s.version) || '—', cardTime(s.builtAt) || '—', s.size || '—'].join(' · '),
93
+ }),
94
+ h('div', { className: 'cx-space-actions' },
95
+ on ? h('button', { className: 'cx-btn xs pri', disabled: busy, onClick: (e) => { e.stopPropagation(); onOpen(s.name); } }, locale === 'zh' ? '首页' : 'OPEN') : h('button', { className: 'cx-btn xs pri', disabled: busy, onClick: (e) => { e.stopPropagation(); onStart(s.name); } }, locale === 'zh' ? '启动' : 'START'),
96
+ h('button', { className: 'cx-btn xs', disabled: busy, onClick: (e) => { e.stopPropagation(); onIde && onIde(node, s.name); } }, locale === 'zh' ? '编程' : 'CODE'),
97
+ h(MenuButton, { disabled: busy, items: [
98
+ ...(onHome ? [{ label: locale === 'zh' ? '打开首页' : 'Open home', icon: 'home', action: () => onHome() }] : []),
99
+ ...toolItems(onTool, (k, tail = '') => `http://127.0.0.1:${node}/s/${encodeURIComponent(s.name)}/c/${k}${tail}`, locale, isOpen),
100
+ ...(on ? [{ sep: true }, { label: '停止', icon: 'stop', danger: tone('stop') === 'danger', action: () => onStop(s.name) }] : []),
101
+ { label: '删除', icon: 'trash', danger: true, action: () => onDelete(s.name) },
102
+ ] })));
103
+ }
104
+
105
+ function ItemIcon({ item, name, size, src, port }) {
106
+ const raw = String(src || (item && (item.icons || item.icon || item.image)) || '').trim();
107
+ const iconSrc = !raw ? ''
108
+ : /^(https?:|data:)/i.test(raw) ? raw
109
+ : raw.startsWith('/') && port ? `http://127.0.0.1:${port}${raw}`
110
+ : '';
111
+ const label = String(name || (item && (item.title || item.name)) || '?').trim().slice(0, 1) || '?';
112
+ let hue = 0;
113
+ const sname = String((item && (item.name || item.command)) || label);
114
+ for (let i = 0; i < sname.length; i += 1) hue = (hue * 31 + sname.charCodeAt(i)) % 360;
115
+ const px = size || 26;
116
+ if (iconSrc && /^(https?:|\/)/.test(iconSrc)) {
117
+ return h('span', { className: 'cx-ai', style: { width: px, height: px } }, h('img', { src: iconSrc, alt: '' }));
118
+ }
119
+ return h('span', { className: 'cx-ai', style: { width: px, height: px, background: `linear-gradient(135deg, hsl(${hue} 72% 42%), hsl(${(hue + 42) % 360} 70% 26%))` } }, label.toUpperCase());
120
+ }
121
+
122
+ function AppRow({ a, active, onSelect, onOpen, locale, node }) {
123
+ return h('div', { className: `cx-app${active ? ' on' : ''}`, onClick: () => (onSelect ? onSelect(a) : onOpen(a)) },
124
+ h(EntityTitle, {
125
+ image: a.place_avatar || a.icon ? (/^https?:/i.test(a.place_avatar || a.icon) ? (a.place_avatar || a.icon) : `http://127.0.0.1:${node}${a.place_avatar || a.icon}`) : '',
126
+ icon: 'app',
127
+ title: a.title || a.name,
128
+ after: cardTime(a.updated) || '',
129
+ meta: [a.city_name, a.company_name, a.service_name].filter(Boolean).join(' · ') || a.command,
130
+ }),
131
+ h('div', { className: 'aa' },
132
+ h('button', { className: 'cx-btn xs pri', onClick: (e) => { e.stopPropagation(); onOpen(a); } }, locale === 'zh' ? '打开' : 'OPEN')));
133
+ }
134
+
135
+ function StoreRow({ s, onCreate, busy, onAction, onCode, onConfig, locale, node }) {
136
+ const online = s.status === 'online';
137
+ const acts = s.actions || ['create', 'online', 'offline', 'code', 'conf'];
138
+ const has = (x) => acts.includes(x);
139
+ const label = (x) => locale === 'zh' ? ({ create: '创建', online: '上架', offline: '下架', code: '编程', conf: '配置' }[x] || x) : ({ create: 'CREATE', online: 'Online', offline: 'Offline', code: 'Code', conf: 'Config' }[x] || x);
140
+ return h('div', { className: 'cx-store' },
141
+ h('div', { className: 'cx-store-main', onClick: () => !busy && onCreate(s) },
142
+ h(ItemIcon, { item: s, name: s.name, size: 26, src: s.icon, port: node }),
143
+ h('div', { style: { flex: 1, minWidth: 0 } },
144
+ h('div', { className: 'cx-store-titleline' },
145
+ h('span', { className: 'sn' }, s.name),
146
+ h('span', { className: `cx-status-r ${online ? 'on' : 'off'}` }, s.status_label || s.status)),
147
+ h('div', { className: 'ss' }, `${s.space} · ${s.command}`))),
148
+ h('div', { className: 'cx-store-actions' },
149
+ has('create') ? h('button', { className: 'cx-btn xs pri', disabled: busy, onClick: (e) => { e.stopPropagation(); onCreate(s); } }, busy ? '…' : label('create')) : null,
150
+ has('online') ? h('button', { className: 'cx-btn xs', disabled: busy, onClick: (e) => { e.stopPropagation(); onAction(s, 'online'); } }, label('online')) : null,
151
+ has('offline') ? h('button', { className: 'cx-btn xs danger', disabled: busy, onClick: (e) => { e.stopPropagation(); onAction(s, 'offline'); } }, label('offline')) : null,
152
+ (has('code') || has('conf') || has('more')) ? h(MenuButton, { disabled: busy, items: [
153
+ has('code') ? { label: label('code'), icon: 'dev', action: () => onCode(s) } : null,
154
+ has('conf') ? { label: label('conf'), icon: 'setting', action: () => onConfig(s) } : null,
155
+ has('more') ? { label: locale === 'zh' ? '更多' : 'More', icon: 'more', action: () => onAction(s, 'more') } : null,
156
+ ].filter(Boolean) }) : null));
157
+ }
158
+
159
+ function OfficialSpaces({ node, onDeploy, locale }) {
160
+ const [keys, setKeys] = useState(['community', 'enterprise']);
161
+ const items = OFFICIAL;
162
+ const toggle = (k) => { if (k === 'community' || k === 'enterprise') return; setKeys((xs) => xs.includes(k) ? xs.filter((x) => x !== k) : [...xs, k]); };
163
+ return h('div', { className: 'cx-official' },
164
+ h('div', { className: 'oh' }, locale === 'zh' ? '一键部署官方空间 / OFFICIAL SPACES' : 'OFFICIAL SPACES / One-click deploy'),
165
+ items.map(([k, label, en]) => {
166
+ const forced = k === 'community' || k === 'enterprise';
167
+ const on = forced || keys.includes(k);
168
+ return h('label', { key: k, className: `cx-official-row${forced ? ' forced' : ''}` },
169
+ h('input', { type: 'checkbox', checked: on, disabled: forced, onChange: () => toggle(k) }),
170
+ h('span', { className: 'cx-space-ico', dangerouslySetInnerHTML: spaceIconHtml(label, 14) }),
171
+ h('span', { className: 'cx-space-name' }, label),
172
+ h('em', { className: 'cx-space-en' }, en || ''),
173
+ forced ? h('i', null, T('默认')) : null);
174
+ }),
175
+ h('button', { className: 'cx-btn xs pri cx-deploy-btn', style: { marginTop: 8 }, disabled: !node || !keys.length, onClick: () => onDeploy(keys) }, locale === 'zh' ? `部署 (${keys.length})` : `DEPLOY (${keys.length})`));
176
+ }
@@ -0,0 +1,176 @@
1
+ function DeployModal({ onClose, onDone, onScan }) {
2
+ const [port, setPort] = useState('9188');
3
+ const [name, setName] = useState('');
4
+ const [mode, setMode] = useState('auto');
5
+ const [parent, setParent] = useState('');
6
+ const [repo, setRepo] = useState('');
7
+ const [sourceBin, setSourceBin] = useState('');
8
+ const [spaceKeys, setSpaceKeys] = useState(['community', 'enterprise']);
9
+ const [busy, setBusy] = useState(false);
10
+ const [err, setErr] = useState('');
11
+ const go = async () => {
12
+ setBusy(true); setErr('');
13
+ try { await api('deploy', { port: Number(port), name, mode, parent, repo, sourceBin }); await api('officialSpaces', { node: Number(port), keys: spaceKeys }); onDone(); }
14
+ catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
15
+ };
16
+ return h(Modal, { title: '部署服务节点 / DEPLOY NODE', desc: 'auto=先 clone 代码库,有 git+go 编译,否则复制或下载 ice.bin;默认选中云社区、云办公。', onClose },
17
+ h('div', { className: 'cx-form' },
18
+ h('label', { className: 'cx-field' }, '部署方式 / MODE', h('select', { className: 'cx-select', value: mode, onChange: (e) => setMode(e.target.value) },
19
+ h('option', { value: 'auto' }, 'auto / 克隆后编译/复制/下载'), h('option', { value: 'copy' }, 'copy / 本机复制'), h('option', { value: 'download' }, 'download / 下载'), h('option', { value: 'source' }, 'source / 源码构建'))),
20
+ h('label', { className: 'cx-field' }, '端口 / PORT', h('input', { className: 'cx-input', value: port, onChange: (e) => setPort(e.target.value) })),
21
+ h('label', { className: 'cx-field' }, '名称 / NAME', h('input', { className: 'cx-input', value: name, placeholder: 'ContextOS-prod', onChange: (e) => setName(e.target.value) })),
22
+ (mode === 'download' || mode === 'source') ? h('label', { className: 'cx-field' }, '父节点 / PARENT', h('input', { className: 'cx-input', value: parent, placeholder: 'https://shylinux.com', onChange: (e) => setParent(e.target.value) })) : null,
23
+ mode === 'source' ? h('label', { className: 'cx-field full' }, '代码仓库 / REPO', h('input', { className: 'cx-input', value: repo, placeholder: 'https://shylinux.com/x/ContextOS', onChange: (e) => setRepo(e.target.value) })) : null,
24
+ mode === 'copy' ? h('label', { className: 'cx-field full' }, '节点镜像 / SOURCE BIN', h('input', { className: 'cx-input', value: sourceBin, placeholder: '留空用 usr/publish,或 /path/bin/ice.bin', onChange: (e) => setSourceBin(e.target.value) })) : null,
25
+ h('label', { className: 'cx-field full' }, '标准空间 / SPACES', h(StandardSpaces, { value: spaceKeys, onChange: setSpaceKeys }))),
26
+ err ? h(ErrorBox, { text: err }) : null,
27
+ h('div', { className: 'cx-foot' },
28
+ onScan ? h('button', { className: 'cx-btn', style: { marginRight: 'auto' }, onClick: onScan, title: '扫描远程主机 / WiFi / 蓝牙' }, '扫描设备') : null,
29
+ h('button', { className: 'cx-btn', onClick: onClose }, T('取消')),
30
+ h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '部署中…' : '部署')));
31
+ }
32
+
33
+ function SpaceModal({ node, onClose, onDone }) {
34
+ const [name, setName] = useState('project');
35
+ const [busy, setBusy] = useState(false);
36
+ const [err, setErr] = useState('');
37
+ const go = async () => {
38
+ setBusy(true); setErr('');
39
+ try {
40
+ const created = await api('createSpace', { node: Number(node), name });
41
+ await api('initSpace', { node: Number(node), space: created.name });
42
+ onDone(created.name);
43
+ } catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
44
+ };
45
+ return h(Modal, { title: '创建项目空间 / CREATE WORKSPACE', desc: '不指定 repos:dream create 建空空间 → web.code.git.status init 生成 .git / src/main.go / go.mod 骨架。', onClose },
46
+ h('div', { className: 'cx-form' }, h('label', { className: 'cx-field full' }, '空间名称 / SPACE NAME', h('input', { className: 'cx-input', value: name, placeholder: 'project', onChange: (e) => setName(e.target.value) }))),
47
+ h('div', { className: 'desc' }, '空间名只用字母、数字、下划线;服务端自动加 YYYYMMDD- 前缀。'),
48
+ err ? h(ErrorBox, { text: err }) : null,
49
+ h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '创建中…' : '创建并初始化')));
50
+ }
51
+
52
+ function AppModal({ node, space, onClose, onDone }) {
53
+ const [name, setName] = useState('yuangong');
54
+ const [help, setHelp] = useState('员工管理');
55
+ const [place, setPlace] = useState('employee');
56
+ const [value, setValue] = useState('profile,position,skill,experience,education,record');
57
+ const [trans, setTrans] = useState('员工信息,工作岗位,专业技能,工作经验,教育经历,事件记录');
58
+ const [busy, setBusy] = useState(false);
59
+ const [err, setErr] = useState('');
60
+ const go = async () => {
61
+ setBusy(true); setErr('');
62
+ try {
63
+ const r = await api('autogenApp', { node: Number(node), space, name, help, place, value, trans });
64
+ onDone(r);
65
+ } catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
66
+ };
67
+ return h(Modal, { title: '生成应用 / GENERATE APP', desc: '云社区 service/autogen 按应用平台风格生成:portal.json + 五角色 + 列表/创建 + 看板;随后 compile。', onClose },
68
+ h('div', { className: 'cx-form' },
69
+ h('label', { className: 'cx-field' }, '应用名称 / APP NAME', h('input', { className: 'cx-input', value: name, onChange: (e) => setName(e.target.value) })),
70
+ h('label', { className: 'cx-field' }, '标题 / TITLE', h('input', { className: 'cx-input', value: help, onChange: (e) => setHelp(e.target.value) })),
71
+ h('label', { className: 'cx-field' }, '场景 / PLACE', h('input', { className: 'cx-input', value: place, onChange: (e) => setPlace(e.target.value) })),
72
+ h('label', { className: 'cx-field' }, '字段 / VALUE (逗号分隔)', h('input', { className: 'cx-input', value: value, onChange: (e) => setValue(e.target.value) })),
73
+ h('label', { className: 'cx-field full' }, '翻译 / TRANS(中文)', h('input', { className: 'cx-input', value: trans, onChange: (e) => setTrans(e.target.value) }))),
74
+ err ? h(ErrorBox, { text: err }) : null,
75
+ h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '生成中…' : '生成并编译')));
76
+ }
77
+
78
+ function CodeModal({ node, space, app, onClose }) {
79
+ const [files, setFiles] = useState([]);
80
+ const [showMore, setShowMore] = useState(false);
81
+ const [pm, setPm] = useState(null);
82
+ const [targetId, setTargetId] = useState('');
83
+ const [planId, setPlanId] = useState('');
84
+ const [taskId, setTaskId] = useState('');
85
+ const [reportId, setReportId] = useState('');
86
+ const [feedbackId, setFeedbackId] = useState('');
87
+ const bootCal = cxRead('cal', {});
88
+ const [calStart, setCalStart] = useState(bootCal.start || '');
89
+ const [calEnd, setCalEnd] = useState(bootCal.end || '');
90
+ useEffect(() => cxWrite('cal', { ...cxRead('cal', {}), start: calStart, end: calEnd }), [calStart, calEnd]);
91
+ const [planStatusFilter, setPlanStatusFilter] = useState([]);
92
+ const [taskStatusFilter, setTaskStatusFilter] = useState([]);
93
+ const [reportTypeFilter, setReportTypeFilter] = useState([]);
94
+ const [feedbackStatusFilter, setFeedbackStatusFilter] = useState([]);
95
+ const [pmForm, setPmForm] = useState(null);
96
+ const sessionStateRef = useRef({ key: '', ready: false, msgs: [] });
97
+ const seedingRef = useRef(false);
98
+ const [file, setFile] = useState('portal.go');
99
+ const [content, setContent] = useState('');
100
+ const [msg, setMsg] = useState('');
101
+ const [busy, setBusy] = useState(false);
102
+ const [err, setErr] = useState('');
103
+ useEffect(() => {
104
+ let alive = true;
105
+ api('listDir', { node, space, path: `src/${app.name}` }).then((d) => {
106
+ if (!alive) return;
107
+ const list = (d.entries || []).filter((e) => e.type === 'file' && /\.(go|json|js|css|md)$/.test(e.name));
108
+ setFiles(list);
109
+ const pick = list.find((e) => e.name === 'portal.go') || list[0];
110
+ if (pick) setFile(pick.name);
111
+ }).catch((e) => setErr(String(e?.message || e)));
112
+ return () => { alive = false; };
113
+ }, [node, space, app.name]);
114
+ useEffect(() => {
115
+ if (!file) return;
116
+ let alive = true;
117
+ api('readFile', { node, space, path: `src/${app.name}/${file}` }).then((d) => { if (alive) setContent(d.content || ''); }).catch((e) => { if (alive) setErr(String(e?.message || e)); });
118
+ return () => { alive = false; };
119
+ }, [node, space, app.name, file]);
120
+ const save = async (build) => {
121
+ setBusy(true); setErr(''); setMsg('');
122
+ try {
123
+ await api('writeFile', { node, space, path: `src/${app.name}/${file}`, content });
124
+ setMsg('saved');
125
+ if (build) { await api('buildSpace', { node, space }); setMsg('saved + built'); }
126
+ } catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
127
+ };
128
+ return h(Modal, { title: `CODE / ${app.title || app.name}`, desc: `编辑 ${space}/src/${app.name};保存后可编译。`, onClose },
129
+ h('div', { className: 'cx-form' },
130
+ h('label', { className: 'cx-field full' }, 'FILE', h('select', { className: 'cx-select', value: file, onChange: (e) => setFile(e.target.value) }, files.map((f) => h('option', { key: f.name, value: f.name }, f.name)))),
131
+ h('label', { className: 'cx-field full' }, 'CONTENT', h('textarea', { className: 'cx-area', style: { minHeight: 300 }, value: content, spellCheck: false, onChange: (e) => setContent(e.target.value) }))),
132
+ msg ? h('div', { className: 'desc' }, msg) : null,
133
+ err ? h(ErrorBox, { text: err }) : null,
134
+ h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('关闭')), h('button', { className: 'cx-btn', disabled: busy, onClick: () => save(false) }, busy ? '…' : T('保存')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: () => save(true) }, T('保存并编译'))));
135
+ }
136
+
137
+ function InstanceModal({ node, space, app, avatar, onClose, onDone }) {
138
+ const [name, setName] = useState(app.title || '我的应用');
139
+ const [city, setCity] = useState(avatar?.city || '深圳市');
140
+ const [company, setCompany] = useState(avatar?.company || '云轩领航');
141
+ const [busy, setBusy] = useState(false);
142
+ const [err, setErr] = useState('');
143
+ const go = async () => {
144
+ setBusy(true); setErr('');
145
+ try { const r = await api('createInstance', { node, space, command: app.command, place: app.place || '', name, city, company, uid: cxActorUid(avatar) }); onDone(r); }
146
+ catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
147
+ };
148
+ return h(Modal, { title: `创建应用实例 / ${app.title || app.name}`, desc: `在 ${space} 用 ${app.command} 建实例;实例行会出现在该用户的 recent 列表。`, onClose },
149
+ h('div', { className: 'cx-form' },
150
+ h('label', { className: 'cx-field full' }, '实例名称 / INSTANCE NAME', h('input', { className: 'cx-input', value: name, onChange: (e) => setName(e.target.value) })),
151
+ h('label', { className: 'cx-field' }, '城市 / CITY', h('input', { className: 'cx-input', value: city, onChange: (e) => setCity(e.target.value) })),
152
+ h('label', { className: 'cx-field' }, '公司 / COMPANY', h('input', { className: 'cx-input', value: company, onChange: (e) => setCompany(e.target.value) }))),
153
+ err ? h(ErrorBox, { text: err }) : null,
154
+ h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '创建中…' : '创建应用实例')));
155
+ }
156
+
157
+ function StoreInstanceModal({ node, service, avatar, onClose, onDone }) {
158
+ const [city, setCity] = useState(avatar?.city || '深圳市');
159
+ const [company, setCompany] = useState(avatar?.company || '云轩领航');
160
+ const [name, setName] = useState(String(service.name || '').split(/\s+/)[0] || '我的应用');
161
+ const [busy, setBusy] = useState(false);
162
+ const [err, setErr] = useState('');
163
+ const go = async () => {
164
+ setBusy(true); setErr('');
165
+ try { const r = await api('createStoreInstance', { node, service, city, company, name, uid: avatar?.uid || '' }); onDone(r); }
166
+ catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
167
+ };
168
+ return h(Modal, { title: `创建应用实例 / ${service.name}`, desc: '从应用商店模板创建实例:城市 + 公司 + 应用名称三个参数。', onClose },
169
+ h('div', { className: 'cx-form' },
170
+ h('label', { className: 'cx-field full' }, '城市名称 / CITY', h('input', { className: 'cx-input', value: city, onChange: (e) => setCity(e.target.value) })),
171
+ h('label', { className: 'cx-field full' }, '公司名称 / COMPANY', h('input', { className: 'cx-input', value: company, onChange: (e) => setCompany(e.target.value) })),
172
+ h('label', { className: 'cx-field full' }, '应用名称 / APP NAME', h('input', { className: 'cx-input', value: name, onChange: (e) => setName(e.target.value) }))),
173
+ h('div', { className: 'desc' }, `space: ${service.space} · command: ${service.command}`),
174
+ err ? h(ErrorBox, { text: err }) : null,
175
+ h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '创建中…' : '创建应用实例')));
176
+ }