dsh-contextos-devops 0.0.0-stage → 1.48.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -2
- package/assets/avatar.jpg +0 -0
- package/assets/client.css +3316 -0
- package/assets/icon-180.png +0 -0
- package/build/client.mjs +64 -0
- package/client.js +7622 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +15 -0
- package/index.js +345 -0
- package/lib/base/env.js +22 -0
- package/lib/base/field.js +25 -0
- package/lib/base/http.js +140 -0
- package/lib/base/platform.js +33 -0
- package/lib/base/registry.js +186 -0
- package/lib/base/state.js +66 -0
- package/lib/base/table.js +47 -0
- package/lib/base/texttable.js +26 -0
- package/lib/base/url.js +178 -0
- package/lib/core/README.md +17 -0
- package/lib/core/apps.js +675 -0
- package/lib/core/auth.js +110 -0
- package/lib/core/avatar.js +289 -0
- package/lib/core/backup.js +105 -0
- package/lib/core/codegen.js +401 -0
- package/lib/core/copilot.js +419 -0
- package/lib/core/data.js +157 -0
- package/lib/core/device.js +148 -0
- package/lib/core/files.js +152 -0
- package/lib/core/gobuild.js +63 -0
- package/lib/core/iam.js +122 -0
- package/lib/core/identity.js +22 -0
- package/lib/core/nodes.js +627 -0
- package/lib/core/ops.js +306 -0
- package/lib/core/project.js +393 -0
- package/lib/core/spaces.js +406 -0
- package/lib/core/terminal.js +137 -0
- package/lib/core/terminal2.js +141 -0
- package/lib/core/users.js +51 -0
- package/lib/core/vocab.js +158 -0
- package/lib/misc/async.js +15 -0
- package/lib/misc/shell.js +27 -0
- package/lib/misc/static.js +33 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +91 -4
- package/src/client/README.md +37 -0
- package/src/client/parts/00-prelude.js +315 -0
- package/src/client/parts/05-bootstrap.js +61 -0
- package/src/client/parts/08-store.js +85 -0
- package/src/client/parts/10-ui.js +188 -0
- package/src/client/parts/12-debug.js +361 -0
- package/src/client/parts/15-layout.js +86 -0
- package/src/client/parts/20-devworkspace.js +789 -0
- package/src/client/parts/25-editor-ui.js +28 -0
- package/src/client/parts/30-modals.js +85 -0
- package/src/client/parts/32-cards.js +176 -0
- package/src/client/parts/34-forms.js +176 -0
- package/src/client/parts/36-data.js +267 -0
- package/src/client/parts/40-project.js +430 -0
- package/src/client/parts/45-map.js +109 -0
- package/src/client/parts/46-pm.js +158 -0
- package/src/client/parts/47-three.js +324 -0
- package/src/client/parts/49-msg.js +167 -0
- package/src/client/parts/50-chat.js +1422 -0
- package/src/client/parts/51-pmselection.js +31 -0
- package/src/client/parts/52-pmdata.js +29 -0
- package/src/client/parts/55-pm.js +146 -0
- package/src/client/parts/58-config.js +381 -0
- package/src/client/parts/59-monitor.js +262 -0
- package/src/client/parts/60-page.js +1518 -0
- package/src/client/styles/kit.css +20 -0
- package/src/client/styles/main.css +3294 -0
- package/src/client/styles/tokens.css +2 -0
- package/vendor/leaflet/leaflet.css +661 -0
- package/vendor/leaflet/leaflet.js +6 -0
- package/vendor/three/three.module.js +53044 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +218 -0
- package/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// 核心业务:内嵌终端(WebSocket + Python pty.fork 真 PTY)。
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import fsp from 'node:fs';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { ptySupported } from '../base/platform.js';
|
|
7
|
+
|
|
8
|
+
// ---- embedded terminal (WebSocket + PTY via /usr/bin/script) ----
|
|
9
|
+
function wsAccept(key) {
|
|
10
|
+
return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
|
|
11
|
+
}
|
|
12
|
+
function wsFrame(data) {
|
|
13
|
+
const payload = Buffer.isBuffer(data) ? data : Buffer.from(String(data), 'utf8');
|
|
14
|
+
const len = payload.length;
|
|
15
|
+
let header;
|
|
16
|
+
if (len < 126) header = Buffer.from([0x81, len]);
|
|
17
|
+
else if (len < 65536) header = Buffer.from([0x81, 126, (len >> 8) & 0xff, len & 0xff]);
|
|
18
|
+
else {
|
|
19
|
+
header = Buffer.alloc(10);
|
|
20
|
+
header[0] = 0x81; header[1] = 127;
|
|
21
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
22
|
+
}
|
|
23
|
+
return Buffer.concat([header, payload]);
|
|
24
|
+
}
|
|
25
|
+
const PTY_HELPER = `
|
|
26
|
+
import os, sys, pty, select, fcntl, termios, struct, signal
|
|
27
|
+
def main():
|
|
28
|
+
cwd = sys.argv[1]
|
|
29
|
+
shell = sys.argv[2] if len(sys.argv) > 2 else (os.environ.get('SHELL') or '/bin/zsh')
|
|
30
|
+
os.chdir(cwd)
|
|
31
|
+
pid, fd = pty.fork()
|
|
32
|
+
if pid == 0:
|
|
33
|
+
os.environ['TERM'] = 'xterm-256color'
|
|
34
|
+
os.execvp(shell, [shell])
|
|
35
|
+
def nonblock(f):
|
|
36
|
+
fcntl.fcntl(f, fcntl.F_SETFL, fcntl.fcntl(f, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
37
|
+
nonblock(fd); nonblock(0)
|
|
38
|
+
while True:
|
|
39
|
+
r, _, _ = select.select([fd, 0], [], [])
|
|
40
|
+
if fd in r:
|
|
41
|
+
try:
|
|
42
|
+
data = os.read(fd, 65536)
|
|
43
|
+
if not data: break
|
|
44
|
+
os.write(1, data)
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
if 0 in r:
|
|
48
|
+
try:
|
|
49
|
+
data = os.read(0, 65536)
|
|
50
|
+
if not data: break
|
|
51
|
+
if data.startswith(b'\\x00CTX_RESIZE '):
|
|
52
|
+
try:
|
|
53
|
+
cols, rows = data.decode().strip().split(' ')[1:3]
|
|
54
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', int(rows), int(cols), 0, 0))
|
|
55
|
+
os.kill(pid, signal.SIGWINCH)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
else:
|
|
59
|
+
os.write(fd, data)
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
62
|
+
main()
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
export function terminalUpgrade(req, socket, head) {
|
|
66
|
+
const key = req.headers['sec-websocket-key'];
|
|
67
|
+
if (!key) { socket.destroy(); return; }
|
|
68
|
+
if (!ptySupported) {
|
|
69
|
+
// Windows 无 pty.fork:明确拒绝,客户端会回落为「用 DSH 自带终端」。
|
|
70
|
+
socket.write('HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n');
|
|
71
|
+
socket.end();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
socket.write(
|
|
75
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
76
|
+
'Upgrade: websocket\r\n' +
|
|
77
|
+
'Connection: Upgrade\r\n' +
|
|
78
|
+
`Sec-WebSocket-Accept: ${wsAccept(key)}\r\n\r\n`,
|
|
79
|
+
);
|
|
80
|
+
let cwd = os.homedir();
|
|
81
|
+
try {
|
|
82
|
+
const p = new URL(req.url || '/', 'http://x').searchParams.get('cwd');
|
|
83
|
+
if (p) cwd = decodeURIComponent(p);
|
|
84
|
+
} catch { /* keep home */ }
|
|
85
|
+
if (!fsp.existsSync(cwd)) cwd = os.homedir();
|
|
86
|
+
|
|
87
|
+
let shell = process.env.SHELL || '/bin/zsh';
|
|
88
|
+
try {
|
|
89
|
+
const q = new URL(req.url || '/', 'http://x').searchParams.get('shell');
|
|
90
|
+
if (q && /^[a-zA-Z0-9_\/.-]+$/.test(q)) shell = q.startsWith('/') ? q : `/bin/${q}`;
|
|
91
|
+
} catch {}
|
|
92
|
+
const python = process.env.PYTHON || 'python3';
|
|
93
|
+
const child = spawn(python, ['-c', PTY_HELPER, cwd, shell], {
|
|
94
|
+
env: { ...process.env, PROMPT_EOL_MARK: '', TERM: 'xterm-256color' }, stdio: ['pipe', 'pipe', 'pipe'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let buf = Buffer.alloc(0);
|
|
98
|
+
const feed = (chunk) => {
|
|
99
|
+
buf = Buffer.concat([buf, chunk]);
|
|
100
|
+
while (buf.length >= 2) {
|
|
101
|
+
const b0 = buf[0], b1 = buf[1];
|
|
102
|
+
const opcode = b0 & 0x0f;
|
|
103
|
+
const masked = (b1 & 0x80) !== 0;
|
|
104
|
+
let len = b1 & 0x7f, off = 2;
|
|
105
|
+
if (len === 126) { if (buf.length < 4) return; len = buf.readUInt16BE(2); off = 4; }
|
|
106
|
+
else if (len === 127) { if (buf.length < 10) return; len = Number(buf.readBigUInt64BE(2)); off = 10; }
|
|
107
|
+
let mask;
|
|
108
|
+
if (masked) { if (buf.length < off + 4) return; mask = buf.subarray(off, off + 4); off += 4; }
|
|
109
|
+
if (buf.length < off + len) return;
|
|
110
|
+
const payload = Buffer.from(buf.subarray(off, off + len));
|
|
111
|
+
buf = buf.subarray(off + len);
|
|
112
|
+
if (masked) for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i & 3];
|
|
113
|
+
if (opcode === 0x8) { try { child.kill(); } catch {} return; }
|
|
114
|
+
if (opcode === 0x9) { socket.write(wsFrame(Buffer.concat([Buffer.from([0x8a, 0]), payload]))); continue; }
|
|
115
|
+
if (opcode === 0x2 || opcode === 0x1) { try { child.stdin.write(payload); } catch {} }
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
let rxN = 0, ptyAcc = Buffer.alloc(0), ptyTimer = 0;
|
|
119
|
+
const flushPty = () => {
|
|
120
|
+
ptyTimer = 0;
|
|
121
|
+
const raw = ptyAcc.toString('utf8');
|
|
122
|
+
ptyAcc = Buffer.alloc(0);
|
|
123
|
+
// zsh PROMPT_SP:清行标记,某些终端渲染会把它显示成乱码,直接丢弃
|
|
124
|
+
const clean = raw.replace(/\x1b\[1m\x1b\[7m%\x1b\[27m\x1b\[1m\x1b\[0m *\r \r/g, '');
|
|
125
|
+
if (!clean) return;
|
|
126
|
+
if (rxN < 3) {
|
|
127
|
+
rxN++;
|
|
128
|
+
try { fsp.appendFileSync('/tmp/cx-term-rx.log', `--- chunk ${rxN} len=${clean.length} text=${JSON.stringify(clean.slice(0, 120))}\n`); } catch {}
|
|
129
|
+
}
|
|
130
|
+
try { socket.write(wsFrame(Buffer.from(clean, 'utf8'))); } catch {}
|
|
131
|
+
};
|
|
132
|
+
child.stdout.on('data', (d) => { ptyAcc = Buffer.concat([ptyAcc, d]); if (!ptyTimer) ptyTimer = setTimeout(flushPty, 30); });
|
|
133
|
+
child.stderr.on('data', (d) => { try { socket.write(wsFrame(d)); } catch {} });
|
|
134
|
+
child.on('exit', () => { try { socket.end(); } catch {} });
|
|
135
|
+
socket.on('data', feed);
|
|
136
|
+
socket.on('error', () => { try { child.kill(); } catch {} });
|
|
137
|
+
socket.on('close', () => { try { child.kill(); } catch {} });
|
|
138
|
+
if (head && head.length) feed(head);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 核心业务:DSH 用户(每个节点一个 dsh_copilot_<port>,uid 持久化)。
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { stateDir } from '../base/env.js';
|
|
5
|
+
import { http, encode, USER_CACHE } from '../base/http.js';
|
|
6
|
+
|
|
7
|
+
let USERS_LOADED = false;
|
|
8
|
+
const PENDING = new Map();
|
|
9
|
+
|
|
10
|
+
export function usersFile() {
|
|
11
|
+
return path.join(stateDir(), 'users.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadUsers() {
|
|
15
|
+
if (USERS_LOADED) return;
|
|
16
|
+
USERS_LOADED = true;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(await fs.readFile(usersFile(), 'utf8'));
|
|
19
|
+
for (const [port, value] of Object.entries(raw || {})) {
|
|
20
|
+
if (value && value.uid) USER_CACHE.set(Number(port), value);
|
|
21
|
+
}
|
|
22
|
+
} catch { /* first run */ }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function saveUsers() {
|
|
26
|
+
await fs.mkdir(stateDir(), { recursive: true });
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [port, value] of USER_CACHE.entries()) out[String(port)] = value;
|
|
29
|
+
await fs.writeFile(usersFile(), JSON.stringify(out, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function ensureUser(node) {
|
|
33
|
+
const port = Number(node);
|
|
34
|
+
if (!port) throw new Error('node is required');
|
|
35
|
+
await loadUsers();
|
|
36
|
+
const found = USER_CACHE.get(port);
|
|
37
|
+
if (found?.uid) return found.uid;
|
|
38
|
+
if (PENDING.has(port)) return PENDING.get(port);
|
|
39
|
+
const job = (async () => {
|
|
40
|
+
const username = `dsh_copilot_${port}`;
|
|
41
|
+
const r = await http(port, `/c/aaa.user/action/create/userrole/tech/username/${encode(username)}/usernick/${encode('DSH Copilot')}`, { method: 'PUT', timeout: 15000 });
|
|
42
|
+
if (r.status !== 200) throw new Error(`create user failed: HTTP ${r.status}`);
|
|
43
|
+
const uid = r.text.trim();
|
|
44
|
+
if (!/^[0-9a-f]{32}$/i.test(uid)) throw new Error(`create user returned no uid: ${r.text.slice(0, 120)}`);
|
|
45
|
+
USER_CACHE.set(port, { uid, username, created: new Date().toISOString() });
|
|
46
|
+
await saveUsers();
|
|
47
|
+
return uid;
|
|
48
|
+
})().finally(() => PENDING.delete(port));
|
|
49
|
+
PENDING.set(port, job);
|
|
50
|
+
return job;
|
|
51
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// 框架通用词表:字段名与操作名只用这些,不自定义。
|
|
2
|
+
// 来源:usr/icebergs/base/mdb/mdb.go、usr/icebergs/base/aaa/aaa.go、平台 model。
|
|
3
|
+
export const FIELDS = {
|
|
4
|
+
uid: '记录主键', id: '自增编号', time: '时间', name: '名称', title: '标题', text: '文本',
|
|
5
|
+
content: '内容', type: '类型', status: '状态', order: '排序', score: '分数', group: '分组',
|
|
6
|
+
valid: '有效', enable: '启用', member: '成员', expire: '过期', icon: '图标', help: '说明',
|
|
7
|
+
user_uid: '用户', place_uid: '实例', project_uid: '项目', target_uid: '目标', plan_uid: '计划',
|
|
8
|
+
task_uid: '任务', report_uid: '报告', feedback_uid: '反馈', image: '图片', link: '链接',
|
|
9
|
+
begin_time: '开始时间', end_time: '结束时间', process_time: '处理时间', finished_time: '完成时间',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ACTIONS = {
|
|
13
|
+
create: '创建', insert: '插入', modify: '修改', update: '更新', remove: '删除', delete: '删除',
|
|
14
|
+
select: '查询', list: '列表', detail: '详情', export: '导出', import: '导入', enable: '启用',
|
|
15
|
+
disable: '停用', prunes: '清理',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 中文 → 框架字段名
|
|
19
|
+
export const FIELD_ALIAS = {
|
|
20
|
+
名称: 'name', 名字: 'name', 标题: 'title', 内容: 'content', 说明: 'text', 备注: 'text', 描述: 'text',
|
|
21
|
+
类型: 'type', 状态: 'status', 顺序: 'order', 排序: 'order', 分数: 'score', 分组: 'group',
|
|
22
|
+
用户: 'user_uid', 成员: 'member', 时间: 'time', 图片: 'image', 链接: 'link',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 中文动作 → 框架动作名
|
|
26
|
+
export const ACTION_ALIAS = {
|
|
27
|
+
创建: 'create', 新建: 'create', 新增: 'create', 添加: 'create',
|
|
28
|
+
修改: 'modify', 编辑: 'modify', 更新: 'update',
|
|
29
|
+
删除: 'remove', 移除: 'remove',
|
|
30
|
+
查询: 'select', 查看: 'select', 列表: 'list', 详情: 'detail',
|
|
31
|
+
导出: 'export', 导入: 'import', 启用: 'enable', 停用: 'disable',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const ROLES = {
|
|
35
|
+
root: '系统角色', tech: '技术人员', void: '普通', creator: '创建人', leader: '负责人',
|
|
36
|
+
worker: '执行人', member: '成员', visitor: '访客', server: '服务',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const STYLES = ['guiding', 'notice', 'danger', 'primary', 'success', 'warning'];
|
|
40
|
+
|
|
41
|
+
export function fieldOf(zh) {
|
|
42
|
+
const k = String(zh || '').trim();
|
|
43
|
+
return FIELD_ALIAS[k] || (FIELDS[k] ? k : '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function actionOf(zh) {
|
|
47
|
+
const k = String(zh || '').trim();
|
|
48
|
+
return ACTION_ALIAS[k] || (ACTIONS[k] ? k : '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function checkField(name) {
|
|
52
|
+
const n = String(name || '').trim();
|
|
53
|
+
if (FIELDS[n]) return { ok: true, name: n, help: FIELDS[n] };
|
|
54
|
+
const alt = fieldOf(n);
|
|
55
|
+
if (alt) return { ok: true, name: alt, help: FIELDS[alt], renamed: alt !== n };
|
|
56
|
+
return { ok: false, name: n, suggest: Object.keys(FIELDS).join(', ') };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function checkAction(name) {
|
|
60
|
+
const n = String(name || '').trim().toLowerCase();
|
|
61
|
+
if (ACTIONS[n]) return { ok: true, name: n, label: ACTIONS[n] };
|
|
62
|
+
const alt = actionOf(n);
|
|
63
|
+
if (alt) return { ok: true, name: alt, label: ACTIONS[alt], renamed: alt !== n };
|
|
64
|
+
return { ok: false, name: n, suggest: Object.keys(ACTIONS).join(', ') };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 生成声明标签:name / help / icon / role / style
|
|
68
|
+
export function declAttrs({ name = '', params = '', help = '', icon = '', role = '', style = '' } = {}) {
|
|
69
|
+
const out = [];
|
|
70
|
+
out.push(`name:"${[name, params].filter(Boolean).join(' ')}"`);
|
|
71
|
+
if (help) out.push(`help:"${help}"`);
|
|
72
|
+
if (icon) out.push(`icon:"${icon}"`);
|
|
73
|
+
if (role) out.push(`role:"${role}"`);
|
|
74
|
+
if (style) out.push(`style:"${style}"`);
|
|
75
|
+
return out.join(' ');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 参数声明文法(usr/icebergs/info.go SplitCmd):
|
|
79
|
+
// name*(:text|textarea|select|button|password)=value@action
|
|
80
|
+
// - name 后跟 * 表示必填(need:must)
|
|
81
|
+
// - :type 指定类型;select 的候选值用逗号分隔写进 =value
|
|
82
|
+
// - @action 绑定按钮动作(create/modify/remove/img/date…)
|
|
83
|
+
// - 后缀自动推导:_info/_content → textarea;_uid/_type/_status、type/status → select;
|
|
84
|
+
// _image → text+img;_time → select+date
|
|
85
|
+
export const PARAM_TYPES = ['text', 'textarea', 'select', 'button', 'password', 'img', 'video', 'date'];
|
|
86
|
+
|
|
87
|
+
export function autoType(name) {
|
|
88
|
+
const n = String(name || '');
|
|
89
|
+
if (/_info$|_content$|^content$|^text$/.test(n)) return 'textarea';
|
|
90
|
+
if (/_uid$|_type$|_status$|^type$|^status$/.test(n)) return 'select';
|
|
91
|
+
if (/_image$|^image$/.test(n)) return 'img';
|
|
92
|
+
if (/_time$/.test(n)) return 'date';
|
|
93
|
+
return 'text';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function paramOf({ name, type = '', required = false, value = '', action = '' } = {}) {
|
|
97
|
+
const n = String(name || '').trim();
|
|
98
|
+
if (!n) return '';
|
|
99
|
+
const t = type || autoType(n);
|
|
100
|
+
let s = n + (required ? '*' : '');
|
|
101
|
+
if (t && t !== 'text') s += `:${t}`;
|
|
102
|
+
if (value) s += `=${value}`;
|
|
103
|
+
if (action) s += `@${action}`;
|
|
104
|
+
return s;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function paramSpec(list = []) {
|
|
108
|
+
return list.map((x) => (typeof x === 'string' ? paramOf({ name: x, required: /^[a-z_]+$/.test(x) && x !== 'content' }) : paramOf(x))).join(' ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 模块常用函数(框架约定,顺序固定)
|
|
112
|
+
export const MODULE_FUNCS = [
|
|
113
|
+
{ name: 'Init', sig: 'func (s %s) Init(m *ice.Message, arg ...string) { s.Hash.Init(m, arg...) }', rank: 0, help: '启动时加载(注册数据、初始化缓存)' },
|
|
114
|
+
{ name: 'Exit', sig: 'func (s %s) Exit(m *ice.Message, arg ...string) { s.Hash.Exit(m, arg...) }', rank: 1, help: '停止时卸载(关闭句柄、落盘)' },
|
|
115
|
+
{ name: 'Inputs', sig: 'func (s %s) Inputs(m *ice.Message, arg ...string) { s.Hash.Inputs(m, arg...) }', rank: 2, help: '参数补全(前端联动取值)' },
|
|
116
|
+
{ name: 'Create', sig: 'func (s %s) Create(m *ice.Message, arg ...string) { s.ValueCreate(m, arg...) }', rank: 3, help: '新增' },
|
|
117
|
+
{ name: 'Modify', sig: 'func (s %s) Modify(m *ice.Message, arg ...string) { s.ValueModify(m, arg...) }', rank: 4, help: '修改' },
|
|
118
|
+
{ name: 'Remove', sig: 'func (s %s) Remove(m *ice.Message, arg ...string) { s.ValueRemove(m, arg...) }', rank: 5, help: '删除' },
|
|
119
|
+
{ name: 'List', sig: 'func (s %s) List(m *ice.Message, arg ...string) *ice.Message { s.Select(m, arg...); return m }', rank: 7, help: '列表' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// 自定义动作落在 Remove 之后、List 之前
|
|
123
|
+
export function funcRank(name) {
|
|
124
|
+
const hit = MODULE_FUNCS.find((f) => f.name === String(name));
|
|
125
|
+
return hit ? hit.rank : 6;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function moduleSkeleton(mod = 'note', cls = 'system') {
|
|
129
|
+
const base = cls === 'platform' ? 'Table' : 'Hash';
|
|
130
|
+
const body = MODULE_FUNCS.map((f) => f.sig.replace('%s', mod).replace(/s\.Hash\./g, `s.${base}.`)).join('\n');
|
|
131
|
+
return `${body}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 权限检查:接口声明 role 之外,函数内还可自查
|
|
135
|
+
// 系统角色:m.IsTech()(tech/root,等价 aaa.IsTechOrRoot(m))
|
|
136
|
+
// 成员角色:s.IsCreator / s.IsLeader / s.IsWorker(model.USER_ROLE 0..3)
|
|
137
|
+
export const ROLE_LEVEL = { visitor: 0, creator: 1, leader: 2, worker: 3 };
|
|
138
|
+
|
|
139
|
+
export function guardOf(role) {
|
|
140
|
+
const r = String(role || '').trim();
|
|
141
|
+
if (!r) return '';
|
|
142
|
+
if (r === 'root' || r === 'tech') return 'if m.WarnNotRight(!m.IsTech()) { return }';
|
|
143
|
+
if (r === 'creator') return 'if m.WarnNotRight(!s.IsCreator(m)) { return }';
|
|
144
|
+
if (r === 'leader') return 'if m.WarnNotRight(!s.IsLeader(m)) { return }';
|
|
145
|
+
if (r === 'worker') return 'if m.WarnNotRight(!s.IsWorker(m)) { return }';
|
|
146
|
+
if (r === 'member' || r === 'visitor') return 'if m.WarnNotRight(!s.IsWorker(m)) { return }';
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 从中文里识别角色
|
|
151
|
+
export function roleOf(text = '') {
|
|
152
|
+
const t = String(text);
|
|
153
|
+
if (/(系统管理员|管理员|运维|技术)/.test(t)) return 'tech';
|
|
154
|
+
if (/(负责人|主管|经理|领导)/.test(t)) return 'leader';
|
|
155
|
+
if (/(创建人|发起人)/.test(t)) return 'creator';
|
|
156
|
+
if (/(执行人|员工|经办人|操作员)/.test(t)) return 'worker';
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 外部辅助:通用异步等待。
|
|
2
|
+
export async function waitFor(fn, { timeout = 40000, interval = 700, label = 'condition' } = {}) {
|
|
3
|
+
const started = Date.now();
|
|
4
|
+
let last;
|
|
5
|
+
while (Date.now() - started < timeout) {
|
|
6
|
+
try {
|
|
7
|
+
last = await fn();
|
|
8
|
+
if (last) return last;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
last = error;
|
|
11
|
+
}
|
|
12
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`timeout waiting for ${label}`);
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 外部辅助工具(叶子层):进程执行与平台 shell 参数。misc 不 import base/core。
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
export const isWin = process.platform === 'win32';
|
|
5
|
+
|
|
6
|
+
/** shell 执行参数:Unix 用 /bin/sh -c,Windows 用 cmd /c。 */
|
|
7
|
+
export function shellArgs(script) {
|
|
8
|
+
return isWin ? ['cmd.exe', ['/d', '/s', '/c', script]] : ['/bin/sh', ['-c', script]];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function runShell(script, cwd, { timeoutMs = 900000 } = {}) {
|
|
12
|
+
const [cmd, args] = shellArgs(script);
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const child = spawn(cmd, args, { cwd, env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
15
|
+
let out = '';
|
|
16
|
+
let err = '';
|
|
17
|
+
child.stdout.on('data', (d) => { out += d; if (out.length > 65536) out = out.slice(-32768); });
|
|
18
|
+
child.stderr.on('data', (d) => { err += d; if (err.length > 65536) err = err.slice(-32768); });
|
|
19
|
+
const t = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} reject(new Error('shell timeout')); }, timeoutMs);
|
|
20
|
+
child.on('error', (e) => { clearTimeout(t); reject(e); });
|
|
21
|
+
child.on('close', (code) => { clearTimeout(t); resolve({ code, out, err }); });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function spawnDetached(cmd, args) {
|
|
26
|
+
try { spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref(); } catch { /* 打不开就静默 */ }
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 外部辅助:静态资源前缀路由(monaco/xterm/three/leaflet)。
|
|
2
|
+
import fsp from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const STATIC_MIME = {
|
|
6
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
7
|
+
'.css': 'text/css; charset=utf-8',
|
|
8
|
+
'.json': 'application/json; charset=utf-8',
|
|
9
|
+
'.ttf': 'font/ttf',
|
|
10
|
+
'.woff': 'font/woff',
|
|
11
|
+
'.woff2': 'font/woff2',
|
|
12
|
+
'.svg': 'image/svg+xml',
|
|
13
|
+
'.map': 'application/json',
|
|
14
|
+
'.html': 'text/html; charset=utf-8',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const makeStatic = (prefix, root) => (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const pathname = decodeURIComponent(new URL(req.url || '/', 'http://x').pathname);
|
|
21
|
+
const rel = pathname.slice(prefix.length).replace(/^\/+/, '');
|
|
22
|
+
const file = path.normalize(path.join(root, rel));
|
|
23
|
+
if (file !== root && !file.startsWith(root + path.sep)) { res.writeHead(403); res.end(); return; }
|
|
24
|
+
const data = fsp.readFileSync(file);
|
|
25
|
+
const ext = path.extname(file).toLowerCase();
|
|
26
|
+
res.writeHead(200, { 'content-type': STATIC_MIME[ext] || 'application/octet-stream', 'cache-control': 'public, max-age=3600' });
|
|
27
|
+
res.end(data);
|
|
28
|
+
} catch {
|
|
29
|
+
res.writeHead(404);
|
|
30
|
+
res.end();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "ContextOS DevOps",
|
|
4
|
+
"description": "Deploy ContextOS nodes, create project spaces and apps, and read/write app records from one panel."
|
|
5
|
+
},
|
|
6
|
+
"contextosDevops": {
|
|
7
|
+
"title": "ContextOS DevOps",
|
|
8
|
+
"subtitle": "Nodes · Spaces · Apps · Data"
|
|
9
|
+
}
|
|
10
|
+
}
|
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-contextos-devops",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.48.31",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "ContextOS DevOps for DeepSeek Harness: one dashboard for cluster nodes, project spaces, apps, avatars, PM boards and IoT devices, with an in-app Copilot that can deploy and scaffold.",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./client": "./client.js",
|
|
9
|
+
"./locale/*.json": "./locale/*.json",
|
|
10
|
+
"./icon": "./icon.svg"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"client.js",
|
|
15
|
+
"lib/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"locale/*.json",
|
|
18
|
+
"icon.svg",
|
|
19
|
+
"README.md",
|
|
20
|
+
"vendor/xterm/*",
|
|
21
|
+
"vendor/three/*",
|
|
22
|
+
"vendor/leaflet/*",
|
|
23
|
+
"src/client/**",
|
|
24
|
+
"build/**",
|
|
25
|
+
"assets/**",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"assets/icon-180.png"
|
|
28
|
+
],
|
|
29
|
+
"dsh": {
|
|
30
|
+
"bundle": {
|
|
31
|
+
"patch": "./cordis.patch.yml"
|
|
32
|
+
},
|
|
33
|
+
"client": {
|
|
34
|
+
"platform": "web",
|
|
35
|
+
"immediately": true,
|
|
36
|
+
"inject": [
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build:client": "node build/client.mjs && node scripts/gate.mjs && node scripts/load-check.mjs && node scripts/hook-order-check.mjs && node scripts/op-check.mjs && node scripts/store-check.mjs && node scripts/ui-check.mjs && node scripts/ui-probe.mjs --soft && node scripts/i18n-check.mjs && node scripts/context-check.mjs && node scripts/docs-check.mjs && node scripts/docs-budget.mjs --check && node scripts/publish-check.mjs",
|
|
44
|
+
"watch:client": "node build/client.mjs --watch",
|
|
45
|
+
"check:ui": "node scripts/ui-check.mjs",
|
|
46
|
+
"check:probe": "node scripts/ui-probe.mjs",
|
|
47
|
+
"check:context": "node scripts/context-check.mjs",
|
|
48
|
+
"verify:factory": "node scripts/accept-factory.mjs",
|
|
49
|
+
"check:i18n": "node scripts/i18n-check.mjs",
|
|
50
|
+
"gate": "node scripts/gate.mjs",
|
|
51
|
+
"verify:ui": "node scripts/ui-verify.mjs",
|
|
52
|
+
"release": "npm run build:client && node scripts/ui-verify.mjs",
|
|
53
|
+
"check:hooks": "node scripts/hook-order-check.mjs",
|
|
54
|
+
"check:ops": "node scripts/op-check.mjs",
|
|
55
|
+
"check:store": "node scripts/store-check.mjs",
|
|
56
|
+
"check:docs": "node scripts/docs-check.mjs",
|
|
57
|
+
"check:load": "node scripts/load-check.mjs",
|
|
58
|
+
"stats:ledger": "node scripts/ledger-stats.mjs",
|
|
59
|
+
"check:budget": "node scripts/docs-budget.mjs --check",
|
|
60
|
+
"push:all": "node scripts/push-all.mjs",
|
|
61
|
+
"check:publish": "node scripts/publish-check.mjs",
|
|
62
|
+
"publish:npm": "node scripts/publish-npm.mjs"
|
|
63
|
+
},
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"author": {
|
|
66
|
+
"name": "shy",
|
|
67
|
+
"url": "https://shylinux.com"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/contextos-devops/dsh-contextos-devops#readme",
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "git+https://github.com/contextos-devops/dsh-contextos-devops.git"
|
|
73
|
+
},
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/contextos-devops/dsh-contextos-devops/issues"
|
|
76
|
+
},
|
|
77
|
+
"keywords": [
|
|
78
|
+
"dsh",
|
|
79
|
+
"dsh-plugin",
|
|
80
|
+
"deepseek",
|
|
81
|
+
"deepseek-harness",
|
|
82
|
+
"contextos",
|
|
83
|
+
"devops",
|
|
84
|
+
"deploy",
|
|
85
|
+
"copilot",
|
|
86
|
+
"cluster",
|
|
87
|
+
"project-management"
|
|
88
|
+
],
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=20",
|
|
91
|
+
"dsh": ">=0.2.2-rc.1 <1.0.0"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Frontend source layout
|
|
2
|
+
|
|
3
|
+
`client.js` is generated by `build/client.mjs`.
|
|
4
|
+
|
|
5
|
+
Source fragments intentionally share the lexical scope of the DSH
|
|
6
|
+
`window.__ModuleLoader__.load({ factory(require) { ... } })` factory. They are
|
|
7
|
+
not standalone ES modules yet.
|
|
8
|
+
|
|
9
|
+
## Fragments (build order)
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
parts/00-prelude.js ModuleLoader shell, constants, icons, translations
|
|
13
|
+
parts/05-bootstrap.js api(), events, timeouts
|
|
14
|
+
parts/10-ui.js Panel/Btn/Field/Tabs/EmptyState
|
|
15
|
+
parts/20-devworkspace.js Monaco, xterm, file tree, Git, preview
|
|
16
|
+
parts/30-modals.js DropSelect/MenuButton/Modal/Scene/AvatarEdit
|
|
17
|
+
parts/32-cards.js NodeCard/SpaceCard/ItemIcon/AppRow/StoreRow
|
|
18
|
+
parts/34-forms.js Deploy/Space/App/Code/Instance/StoreInstance
|
|
19
|
+
parts/36-data.js Avatar/Device/Data modals
|
|
20
|
+
parts/40-project.js InitOverlay, PmCalendar, MultiFilter
|
|
21
|
+
parts/45-map.js Leaflet task map
|
|
22
|
+
parts/46-pm.js PM form, RichText
|
|
23
|
+
parts/47-three.js ThreeAvatar, ThreeCity, TechBackdrop
|
|
24
|
+
parts/50-chat.js ChatPanel and action execution
|
|
25
|
+
parts/60-page.js Page, Boundary, plugin registration
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
|
|
30
|
+
- Edit files in `src/client/parts/` and `src/client/styles/`.
|
|
31
|
+
- Do not edit `client.js` directly.
|
|
32
|
+
- Keep fragments in build order; later fragments can use earlier declarations.
|
|
33
|
+
- CSS order is `main.css` then `kit.css`.
|
|
34
|
+
- Build: `npm run build:client`.
|
|
35
|
+
- Validate: `node --check client.js`.
|
|
36
|
+
- The default avatar is served from `assets/avatar.jpg`, not embedded in JS.
|
|
37
|
+
- Styles are built into `assets/client.css` and loaded via a cached `<link>`; only critical shell CSS stays inline.
|