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.
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 +91 -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,186 @@
1
+ // 节点台账与磁盘扫描:~/.contextos/nodes.json(与 macOS 客户端共用)。
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { HOME, stateDir } from './env.js';
5
+ import { platformTriple } from './platform.js';
6
+ import { probe } from './http.js';
7
+
8
+ export function registryPath() {
9
+ return process.env.CTX_NODES_FILE || path.join(HOME(), '.contextos', 'nodes.json');
10
+ }
11
+
12
+ export async function readRegistry() {
13
+ let list = [];
14
+ try {
15
+ const d = JSON.parse(await fs.readFile(registryPath(), 'utf8'));
16
+ list = Array.isArray(d) ? d : Array.isArray(d.nodes) ? d.nodes : [];
17
+ } catch {
18
+ /* no registry yet */
19
+ }
20
+ if (list.length) return list;
21
+ // 老版本插件的台账迁移一次
22
+ try {
23
+ const d = JSON.parse(await fs.readFile(path.join(stateDir(), 'nodes.json'), 'utf8'));
24
+ const old = Array.isArray(d.nodes) ? d.nodes : [];
25
+ list = old.map((n) => ({ dir: n.dir, name: n.name, port: n.port, source: 'record', createdAt: n.createdAt }));
26
+ } catch {
27
+ /* nothing to migrate */
28
+ }
29
+ return list;
30
+ }
31
+
32
+ export async function writeRegistry(nodes) {
33
+ await fs.mkdir(path.dirname(registryPath()), { recursive: true });
34
+ await fs.writeFile(registryPath(), JSON.stringify(nodes, null, 2));
35
+ }
36
+
37
+ /** 把探到的运行信息回写到磁盘台账(~/.contextos/nodes.json),供下次直接读取 */
38
+ export async function saveNodeInfo(nodes) {
39
+ const prev = await readRegistry();
40
+ const byDir = new Map(prev.filter((x) => x && x.dir).map((x) => [x.dir, x]));
41
+ const merged = nodes.map((n) => {
42
+ const old = byDir.get(n.dir) || {};
43
+ return {
44
+ ...old,
45
+ dir: n.dir, name: n.name, port: n.port, platform: n.platform, source: n.source,
46
+ version: n.version || old.version || '',
47
+ builtAt: n.builtAt || old.builtAt || '',
48
+ sizeText: n.sizeText || old.sizeText || '',
49
+ state: n.state || old.state || '',
50
+ updatedAt: new Date().toISOString(),
51
+ };
52
+ });
53
+ // 磁盘上还有但这次没扫到的记录保留下来,不丢台账
54
+ for (const [dir, rec] of byDir) if (!merged.some((m) => m.dir === dir)) merged.push(rec);
55
+ await fs.mkdir(path.dirname(registryPath()), { recursive: true });
56
+ await fs.writeFile(registryPath(), JSON.stringify({ updatedAt: new Date().toISOString(), nodes: merged }, null, 2));
57
+ return merged;
58
+ }
59
+
60
+ export async function refreshNodes() {
61
+ const nodes = await listNodes();
62
+ await saveNodeInfo(nodes).catch(() => null);
63
+ return nodes;
64
+ }
65
+
66
+ /** 从目录名 / ContextOS-<port> 尾部推端口;读 var/log/ice.port 优先。 */
67
+ export async function portOnDisk(dir) {
68
+ try {
69
+ const p = Number((await fs.readFile(path.join(dir, 'var', 'log', 'ice.port'), 'utf8')).trim());
70
+ if (p > 0 && p <= 65535) return p;
71
+ } catch {
72
+ /* fall through */
73
+ }
74
+ const m = /(\d+)$/.exec(path.basename(dir));
75
+ return m ? Number(m[1]) : 0;
76
+ }
77
+
78
+ /** 磁盘扫描:任何含 bin/ice.bin 的 ~/ContextOS* 目录都是一个节点(对齐 CtxDeployer.scanLocalInstalls)。 */
79
+ export async function scanLocalNodes() {
80
+ const out = [];
81
+ let subs = [];
82
+ try {
83
+ subs = await fs.readdir(HOME());
84
+ } catch {
85
+ return out;
86
+ }
87
+ for (const sub of subs) {
88
+ if (!/^ContextOS/.test(sub)) continue;
89
+ const dir = path.join(HOME(), sub);
90
+ let st;
91
+ try {
92
+ st = await fs.stat(path.join(dir, 'bin', 'ice.bin'));
93
+ if (!st.isFile()) continue;
94
+ } catch {
95
+ continue;
96
+ }
97
+ out.push({
98
+ dir,
99
+ bin: path.join(dir, 'bin', 'ice.bin'),
100
+ name: sub,
101
+ port: (await portOnDisk(dir)) || 9020,
102
+ source: 'scan',
103
+ platform: platformTriple(),
104
+ size: st.size,
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ export async function binExists(node) {
111
+ try {
112
+ return (await fs.stat(node.bin || path.join(node.dir, 'bin', 'ice.bin'))).isFile();
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export async function pidAlive(dir) {
119
+ try {
120
+ const pid = Number((await fs.readFile(path.join(dir, 'var', 'log', 'ice.pid'), 'utf8')).trim());
121
+ if (!(pid > 1)) return { alive: false, pid: 0 };
122
+ process.kill(pid, 0);
123
+ return { alive: true, pid };
124
+ } catch {
125
+ return { alive: false, pid: 0 };
126
+ }
127
+ }
128
+
129
+ export async function listNodes() {
130
+ const registry = await readRegistry();
131
+ const scanned = await scanLocalNodes();
132
+ const byDir = new Map();
133
+ for (const sc of scanned) byDir.set(sc.dir, { ...sc });
134
+ for (const rec of registry) {
135
+ if (!rec.dir) continue;
136
+ const base = byDir.get(rec.dir) || {
137
+ dir: rec.dir,
138
+ bin: path.join(rec.dir, 'bin', 'ice.bin'),
139
+ name: path.basename(rec.dir),
140
+ port: rec.port || 9020,
141
+ source: 'record',
142
+ platform: rec.platform || platformTriple(),
143
+ };
144
+ for (const k of ['name', 'port', 'parent', 'platform', 'source', 'container', 'image', 'resident', 'env', 'createdAt']) {
145
+ if (rec[k] !== undefined && rec[k] !== null && rec[k] !== '') base[k] = rec[k];
146
+ }
147
+ if (rec.source) base.source = rec.source;
148
+ if (!base.port) base.port = (await portOnDisk(rec.dir)) || 9020;
149
+ byDir.set(rec.dir, base);
150
+ }
151
+ const nodes = [...byDir.values()];
152
+ const out = await Promise.all(nodes.map(async (n) => {
153
+ const st = await probe(n.port, 1500);
154
+ const isContainer = !!n.container;
155
+ const hasBin = isContainer ? true : await binExists(n);
156
+ const pid = await pidAlive(n.dir);
157
+ const running = isContainer ? st.live : pid.pid > 1 ? pid.alive : st.live;
158
+ const iconFile = ['src/main.png', 'src/main.jpg', 'src/main.ico'].find((rel) => {
159
+ try { return fs.existsSync(path.join(n.dir, rel)); } catch { return false; }
160
+ });
161
+ const image = iconFile ? `http://127.0.0.1:${n.port}/p/${iconFile}?pod=${encodeURIComponent(n.name || '')}` : '';
162
+ // 版本/编译时间/大小统一从 /c/runtime 取(boot + make),拿不到才退回本地文件
163
+ const rt = st.runtime || {};
164
+ const versionText = String(st.version || '');
165
+ const ver = String(rt.version || (versionText.match(/v\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)+/) || [''])[0] || '');
166
+ let builtAt = String(rt.builtAt || '');
167
+ let sizeText = String(rt.sizeText || '');
168
+ if (!builtAt || !sizeText) {
169
+ try {
170
+ const bst = await fs.stat(path.join(n.dir, 'bin', 'ice.bin'));
171
+ if (!builtAt) builtAt = bst.mtime.toISOString();
172
+ if (!sizeText) sizeText = `${Math.round(bst.size / 1048576)}M`;
173
+ } catch { /* ignore */ }
174
+ }
175
+ return { ...n, ...st, version: ver, versionText, builtAt, sizeText, image, hasBin, pid: pid.pid, pidAlive: pid.alive, url: `http://localhost:${n.port}/`, state: running ? 'running' : isContainer || hasBin ? 'stopped' : 'missing' };
176
+ }));
177
+ // 排序:运行中优先,其次名称倒序(ContextOS-9191 在 ContextOS-9090 之前)
178
+ out.sort((a, b) => {
179
+ const ar = a.state === 'running', br = b.state === 'running';
180
+ if (ar !== br) return ar ? -1 : 1;
181
+ return String(b.name || '').localeCompare(String(a.name || ''), 'zh-CN', { numeric: true });
182
+ });
183
+ // 每次探测完顺手落盘,台账始终是最新的运行信息
184
+ await saveNodeInfo(out).catch(() => null);
185
+ return out;
186
+ }
@@ -0,0 +1,66 @@
1
+ // 基础功能:插件状态目录(数据文件清单/读取/旧目录迁移)。
2
+ import fs from 'node:fs/promises';
3
+ import fsp from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { stateDir } from './env.js';
7
+
8
+ let STATE_MIGRATED = false;
9
+
10
+ export async function listDataFiles() {
11
+ const dir = stateDir();
12
+ const out = [];
13
+ try {
14
+ const names = await fs.readdir(dir);
15
+ for (const name of names) {
16
+ try {
17
+ const st = await fs.stat(path.join(dir, name));
18
+ out.push({ name, size: st.size, mtime: st.mtime.toISOString() });
19
+ } catch { /* skip */ }
20
+ }
21
+ } catch { /* dir missing */ }
22
+ out.sort((a, b) => a.name.localeCompare(b.name));
23
+ return { dir, files: out };
24
+ }
25
+
26
+ export async function readDataFile({ name = '' } = {}) {
27
+ const safe = path.basename(String(name));
28
+ if (!safe || safe !== String(name) || !/^[A-Za-z0-9._-]+$/.test(safe)) throw new Error('invalid file name');
29
+ const full = path.join(stateDir(), safe);
30
+ const st = await fs.stat(full);
31
+ if (!st.isFile()) throw new Error('not a file');
32
+ if (st.size > 512 * 1024) throw new Error('file too large');
33
+ return { name: safe, size: st.size, content: await fs.readFile(full, 'utf8') };
34
+ }
35
+
36
+ // 改名兼容:state.json 里的 chat:/scroll: 键带着旧插件前缀(contextos-devops-dsh: 或更早的
37
+ // contextos-devops:)。不迁移的话,改名前保存的对话与滚动位置在新版本里读不到。
38
+ export function migrateStateKeys(data) {
39
+ if (!data || typeof data !== 'object') return data;
40
+ const LEGACY = ['contextos-devops-dsh:', 'contextos-devops:'];
41
+ const NEXT = 'dsh-contextos-devops:';
42
+ let moved = 0;
43
+ for (const key of Object.keys(data)) {
44
+ if (!key.startsWith('chat:') && !key.startsWith('scroll:')) continue;
45
+ for (const pre of LEGACY) {
46
+ if (!key.includes(':' + pre)) continue;
47
+ const next = key.replace(':' + pre, ':' + NEXT);
48
+ if (!(next in data)) { data[next] = data[key]; moved += 1; }
49
+ delete data[key];
50
+ break;
51
+ }
52
+ }
53
+ if (moved) data.migratedKeys = (Number(data.migratedKeys) || 0) + moved;
54
+ return data;
55
+ }
56
+
57
+ export async function migrateStateOnce() {
58
+ if (STATE_MIGRATED) return;
59
+ STATE_MIGRATED = true;
60
+ try {
61
+ const dir = stateDir();
62
+ const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
63
+ const old = path.join(home, 'contextos-devops');
64
+ if (old !== dir && fsp.existsSync(old) && !fsp.existsSync(dir)) await fs.rename(old, dir);
65
+ } catch { /* keep old */ }
66
+ }
@@ -0,0 +1,47 @@
1
+ // 基础功能:ContextOS 固定宽度表格解析。
2
+ export function decodeCell(v) {
3
+ if (typeof v !== 'string' || !v.includes('%')) return v;
4
+ try {
5
+ return decodeURIComponent(v.replace(/\+/g, ' '));
6
+ } catch {
7
+ return v;
8
+ }
9
+ }
10
+
11
+ /** Fixed-width table: use header column offsets, not a 2+ space split. */
12
+ export function parseFixedTable(text) {
13
+ const lines = String(text || '').split('\n').filter((l) => l.trim() !== '');
14
+ if (lines.length < 1) return { columns: [], rows: [] };
15
+ const header = lines[0];
16
+ const heads = [];
17
+ const starts = [];
18
+ for (const m of header.matchAll(/\S+/g)) {
19
+ heads.push(m[0]);
20
+ starts.push(m.index);
21
+ }
22
+ const rows = lines.slice(1).map((line) => {
23
+ const row = {};
24
+ heads.forEach((head, i) => {
25
+ const start = starts[i];
26
+ const end = i + 1 < starts.length ? starts[i + 1] : line.length;
27
+ row[head] = decodeCell(String(line.slice(start, end)).trim());
28
+ });
29
+ return row;
30
+ });
31
+ return { columns: heads, rows };
32
+ }
33
+
34
+ /** 空格分隔的表格(非固定宽度)。 */
35
+ export function parseTable(text) {
36
+ const lines = String(text || '').split('\n').filter((l) => l.trim() !== '');
37
+ if (lines.length === 0) return { columns: [], rows: [] };
38
+ const cut = (line) => line.trim().split(/\s{2,}/);
39
+ const columns = cut(lines[0]);
40
+ const rows = lines.slice(1).map((line) => {
41
+ const cells = cut(line);
42
+ const row = {};
43
+ columns.forEach((c, i) => { row[c] = decodeCell(cells[i] ?? ''); });
44
+ return row;
45
+ });
46
+ return { columns, rows };
47
+ }
@@ -0,0 +1,26 @@
1
+ // ContextOS 的系统级列表(/c/aaa.user 等)返回等宽文本表:
2
+ // time userrole username usernick
3
+ // 2026-11-14+10%3A48%3A09.375 tech dsh_copilot_9090 DSH Copilot
4
+ // 列为空格填充的固定宽度;这里按表头的列起始位置切片解析。
5
+ export function parseTextTable(text) {
6
+ const raw = String(text || '').replace(/\r/g, '');
7
+ const lines = raw.split('\n').filter((l) => l.trim() !== '');
8
+ if (lines.length < 1) return { columns: [], rows: [] };
9
+ const header = lines[0];
10
+ const starts = [];
11
+ for (let i = 0; i < header.length; i++) {
12
+ if (header[i] !== ' ' && (i === 0 || header[i - 1] === ' ')) starts.push(i);
13
+ }
14
+ const columns = starts.map((s, i) => header.slice(s, i + 1 < starts.length ? starts[i + 1] : header.length).trim());
15
+ const rows = lines.slice(1).map((line) => {
16
+ const row = {};
17
+ starts.forEach((s, i) => {
18
+ const end = i + 1 < starts.length ? starts[i + 1] : line.length;
19
+ let v = line.slice(s, end).trim();
20
+ try { v = decodeURIComponent(v.replace(/\+/g, ' ')); } catch { /* 原样保留 */ }
21
+ row[columns[i]] = v;
22
+ });
23
+ return row;
24
+ });
25
+ return { columns, rows };
26
+ }
@@ -0,0 +1,178 @@
1
+ // 两种 URL 严格区分,任何地方都不许再手拼:
2
+ // 接口 URL(读写数据) /s/<pod>/c/<portal>/action/run/<cmd>/action/<action>/<k>/<v>/...
3
+ // ?user.uid=<当前分身>&<place>_uid=<实例>&uid=<记录> ← uid 只用于改/删
4
+ // 网页 URL(浏览器打开) /s/<pod>/c/<portal>?debug=true&<place>_uid=<实例>#<实例>:<cmd>:<记录>
5
+ // 空间一律用名称 pod,绝不用空间 uid。
6
+ export const DEFAULT_PLACE = 'system';
7
+
8
+ export function portalOf(command) {
9
+ const parts = String(command || '').split('.');
10
+ return parts.length >= 3 ? `${parts.slice(0, -1).join('.')}.portal` : '';
11
+ }
12
+
13
+ export function placeUidKey(place) {
14
+ const p = String(place || DEFAULT_PLACE).replace(/_(uid|name)$/, '');
15
+ return `${p || DEFAULT_PLACE}_uid`;
16
+ }
17
+
18
+ // 接口 URL:seg 是已经编码好的 k/v 路径段,scope 是查询参数
19
+ export function apiPath({ pod, command, action = 'create', seg = '', scope = {}, portal }) {
20
+ const portalOn = portal === undefined ? portalOf(command) : portal;
21
+ const usePortal = !!portalOn;
22
+ const base = usePortal
23
+ ? `/s/${encodeURIComponent(pod)}/c/${portalOn}/action/run/${command}/action/${action}`
24
+ : `/s/${encodeURIComponent(pod)}/c/${command}/action/${action}`;
25
+ const query = new URLSearchParams();
26
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
27
+ const qs = query.toString();
28
+ return `${base}${seg ? `/${seg}` : ''}${qs ? `?${qs}` : ''}`;
29
+ }
30
+
31
+ // 平台数据接口(唯一形态,别自己拼):
32
+ // 写: /s/<pod>/c/<portal>/action/run/<cmd>/action/<action>/<k>/<v>…?user.uid=&<place>_uid=
33
+ // 列表: /s/<pod>/c/<portal>/action/run/<cmd>/<place_uid>?user.uid=
34
+ // 详情: /s/<pod>/c/<portal>/action/run/<cmd>/<place_uid>/<uid>?user.uid=
35
+ export function dataPath({ pod, command, action = 'create', seg = '', inst = '', record = '', portal, scope = {} }) {
36
+ const portalOn = portal === undefined ? portalOf(command) : portal;
37
+ if (!portalOn) throw new Error(`not a platform command: ${command}`);
38
+ const base = `/s/${encodeURIComponent(pod)}/c/${portalOn}/action/run/${command}`;
39
+ const query = new URLSearchParams();
40
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
41
+ const qs = query.toString();
42
+ const tail = qs ? `?${qs}` : '';
43
+ if (!action) {
44
+ const path = inst ? `/${encodeURIComponent(inst)}${record ? `/${encodeURIComponent(record)}` : ''}` : '';
45
+ return `${base}${path}${tail}`;
46
+ }
47
+ return `${base}/action/${action}${seg ? `/${seg}` : ''}${tail}`;
48
+ }
49
+
50
+ // 网页 URL:给浏览器打开某个实例(或某条记录)
51
+ export function webUrl({ node, pod, command, place = DEFAULT_PLACE, placeUid = '', recordUid = '' }) {
52
+ const portal = portalOf(command) || command;
53
+ const key = placeUidKey(place);
54
+ const hash = recordUid ? `#${placeUid || ''}:${command}:${recordUid}` : '';
55
+ return `http://127.0.0.1:${node}/s/${encodeURIComponent(pod)}/c/${portal}?debug=true&${key}=${encodeURIComponent(placeUid || '')}${hash}`;
56
+ }
57
+
58
+ // ── 两种接口层:系统级 / 平台级 ───────────────────────────────────────────
59
+ // 系统级:节点自身的命令,直连 /c/<cmd>;空间命令 /s/<pod>/c/<cmd>(不经过门户、不查成员权限)
60
+ // 平台级:应用平台(云社区/云办公…)把门户当入口,/s/<pod>/c/<portal>/action/run/<cmd>
61
+ // 批量命令走门户时会先检查成员权限(member/apply/allow)
62
+ export function systemUrl({ node, command, action = '', seg = '', scope = {} }) {
63
+ const base = `/c/${command}${action ? `/action/${action}` : ''}`;
64
+ const query = new URLSearchParams();
65
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
66
+ const qs = query.toString();
67
+ return `http://127.0.0.1:${node}${base}${seg ? `/${seg}` : ''}${qs ? `?${qs}` : ''}`;
68
+ }
69
+
70
+ export function spaceUrl({ node, pod, command, action = '', seg = '', scope = {} }) {
71
+ const base = `/s/${encodeURIComponent(pod)}/c/${command}${action ? `/action/${action}` : ''}`;
72
+ const query = new URLSearchParams();
73
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
74
+ const qs = query.toString();
75
+ return `http://127.0.0.1:${node}${base}${seg ? `/${seg}` : ''}${qs ? `?${qs}` : ''}`;
76
+ }
77
+
78
+ // 平台级:门户 + 内部命令(会做成员权限检查)
79
+ export function platformUrl({ node, pod, command, portal, action = '', seg = '', scope = {} }) {
80
+ const p = portal || portalOf(command);
81
+ const base = `/s/${encodeURIComponent(pod)}/c/${p}${command ? `/action/run/${command}` : ''}${action ? `/action/${action}` : ''}`;
82
+ const query = new URLSearchParams();
83
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
84
+ const qs = query.toString();
85
+ return `http://127.0.0.1:${node}${base}${seg ? `/${seg}` : ''}${qs ? `?${qs}` : ''}`;
86
+ }
87
+
88
+ // 五种使用形式:同一条命令,五个入口
89
+ // 参数三类:位置参数 arg(URL Path)、选项参数 opt(URL Query)、锚定参数 hash(URL Hash)
90
+ // 内部 CMD:本进程 m.Cmd("<cmd>", <arg>, map[string]string{<opt>})
91
+ // 其它空间 m.Cmd("space", "<pod>", "<cmd>", <arg>, map[string]string{<opt>})
92
+ // 命令 CLI:本进程 ./bin/ice.bin <cmd> <arg>;发给服务节点 ./bin/ice.bin admin <cmd> <arg>
93
+ export function goOpt(opt = {}) {
94
+ const pairs = Object.entries(opt || {}).map(([k, v]) => `"${k}": "${String(v).replace(/"/g, '\\"')}"`);
95
+ return pairs.length ? `map[string]string{${pairs.join(', ')}}` : '';
96
+ }
97
+
98
+ export function commandForms({ node, pod = '', command, portal = '', arg = '', opt = {}, hash = '' }) {
99
+ const a = String(arg || '');
100
+ const goArgs = [a ? `"${a}"` : '', goOpt(opt)].filter(Boolean).join(', ');
101
+ const goOpts = goArgs ? `, ${goArgs}` : '';
102
+ return {
103
+ web: (pod ? spaceUrl({ node, pod, command, seg: a, scope: opt }) : systemUrl({ node, command, seg: a, scope: opt })) + (hash ? `#${hash}` : ''),
104
+ api: pod ? spaceUrl({ node, pod, command, seg: a, scope: opt }) : systemUrl({ node, command, seg: a, scope: opt }),
105
+ platform: pod && portal ? platformUrl({ node, pod, command, portal, seg: a, scope: opt }) : '',
106
+ inner: pod ? `m.Cmd("space", "${pod}", "${command}"${goOpts})` : `m.Cmd("${command}"${goOpts})`,
107
+ cli: `./bin/ice.bin ${command}${a ? ` ${a}` : ''}`,
108
+ cliAdmin: `./bin/ice.bin admin ${command}${a ? ` ${a}` : ''}`,
109
+ mcp: `mcp://contextos/${command}${pod ? `?pod=${pod}` : ''}`,
110
+ };
111
+ }
112
+
113
+ // 参数三类与 URL 的对应关系
114
+ export function paramKinds({ arg = '', opt = {}, hash = '' }) {
115
+ return {
116
+ arg: { url: 'Path', value: arg },
117
+ opt: { url: 'Query', value: opt },
118
+ hash: { url: 'Hash', value: hash },
119
+ };
120
+ }
121
+
122
+ // ── 方法语义(严格按 URL 结构判断,不许凭感觉选) ─────────────────────────
123
+ // 最后一段命令后面没有 /action/ → GET
124
+ // 有 /action/ 且 action 是 create 或以 Create 结尾 → PUT
125
+ // 有 /action/ 且 action 是 remove/trash 或以 Remove 结尾 → DELETE
126
+ // 其它 → POST
127
+ export function methodOf({ action = '' } = {}) {
128
+ const a = String(action || '');
129
+ if (!a) return 'GET';
130
+ if (a === 'create' || /Create$/.test(a)) return 'PUT';
131
+ if (a === 'remove' || a === 'trash' || /Remove$/.test(a)) return 'DELETE';
132
+ return 'POST';
133
+ }
134
+
135
+ export function methodOfUrl(url = '') {
136
+ const m = String(url).match(/\/action\/([^/?#]+)/);
137
+ return methodOf({ action: m ? m[1] : '' });
138
+ }
139
+
140
+ // ── 文件上传:平台级接口 ────────────────────────────────────────────────
141
+ // POST /s/<pod>/c/<portal>/action/run/<cmd>/action/upload (multipart/form-data)
142
+ // 门户先查成员权限,再转给内部命令的 upload
143
+ export function uploadUrl({ node, pod = '', command, portal = '', scope = {} }) {
144
+ const p = portal || portalOf(command);
145
+ const base = pod && p
146
+ ? `/s/${encodeURIComponent(pod)}/c/${p}/action/run/${command}/action/upload`
147
+ : `/c/${command}/action/upload`;
148
+ const query = new URLSearchParams();
149
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
150
+ const qs = query.toString();
151
+ return `http://127.0.0.1:${node}${base}${qs ? `?${qs}` : ''}`;
152
+ }
153
+
154
+ // ── 网页转发:web.chat.action ────────────────────────────────────────────
155
+ // 前端页面把命令串交给 web.chat.action,由它按 cmds 转发到真实命令。
156
+ // POST /chat/action/?_index=<cmd>
157
+ // form-data: cmds=action&cmds=run&cmds=<portal>&cmds=action&cmds=run&cmds=<cmd>&cmds=action&cmds=<action>
158
+ export const CHAT_ACTION = 'web.chat.action';
159
+
160
+ export function chatActionUrl({ node, index = '', scope = {} }) {
161
+ const query = new URLSearchParams();
162
+ if (index) query.set('_index', index);
163
+ for (const [k, v] of Object.entries(scope || {})) if (v !== '' && v != null) query.set(k, v);
164
+ const qs = query.toString();
165
+ return `http://127.0.0.1:${node}/chat/action/${qs ? `?${qs}` : ''}`;
166
+ }
167
+
168
+ // cmds 链:门户 → 内部命令 → 动作
169
+ export function cmdsChain({ portal = '', command = '', action = '' }) {
170
+ const out = [];
171
+ if (portal) out.push('action', 'run', portal);
172
+ if (command) out.push('action', 'run', command);
173
+ if (action) out.push('action', action);
174
+ return out;
175
+ }
176
+
177
+ // 全局参数:系统按用户语言与主题返回数据
178
+ export const GLOBAL_PARAMS = ['user.lang', 'sess.theme', 'sess.bg', 'sess.fg', 'sess.daemon'];
@@ -0,0 +1,17 @@
1
+ # lib/core · 核心业务逻辑
2
+
3
+ 引用方向:`core → base → misc`(core、base 可引用 misc;misc 不引用任何内部模块)。
4
+
5
+ | 文件 | 职责 |
6
+ |---|---|
7
+ | `nodes.js` | 节点:部署/启停/打开/终端/删除/官方空间/容器与远程部署/流水线 |
8
+ | `spaces.js` | 空间:创建/启动/停止/编译/初始化/官方空间/路径 |
9
+ | `apps.js` | 应用:列表/生成/代码/实例/商店/最近应用 |
10
+ | `files.js` | 文件:列目录/读写/树/新建/改名/删除/git 状态 |
11
+ | `data.js` | 数据:读/写/删(回读校验) |
12
+ | `users.js` | 用户:每节点 `dsh_copilot_<port>`,uid 持久化 |
13
+ | `project.js` | 项目管理:target/plan/task/report/feedback |
14
+ | `avatar.js` | 数字分身/背景/用户媒体 |
15
+ | `copilot.js` | AI Copilot:模型解析/对话/动作提取/目标拆解 |
16
+ | `device.js` | 周边设备发现(节点/WiFi/蓝牙/局域网) |
17
+ | `terminal.js` | 内嵌终端(WebSocket + PTY) |