@yeaft/webchat-agent 1.0.14 → 1.0.16
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/package.json +1 -1
- package/terminal.js +14 -2
- package/yeaft/systemd-scope.js +88 -0
- package/yeaft/tasks/manager.js +3 -0
- package/yeaft/tasks/shell-runner.js +11 -3
- package/yeaft/tools/bash.js +9 -2
package/package.json
CHANGED
package/terminal.js
CHANGED
|
@@ -3,6 +3,8 @@ import { existsSync, chmodSync, statSync } from 'fs';
|
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import ctx from './context.js';
|
|
6
|
+
import { getRuntimePlatformInfo } from './yeaft/runtime-platform.js';
|
|
7
|
+
import { wrapInvocationInSystemdUserScope } from './yeaft/systemd-scope.js';
|
|
6
8
|
|
|
7
9
|
// Package name of the PTY backend. We use the Homebridge prebuilt fork
|
|
8
10
|
// because upstream node-pty ships no Linux prebuilds and falls back to
|
|
@@ -91,12 +93,22 @@ export async function handleTerminalCreate(msg) {
|
|
|
91
93
|
? `${process.env.SystemRoot || 'C:\\Windows'}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
|
|
92
94
|
: (process.env.COMSPEC || 'cmd.exe')))
|
|
93
95
|
: (process.env.SHELL || 'bash');
|
|
94
|
-
const
|
|
96
|
+
const terminalEnv = { ...process.env };
|
|
97
|
+
const terminalInvocation = wrapInvocationInSystemdUserScope(
|
|
98
|
+
{ command: shell, args: [], family: platform() === 'win32' ? 'powershell' : 'posix' },
|
|
99
|
+
{
|
|
100
|
+
runtimePlatform: getRuntimePlatformInfo(),
|
|
101
|
+
env: terminalEnv,
|
|
102
|
+
scopeId: `terminal-${terminalId}`,
|
|
103
|
+
scopePrefix: 'yeaft-terminal',
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
const ptyProcess = pty.spawn(terminalInvocation.command, terminalInvocation.args || [], {
|
|
95
107
|
name: 'xterm-256color',
|
|
96
108
|
cols: cols || 80,
|
|
97
109
|
rows: rows || 24,
|
|
98
110
|
cwd: workDir,
|
|
99
|
-
env:
|
|
111
|
+
env: terminalEnv
|
|
100
112
|
});
|
|
101
113
|
|
|
102
114
|
// 输出缓冲 - 每 16ms 批量发送
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* systemd-scope.js — run child processes outside the agent service cgroup.
|
|
3
|
+
*
|
|
4
|
+
* When yeaft-agent runs as a systemd user service, shell tasks inherit the
|
|
5
|
+
* yeaft-agent.service cgroup by default. Long-lived commands then show up as
|
|
6
|
+
* "left-over process" entries every time the agent service restarts. Wrapping
|
|
7
|
+
* shell commands in a transient user scope keeps those user workloads alive
|
|
8
|
+
* without polluting the agent service lifecycle.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { delimiter, isAbsolute, join } from 'path';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SCOPE_PREFIX = 'yeaft-shell';
|
|
15
|
+
const UNIT_MAX_LENGTH = 180;
|
|
16
|
+
|
|
17
|
+
function hasPathSeparator(command) {
|
|
18
|
+
return command.includes('/') || command.includes('\\');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function findExecutableOnPath(command, env = process.env) {
|
|
22
|
+
if (!command || typeof command !== 'string') return null;
|
|
23
|
+
if (hasPathSeparator(command)) return existsSync(command) ? command : null;
|
|
24
|
+
|
|
25
|
+
const pathValue = env.PATH || '';
|
|
26
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
27
|
+
if (!dir) continue;
|
|
28
|
+
const candidate = isAbsolute(dir) ? join(dir, command) : join(process.cwd(), dir, command);
|
|
29
|
+
if (existsSync(candidate)) return candidate;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shouldUseSystemdUserScope({ runtimePlatform, env = process.env, systemdRunPath = null } = {}) {
|
|
35
|
+
if (!runtimePlatform?.isLinux) return false;
|
|
36
|
+
if (env.YEAFT_DISABLE_SYSTEMD_SCOPE === '1') return false;
|
|
37
|
+
|
|
38
|
+
// INVOCATION_ID is set for systemd services and transient scopes. XDG_RUNTIME_DIR
|
|
39
|
+
// is required for `systemd-run --user` to talk to the user manager.
|
|
40
|
+
if (!env.INVOCATION_ID || !env.XDG_RUNTIME_DIR) return false;
|
|
41
|
+
|
|
42
|
+
const resolvedSystemdRun = systemdRunPath || findExecutableOnPath('systemd-run', env);
|
|
43
|
+
return !!resolvedSystemdRun;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sanitizeSystemdUnitPart(value) {
|
|
47
|
+
const raw = String(value || '').trim() || `${Date.now()}-${process.pid}`;
|
|
48
|
+
return raw
|
|
49
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '-')
|
|
50
|
+
.replace(/^-+|-+$/g, '')
|
|
51
|
+
.slice(0, UNIT_MAX_LENGTH) || `${Date.now()}-${process.pid}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildSystemdScopeName(scopeId, prefix = DEFAULT_SCOPE_PREFIX) {
|
|
55
|
+
const safePrefix = sanitizeSystemdUnitPart(prefix).slice(0, 48);
|
|
56
|
+
const safeId = sanitizeSystemdUnitPart(scopeId);
|
|
57
|
+
const base = `${safePrefix}-${safeId}`.slice(0, UNIT_MAX_LENGTH);
|
|
58
|
+
return base.endsWith('.scope') ? base : `${base}.scope`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function wrapInvocationInSystemdUserScope(invocation, {
|
|
62
|
+
runtimePlatform,
|
|
63
|
+
env = process.env,
|
|
64
|
+
scopeId = null,
|
|
65
|
+
scopePrefix = DEFAULT_SCOPE_PREFIX,
|
|
66
|
+
systemdRunPath = null,
|
|
67
|
+
} = {}) {
|
|
68
|
+
if (!shouldUseSystemdUserScope({ runtimePlatform, env, systemdRunPath })) {
|
|
69
|
+
return { ...invocation, systemdScope: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const scopeName = buildSystemdScopeName(scopeId, scopePrefix);
|
|
73
|
+
return {
|
|
74
|
+
command: systemdRunPath || findExecutableOnPath('systemd-run', env) || 'systemd-run',
|
|
75
|
+
args: [
|
|
76
|
+
'--user',
|
|
77
|
+
'--scope',
|
|
78
|
+
'--quiet',
|
|
79
|
+
'--collect',
|
|
80
|
+
`--unit=${scopeName}`,
|
|
81
|
+
invocation.command,
|
|
82
|
+
...(invocation.args || []),
|
|
83
|
+
],
|
|
84
|
+
family: invocation.family,
|
|
85
|
+
systemdScope: scopeName,
|
|
86
|
+
wrappedCommand: invocation.command,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/yeaft/tasks/manager.js
CHANGED
|
@@ -149,6 +149,7 @@ export class TaskManager {
|
|
|
149
149
|
command,
|
|
150
150
|
cwd,
|
|
151
151
|
pid: null,
|
|
152
|
+
systemdScope: null,
|
|
152
153
|
platform: (runtimePlatform || this.runtimePlatform)?.platform || process.platform,
|
|
153
154
|
},
|
|
154
155
|
log: {
|
|
@@ -170,6 +171,7 @@ export class TaskManager {
|
|
|
170
171
|
command,
|
|
171
172
|
cwd,
|
|
172
173
|
runtimePlatform: runtime,
|
|
174
|
+
scopeId: task.id,
|
|
173
175
|
onOutput: (stream, text) => {
|
|
174
176
|
const prefix = stream === 'stderr' ? '[stderr] ' : '';
|
|
175
177
|
this.store.appendLog(task.sessionId, task.id, prefix ? text.split(/(\n)/).map(part => part === '\n' ? part : (part ? `${prefix}${part}` : part)).join('') : text);
|
|
@@ -191,6 +193,7 @@ export class TaskManager {
|
|
|
191
193
|
});
|
|
192
194
|
|
|
193
195
|
task.runtime.pid = runner.pid;
|
|
196
|
+
task.runtime.systemdScope = runner.systemdScope || null;
|
|
194
197
|
this.processes.set(this.#key(task.sessionId, task.id), runner);
|
|
195
198
|
this.store.writeTask(task);
|
|
196
199
|
this.#emit('updated', task);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn, spawnSync } from 'child_process';
|
|
6
6
|
import { buildShellInvocation, getRuntimePlatformInfo } from '../runtime-platform.js';
|
|
7
|
+
import { wrapInvocationInSystemdUserScope } from '../systemd-scope.js';
|
|
7
8
|
|
|
8
9
|
export function buildWindowsTaskkillArgs(pid) {
|
|
9
10
|
return ['/pid', String(pid), '/t', '/f'];
|
|
@@ -33,12 +34,18 @@ export function killShellProcessTree(pid, runtimePlatform, signal = 'SIGTERM') {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export function startShellProcess({ command, cwd, runtimePlatform, onOutput, onExit, onError }) {
|
|
37
|
+
export function startShellProcess({ command, cwd, runtimePlatform, scopeId = null, onOutput, onExit, onError }) {
|
|
37
38
|
const platform = runtimePlatform || getRuntimePlatformInfo();
|
|
38
|
-
const
|
|
39
|
+
const env = { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' };
|
|
40
|
+
const baseInvocation = buildShellInvocation(command, { runtimePlatform: platform });
|
|
41
|
+
const invocation = wrapInvocationInSystemdUserScope(baseInvocation, {
|
|
42
|
+
runtimePlatform: platform,
|
|
43
|
+
env,
|
|
44
|
+
scopeId,
|
|
45
|
+
});
|
|
39
46
|
const proc = spawn(invocation.command, invocation.args, {
|
|
40
47
|
cwd,
|
|
41
|
-
env
|
|
48
|
+
env,
|
|
42
49
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
50
|
detached: !platform.isWindows,
|
|
44
51
|
windowsHide: true,
|
|
@@ -57,6 +64,7 @@ export function startShellProcess({ command, cwd, runtimePlatform, onOutput, onE
|
|
|
57
64
|
|
|
58
65
|
return {
|
|
59
66
|
pid: proc.pid || null,
|
|
67
|
+
systemdScope: invocation.systemdScope || null,
|
|
60
68
|
kill(signal = 'SIGTERM') {
|
|
61
69
|
return killShellProcessTree(proc.pid, platform, signal);
|
|
62
70
|
},
|
package/yeaft/tools/bash.js
CHANGED
|
@@ -13,6 +13,7 @@ import { spawn } from 'child_process';
|
|
|
13
13
|
import { existsSync } from 'fs';
|
|
14
14
|
import { resolve } from 'path';
|
|
15
15
|
import { buildShellInvocation, getRuntimePlatformInfo } from '../runtime-platform.js';
|
|
16
|
+
import { wrapInvocationInSystemdUserScope } from '../systemd-scope.js';
|
|
16
17
|
|
|
17
18
|
export { buildShellInvocation };
|
|
18
19
|
|
|
@@ -32,10 +33,16 @@ const MAX_TIMEOUT_MS = 600_000;
|
|
|
32
33
|
function runCommand(command, { cwd, timeout, signal, runtimePlatform }) {
|
|
33
34
|
return new Promise((resolve) => {
|
|
34
35
|
const platform = runtimePlatform || getRuntimePlatformInfo();
|
|
35
|
-
const
|
|
36
|
+
const env = { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' };
|
|
37
|
+
const baseInvocation = buildShellInvocation(command, { runtimePlatform: platform });
|
|
38
|
+
const invocation = wrapInvocationInSystemdUserScope(baseInvocation, {
|
|
39
|
+
runtimePlatform: platform,
|
|
40
|
+
env,
|
|
41
|
+
scopeId: `foreground-${Date.now()}-${process.pid}`,
|
|
42
|
+
});
|
|
36
43
|
const proc = spawn(invocation.command, invocation.args, {
|
|
37
44
|
cwd,
|
|
38
|
-
env
|
|
45
|
+
env,
|
|
39
46
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
40
47
|
detached: !platform.isWindows,
|
|
41
48
|
});
|