dsh-deeptutor 0.1.2 → 0.1.4
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/README.md +169 -136
- package/README.zh.md +125 -107
- package/cordis.patch.yml +17 -17
- package/package.json +60 -56
- package/scripts/install-profile.mjs +198 -0
- package/scripts/md-to-html.js +353 -353
- package/src/cli-exec.ts +251 -251
- package/src/config.ts +70 -70
- package/src/html-render.ts +53 -53
- package/src/http-api.ts +83 -83
- package/src/index.ts +449 -449
- package/src/turn.ts +174 -174
package/src/cli-exec.ts
CHANGED
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DeepTutor bridge: CLI execution (local process or SSH), including the notebook
|
|
3
|
-
* archiver's remote/local script plumbing.
|
|
4
|
-
*/
|
|
5
|
-
import { execFile, spawn } from "node:child_process";
|
|
6
|
-
import { rmSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { DEEPTUTOR_BIN, DEEPTUTOR_HOME, LOCAL_BIN, SSH_HOST } from "./config.ts";
|
|
10
|
-
|
|
11
|
-
export interface CliResult {
|
|
12
|
-
stdout: string;
|
|
13
|
-
stderr: string;
|
|
14
|
-
code: number;
|
|
15
|
-
killed: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Remote runner: reads argv from a JSON file and executes without a shell (avoids all quoting/escaping).
|
|
19
|
-
const PY_RUNNER = [
|
|
20
|
-
"import json, subprocess, sys",
|
|
21
|
-
"a = json.load(open(sys.argv[1]))",
|
|
22
|
-
"r = subprocess.run(a['argv'], capture_output=True, text=True, cwd=a.get('cwd'))",
|
|
23
|
-
"sys.stdout.write(r.stdout)",
|
|
24
|
-
"if r.stderr.strip():",
|
|
25
|
-
" sys.stdout.write('__DT_STDERR__' + r.stderr[-3000:])",
|
|
26
|
-
"sys.exit(0 if r.returncode == 0 else 1)",
|
|
27
|
-
].join("\n");
|
|
28
|
-
|
|
29
|
-
// Remote notebook archiver (CLI fallback): ensure notebook exists → write temp md → add-md.
|
|
30
|
-
// NOTE: add-md takes the notebook ID as its first arg (passing a name silently "succeeds" without writing).
|
|
31
|
-
const PY_NOTE = [
|
|
32
|
-
"import json, subprocess, sys",
|
|
33
|
-
"a = json.load(open(sys.argv[1]))",
|
|
34
|
-
"dt = a['dt']",
|
|
35
|
-
"nb_id = None",
|
|
36
|
-
"r = subprocess.run([dt, 'notebook', 'list'], capture_output=True, text=True)",
|
|
37
|
-
"for line in r.stdout.splitlines():",
|
|
38
|
-
" parts = line.split('│')",
|
|
39
|
-
" if len(parts) >= 3 and parts[2].strip() == a['notebook']:",
|
|
40
|
-
" nb_id = parts[1].strip()",
|
|
41
|
-
" break",
|
|
42
|
-
"if not nb_id:",
|
|
43
|
-
" r = subprocess.run([dt, 'notebook', 'create', a['notebook'], '--description', a['description']], capture_output=True, text=True)",
|
|
44
|
-
" try:",
|
|
45
|
-
" nb_id = json.loads(r.stdout)['id']",
|
|
46
|
-
" except Exception:",
|
|
47
|
-
" sys.stdout.write(r.stdout)",
|
|
48
|
-
" sys.exit(1)",
|
|
49
|
-
"open(a['path'], 'w').write(a['content'])",
|
|
50
|
-
"r = subprocess.run([dt, 'notebook', 'add-md', nb_id, a['path'], '--title', a['title'], '--type', a['type']], capture_output=True, text=True)",
|
|
51
|
-
"sys.stdout.write(r.stdout)",
|
|
52
|
-
"if r.stderr.strip():",
|
|
53
|
-
" sys.stdout.write('__DT_STDERR__' + r.stderr[-2000:])",
|
|
54
|
-
"sys.exit(0 if r.returncode == 0 else 1)",
|
|
55
|
-
].join("\n");
|
|
56
|
-
|
|
57
|
-
const b64 = (s: string) => Buffer.from(s, "utf8").toString("base64");
|
|
58
|
-
|
|
59
|
-
/** Timeout for each notebook-archiver CLI step (list / create / add-md). */
|
|
60
|
-
const NOTE_STEP_TIMEOUT_MS = 30_000;
|
|
61
|
-
|
|
62
|
-
/** Exec one command with optional abort + timeout, resolving instead of throwing. */
|
|
63
|
-
function execFileResolved(
|
|
64
|
-
file: string,
|
|
65
|
-
args: string[],
|
|
66
|
-
opts: { signal?: AbortSignal; timeoutMs?: number; env?: Record<string, string> } = {},
|
|
67
|
-
): Promise<CliResult> {
|
|
68
|
-
return new Promise((resolve) => {
|
|
69
|
-
const child = execFile(
|
|
70
|
-
file,
|
|
71
|
-
args,
|
|
72
|
-
{
|
|
73
|
-
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
74
|
-
windowsHide: true,
|
|
75
|
-
timeout: opts.timeoutMs,
|
|
76
|
-
},
|
|
77
|
-
(err, stdout, stderr) => {
|
|
78
|
-
const e: any = err;
|
|
79
|
-
resolve({
|
|
80
|
-
stdout: stdout ?? "",
|
|
81
|
-
stderr: stderr ?? "",
|
|
82
|
-
code: e ? (typeof e.code === "number" ? e.code : e.code === "ENOENT" ? 127 : 1) : 0,
|
|
83
|
-
killed: e?.killed ?? false,
|
|
84
|
-
});
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
opts.signal?.addEventListener(
|
|
88
|
-
"abort",
|
|
89
|
-
() => {
|
|
90
|
-
if (process.platform === "win32") {
|
|
91
|
-
// child.kill() on Windows only kills the shell, not the process tree
|
|
92
|
-
// (deeptutor CLI is a Python app; its subprocesses would linger)
|
|
93
|
-
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
94
|
-
} else {
|
|
95
|
-
child.kill();
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
{ once: true },
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function sshExec(
|
|
104
|
-
argv: string[],
|
|
105
|
-
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
106
|
-
): Promise<CliResult> {
|
|
107
|
-
const payload = JSON.stringify({ argv, cwd: DEEPTUTOR_HOME });
|
|
108
|
-
const script = [
|
|
109
|
-
`export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
|
|
110
|
-
"export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
|
|
111
|
-
"F=/tmp/dt_pi_$$",
|
|
112
|
-
`echo '${b64(payload)}' | base64 -d > "$F.json"`,
|
|
113
|
-
`echo '${b64(PY_RUNNER)}' | base64 -d > "$F.py"`,
|
|
114
|
-
`python3 "$F.py" "$F.json"`,
|
|
115
|
-
"rc=$?",
|
|
116
|
-
'rm -f "$F.json" "$F.py"',
|
|
117
|
-
"exit $rc",
|
|
118
|
-
].join("\n");
|
|
119
|
-
const remote = `echo '${b64(script)}' | base64 -d | bash`;
|
|
120
|
-
const res = await execFileResolved(
|
|
121
|
-
"ssh",
|
|
122
|
-
["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST, remote],
|
|
123
|
-
{ signal: opts.signal, timeoutMs: opts.timeoutMs ?? 600_000 },
|
|
124
|
-
);
|
|
125
|
-
const out = res.stdout ?? "";
|
|
126
|
-
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
127
|
-
return {
|
|
128
|
-
stdout: stdout ?? "",
|
|
129
|
-
stderr: (stderr ?? "").trim(),
|
|
130
|
-
code: res.code ?? -1,
|
|
131
|
-
killed: res.killed,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Unified CLI entry:
|
|
137
|
-
* - Remote deployment (SSH_HOST set) → execute on the server over SSH;
|
|
138
|
-
* - Local deployment (SSH_HOST empty) → execute DEEPTUTOR_LOCAL_BIN locally.
|
|
139
|
-
* args are deeptutor subcommand args (without the program name).
|
|
140
|
-
*/
|
|
141
|
-
export async function runCli(
|
|
142
|
-
args: string[],
|
|
143
|
-
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
144
|
-
): Promise<CliResult> {
|
|
145
|
-
if (SSH_HOST) {
|
|
146
|
-
return sshExec([DEEPTUTOR_BIN, ...args], opts);
|
|
147
|
-
}
|
|
148
|
-
// Local mode: execute directly (force UTF-8 output to avoid Windows cp1252 console crashes)
|
|
149
|
-
const res = await execFileResolved(
|
|
150
|
-
LOCAL_BIN,
|
|
151
|
-
args,
|
|
152
|
-
{ signal: opts.signal, timeoutMs: opts.timeoutMs, env: { PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" } },
|
|
153
|
-
);
|
|
154
|
-
const out = res.stdout ?? "";
|
|
155
|
-
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
156
|
-
return {
|
|
157
|
-
stdout: stdout ?? "",
|
|
158
|
-
stderr: (stderr ?? "").trim(),
|
|
159
|
-
code: res.code,
|
|
160
|
-
killed: res.killed,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Local-deployment notebook archiver: list → parse ID → create → add-md */
|
|
165
|
-
export async function cliNoteLocal(
|
|
166
|
-
nbName: string,
|
|
167
|
-
title: string,
|
|
168
|
-
recordType: string,
|
|
169
|
-
content: string,
|
|
170
|
-
signal?: AbortSignal,
|
|
171
|
-
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
172
|
-
const list = await runCli(["notebook", "list"], { signal, timeoutMs: NOTE_STEP_TIMEOUT_MS });
|
|
173
|
-
let nbId: string | undefined;
|
|
174
|
-
for (const line of (list.stdout ?? "").split("\n")) {
|
|
175
|
-
const parts = line.split("│");
|
|
176
|
-
if (parts.length >= 3 && parts[2].trim() === nbName) {
|
|
177
|
-
nbId = parts[1].trim();
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (!nbId) {
|
|
182
|
-
const created = await runCli(
|
|
183
|
-
[
|
|
184
|
-
"notebook",
|
|
185
|
-
"create",
|
|
186
|
-
nbName,
|
|
187
|
-
"--description",
|
|
188
|
-
`Archived by dsh agent (${new Date().toISOString().slice(0, 10)})`,
|
|
189
|
-
],
|
|
190
|
-
{ signal, timeoutMs: NOTE_STEP_TIMEOUT_MS },
|
|
191
|
-
);
|
|
192
|
-
let parsedId: unknown;
|
|
193
|
-
try {
|
|
194
|
-
parsedId = JSON.parse(created.stdout ?? "{}").id;
|
|
195
|
-
} catch {
|
|
196
|
-
return { stdout: created.stdout ?? "", stderr: created.stderr, code: created.code };
|
|
197
|
-
}
|
|
198
|
-
if (typeof parsedId !== "string") {
|
|
199
|
-
return { stdout: created.stdout ?? "", stderr: created.stderr, code: created.code };
|
|
200
|
-
}
|
|
201
|
-
nbId = parsedId;
|
|
202
|
-
}
|
|
203
|
-
const tmp = join(tmpdir(), `dt_pi_note_${Date.now()}.md`);
|
|
204
|
-
writeFileSync(tmp, content, "utf8");
|
|
205
|
-
const add = await runCli(
|
|
206
|
-
["notebook", "add-md", nbId, tmp, "--title", title, "--type", recordType],
|
|
207
|
-
{ signal, timeoutMs: NOTE_STEP_TIMEOUT_MS },
|
|
208
|
-
);
|
|
209
|
-
rmSync(tmp, { force: true });
|
|
210
|
-
return { stdout: add.stdout ?? "", stderr: add.stderr, code: add.code };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Remote-deployment notebook archiver (SSH CLI fallback): runs PY_NOTE on the server. */
|
|
214
|
-
export async function cliNoteRemote(
|
|
215
|
-
nbName: string,
|
|
216
|
-
title: string,
|
|
217
|
-
recordType: string,
|
|
218
|
-
content: string,
|
|
219
|
-
signal?: AbortSignal,
|
|
220
|
-
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
221
|
-
const path = `/tmp/dt_pi_note_${Date.now()}.md`;
|
|
222
|
-
const payload = JSON.stringify({
|
|
223
|
-
dt: DEEPTUTOR_BIN,
|
|
224
|
-
notebook: nbName,
|
|
225
|
-
description: `Archived by dsh agent (${new Date().toISOString().slice(0, 10)})`,
|
|
226
|
-
title,
|
|
227
|
-
type: recordType,
|
|
228
|
-
content,
|
|
229
|
-
path,
|
|
230
|
-
});
|
|
231
|
-
const script = [
|
|
232
|
-
`export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
|
|
233
|
-
"export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
|
|
234
|
-
"F=/tmp/dt_pi_note_$$",
|
|
235
|
-
`echo '${b64(payload)}' | base64 -d > "$F.json"`,
|
|
236
|
-
`echo '${b64(PY_NOTE)}' | base64 -d > "$F.py"`,
|
|
237
|
-
`python3 "$F.py" "$F.json"`,
|
|
238
|
-
"rc=$?",
|
|
239
|
-
'rm -f "$F.json" "$F.py"',
|
|
240
|
-
"exit $rc",
|
|
241
|
-
].join("\n");
|
|
242
|
-
const remote = `echo '${b64(script)}' | base64 -d | bash`;
|
|
243
|
-
const res = await execFileResolved(
|
|
244
|
-
"ssh",
|
|
245
|
-
["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST, remote],
|
|
246
|
-
{ signal, timeoutMs: 60_000 },
|
|
247
|
-
);
|
|
248
|
-
const out = res.stdout ?? "";
|
|
249
|
-
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
250
|
-
return { stdout: stdout ?? "", stderr: (stderr ?? "").trim(), code: res.code ?? -1 };
|
|
251
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: CLI execution (local process or SSH), including the notebook
|
|
3
|
+
* archiver's remote/local script plumbing.
|
|
4
|
+
*/
|
|
5
|
+
import { execFile, spawn } from "node:child_process";
|
|
6
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { DEEPTUTOR_BIN, DEEPTUTOR_HOME, LOCAL_BIN, SSH_HOST } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
export interface CliResult {
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
code: number;
|
|
15
|
+
killed: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Remote runner: reads argv from a JSON file and executes without a shell (avoids all quoting/escaping).
|
|
19
|
+
const PY_RUNNER = [
|
|
20
|
+
"import json, subprocess, sys",
|
|
21
|
+
"a = json.load(open(sys.argv[1]))",
|
|
22
|
+
"r = subprocess.run(a['argv'], capture_output=True, text=True, cwd=a.get('cwd'))",
|
|
23
|
+
"sys.stdout.write(r.stdout)",
|
|
24
|
+
"if r.stderr.strip():",
|
|
25
|
+
" sys.stdout.write('__DT_STDERR__' + r.stderr[-3000:])",
|
|
26
|
+
"sys.exit(0 if r.returncode == 0 else 1)",
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
// Remote notebook archiver (CLI fallback): ensure notebook exists → write temp md → add-md.
|
|
30
|
+
// NOTE: add-md takes the notebook ID as its first arg (passing a name silently "succeeds" without writing).
|
|
31
|
+
const PY_NOTE = [
|
|
32
|
+
"import json, subprocess, sys",
|
|
33
|
+
"a = json.load(open(sys.argv[1]))",
|
|
34
|
+
"dt = a['dt']",
|
|
35
|
+
"nb_id = None",
|
|
36
|
+
"r = subprocess.run([dt, 'notebook', 'list'], capture_output=True, text=True)",
|
|
37
|
+
"for line in r.stdout.splitlines():",
|
|
38
|
+
" parts = line.split('│')",
|
|
39
|
+
" if len(parts) >= 3 and parts[2].strip() == a['notebook']:",
|
|
40
|
+
" nb_id = parts[1].strip()",
|
|
41
|
+
" break",
|
|
42
|
+
"if not nb_id:",
|
|
43
|
+
" r = subprocess.run([dt, 'notebook', 'create', a['notebook'], '--description', a['description']], capture_output=True, text=True)",
|
|
44
|
+
" try:",
|
|
45
|
+
" nb_id = json.loads(r.stdout)['id']",
|
|
46
|
+
" except Exception:",
|
|
47
|
+
" sys.stdout.write(r.stdout)",
|
|
48
|
+
" sys.exit(1)",
|
|
49
|
+
"open(a['path'], 'w').write(a['content'])",
|
|
50
|
+
"r = subprocess.run([dt, 'notebook', 'add-md', nb_id, a['path'], '--title', a['title'], '--type', a['type']], capture_output=True, text=True)",
|
|
51
|
+
"sys.stdout.write(r.stdout)",
|
|
52
|
+
"if r.stderr.strip():",
|
|
53
|
+
" sys.stdout.write('__DT_STDERR__' + r.stderr[-2000:])",
|
|
54
|
+
"sys.exit(0 if r.returncode == 0 else 1)",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
const b64 = (s: string) => Buffer.from(s, "utf8").toString("base64");
|
|
58
|
+
|
|
59
|
+
/** Timeout for each notebook-archiver CLI step (list / create / add-md). */
|
|
60
|
+
const NOTE_STEP_TIMEOUT_MS = 30_000;
|
|
61
|
+
|
|
62
|
+
/** Exec one command with optional abort + timeout, resolving instead of throwing. */
|
|
63
|
+
function execFileResolved(
|
|
64
|
+
file: string,
|
|
65
|
+
args: string[],
|
|
66
|
+
opts: { signal?: AbortSignal; timeoutMs?: number; env?: Record<string, string> } = {},
|
|
67
|
+
): Promise<CliResult> {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const child = execFile(
|
|
70
|
+
file,
|
|
71
|
+
args,
|
|
72
|
+
{
|
|
73
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
timeout: opts.timeoutMs,
|
|
76
|
+
},
|
|
77
|
+
(err, stdout, stderr) => {
|
|
78
|
+
const e: any = err;
|
|
79
|
+
resolve({
|
|
80
|
+
stdout: stdout ?? "",
|
|
81
|
+
stderr: stderr ?? "",
|
|
82
|
+
code: e ? (typeof e.code === "number" ? e.code : e.code === "ENOENT" ? 127 : 1) : 0,
|
|
83
|
+
killed: e?.killed ?? false,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
opts.signal?.addEventListener(
|
|
88
|
+
"abort",
|
|
89
|
+
() => {
|
|
90
|
+
if (process.platform === "win32") {
|
|
91
|
+
// child.kill() on Windows only kills the shell, not the process tree
|
|
92
|
+
// (deeptutor CLI is a Python app; its subprocesses would linger)
|
|
93
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
94
|
+
} else {
|
|
95
|
+
child.kill();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{ once: true },
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function sshExec(
|
|
104
|
+
argv: string[],
|
|
105
|
+
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
106
|
+
): Promise<CliResult> {
|
|
107
|
+
const payload = JSON.stringify({ argv, cwd: DEEPTUTOR_HOME });
|
|
108
|
+
const script = [
|
|
109
|
+
`export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
|
|
110
|
+
"export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
|
|
111
|
+
"F=/tmp/dt_pi_$$",
|
|
112
|
+
`echo '${b64(payload)}' | base64 -d > "$F.json"`,
|
|
113
|
+
`echo '${b64(PY_RUNNER)}' | base64 -d > "$F.py"`,
|
|
114
|
+
`python3 "$F.py" "$F.json"`,
|
|
115
|
+
"rc=$?",
|
|
116
|
+
'rm -f "$F.json" "$F.py"',
|
|
117
|
+
"exit $rc",
|
|
118
|
+
].join("\n");
|
|
119
|
+
const remote = `echo '${b64(script)}' | base64 -d | bash`;
|
|
120
|
+
const res = await execFileResolved(
|
|
121
|
+
"ssh",
|
|
122
|
+
["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST, remote],
|
|
123
|
+
{ signal: opts.signal, timeoutMs: opts.timeoutMs ?? 600_000 },
|
|
124
|
+
);
|
|
125
|
+
const out = res.stdout ?? "";
|
|
126
|
+
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
127
|
+
return {
|
|
128
|
+
stdout: stdout ?? "",
|
|
129
|
+
stderr: (stderr ?? "").trim(),
|
|
130
|
+
code: res.code ?? -1,
|
|
131
|
+
killed: res.killed,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Unified CLI entry:
|
|
137
|
+
* - Remote deployment (SSH_HOST set) → execute on the server over SSH;
|
|
138
|
+
* - Local deployment (SSH_HOST empty) → execute DEEPTUTOR_LOCAL_BIN locally.
|
|
139
|
+
* args are deeptutor subcommand args (without the program name).
|
|
140
|
+
*/
|
|
141
|
+
export async function runCli(
|
|
142
|
+
args: string[],
|
|
143
|
+
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
144
|
+
): Promise<CliResult> {
|
|
145
|
+
if (SSH_HOST) {
|
|
146
|
+
return sshExec([DEEPTUTOR_BIN, ...args], opts);
|
|
147
|
+
}
|
|
148
|
+
// Local mode: execute directly (force UTF-8 output to avoid Windows cp1252 console crashes)
|
|
149
|
+
const res = await execFileResolved(
|
|
150
|
+
LOCAL_BIN,
|
|
151
|
+
args,
|
|
152
|
+
{ signal: opts.signal, timeoutMs: opts.timeoutMs, env: { PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" } },
|
|
153
|
+
);
|
|
154
|
+
const out = res.stdout ?? "";
|
|
155
|
+
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
156
|
+
return {
|
|
157
|
+
stdout: stdout ?? "",
|
|
158
|
+
stderr: (stderr ?? "").trim(),
|
|
159
|
+
code: res.code,
|
|
160
|
+
killed: res.killed,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Local-deployment notebook archiver: list → parse ID → create → add-md */
|
|
165
|
+
export async function cliNoteLocal(
|
|
166
|
+
nbName: string,
|
|
167
|
+
title: string,
|
|
168
|
+
recordType: string,
|
|
169
|
+
content: string,
|
|
170
|
+
signal?: AbortSignal,
|
|
171
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
172
|
+
const list = await runCli(["notebook", "list"], { signal, timeoutMs: NOTE_STEP_TIMEOUT_MS });
|
|
173
|
+
let nbId: string | undefined;
|
|
174
|
+
for (const line of (list.stdout ?? "").split("\n")) {
|
|
175
|
+
const parts = line.split("│");
|
|
176
|
+
if (parts.length >= 3 && parts[2].trim() === nbName) {
|
|
177
|
+
nbId = parts[1].trim();
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!nbId) {
|
|
182
|
+
const created = await runCli(
|
|
183
|
+
[
|
|
184
|
+
"notebook",
|
|
185
|
+
"create",
|
|
186
|
+
nbName,
|
|
187
|
+
"--description",
|
|
188
|
+
`Archived by dsh agent (${new Date().toISOString().slice(0, 10)})`,
|
|
189
|
+
],
|
|
190
|
+
{ signal, timeoutMs: NOTE_STEP_TIMEOUT_MS },
|
|
191
|
+
);
|
|
192
|
+
let parsedId: unknown;
|
|
193
|
+
try {
|
|
194
|
+
parsedId = JSON.parse(created.stdout ?? "{}").id;
|
|
195
|
+
} catch {
|
|
196
|
+
return { stdout: created.stdout ?? "", stderr: created.stderr, code: created.code };
|
|
197
|
+
}
|
|
198
|
+
if (typeof parsedId !== "string") {
|
|
199
|
+
return { stdout: created.stdout ?? "", stderr: created.stderr, code: created.code };
|
|
200
|
+
}
|
|
201
|
+
nbId = parsedId;
|
|
202
|
+
}
|
|
203
|
+
const tmp = join(tmpdir(), `dt_pi_note_${Date.now()}.md`);
|
|
204
|
+
writeFileSync(tmp, content, "utf8");
|
|
205
|
+
const add = await runCli(
|
|
206
|
+
["notebook", "add-md", nbId, tmp, "--title", title, "--type", recordType],
|
|
207
|
+
{ signal, timeoutMs: NOTE_STEP_TIMEOUT_MS },
|
|
208
|
+
);
|
|
209
|
+
rmSync(tmp, { force: true });
|
|
210
|
+
return { stdout: add.stdout ?? "", stderr: add.stderr, code: add.code };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Remote-deployment notebook archiver (SSH CLI fallback): runs PY_NOTE on the server. */
|
|
214
|
+
export async function cliNoteRemote(
|
|
215
|
+
nbName: string,
|
|
216
|
+
title: string,
|
|
217
|
+
recordType: string,
|
|
218
|
+
content: string,
|
|
219
|
+
signal?: AbortSignal,
|
|
220
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
221
|
+
const path = `/tmp/dt_pi_note_${Date.now()}.md`;
|
|
222
|
+
const payload = JSON.stringify({
|
|
223
|
+
dt: DEEPTUTOR_BIN,
|
|
224
|
+
notebook: nbName,
|
|
225
|
+
description: `Archived by dsh agent (${new Date().toISOString().slice(0, 10)})`,
|
|
226
|
+
title,
|
|
227
|
+
type: recordType,
|
|
228
|
+
content,
|
|
229
|
+
path,
|
|
230
|
+
});
|
|
231
|
+
const script = [
|
|
232
|
+
`export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
|
|
233
|
+
"export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
|
|
234
|
+
"F=/tmp/dt_pi_note_$$",
|
|
235
|
+
`echo '${b64(payload)}' | base64 -d > "$F.json"`,
|
|
236
|
+
`echo '${b64(PY_NOTE)}' | base64 -d > "$F.py"`,
|
|
237
|
+
`python3 "$F.py" "$F.json"`,
|
|
238
|
+
"rc=$?",
|
|
239
|
+
'rm -f "$F.json" "$F.py"',
|
|
240
|
+
"exit $rc",
|
|
241
|
+
].join("\n");
|
|
242
|
+
const remote = `echo '${b64(script)}' | base64 -d | bash`;
|
|
243
|
+
const res = await execFileResolved(
|
|
244
|
+
"ssh",
|
|
245
|
+
["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST, remote],
|
|
246
|
+
{ signal, timeoutMs: 60_000 },
|
|
247
|
+
);
|
|
248
|
+
const out = res.stdout ?? "";
|
|
249
|
+
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
250
|
+
return { stdout: stdout ?? "", stderr: (stderr ?? "").trim(), code: res.code ?? -1 };
|
|
251
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DeepTutor bridge: shared configuration (env vars) and small helpers.
|
|
3
|
-
*
|
|
4
|
-
* Config (optional env vars, agent-agnostic):
|
|
5
|
-
* DEEPTUTOR_SSH_HOST SSH host alias (remote deployment; unset/empty = local deployment)
|
|
6
|
-
* DEEPTUTOR_API_BASE API base URL (default http://127.0.0.1:8001, local or tunnel port)
|
|
7
|
-
* DEEPTUTOR_LOCAL_BIN local deeptutor executable (local CLI fallback; default: deeptutor on PATH)
|
|
8
|
-
* DEEPTUTOR_REMOTE_BIN remote deeptutor binary path (remote CLI fallback)
|
|
9
|
-
* DEEPTUTOR_REMOTE_HOME remote DEEPTUTOR_HOME workspace (remote)
|
|
10
|
-
*/
|
|
11
|
-
export const SSH_HOST = process.env.DEEPTUTOR_SSH_HOST ?? "";
|
|
12
|
-
export const API_BASE = process.env.DEEPTUTOR_API_BASE ?? "http://127.0.0.1:8001";
|
|
13
|
-
export const TUNNEL_PORT = Number(new URL(API_BASE).port || 8001);
|
|
14
|
-
export const LOCAL_BIN = process.env.DEEPTUTOR_LOCAL_BIN ?? "deeptutor";
|
|
15
|
-
export const DEEPTUTOR_BIN =
|
|
16
|
-
process.env.DEEPTUTOR_REMOTE_BIN ?? "/home/ubuntu/my-deeptutor/.venv/bin/deeptutor";
|
|
17
|
-
export const DEEPTUTOR_HOME = process.env.DEEPTUTOR_REMOTE_HOME ?? "/home/ubuntu/my-deeptutor";
|
|
18
|
-
|
|
19
|
-
// Whether the user explicitly configured anything (show a one-time hint when not)
|
|
20
|
-
const IS_CONFIGURED = Boolean(
|
|
21
|
-
process.env.DEEPTUTOR_SSH_HOST ||
|
|
22
|
-
process.env.DEEPTUTOR_API_BASE ||
|
|
23
|
-
process.env.DEEPTUTOR_LOCAL_BIN ||
|
|
24
|
-
process.env.DEEPTUTOR_REMOTE_BIN ||
|
|
25
|
-
process.env.DEEPTUTOR_REMOTE_HOME,
|
|
26
|
-
);
|
|
27
|
-
let configHintShown = false;
|
|
28
|
-
|
|
29
|
-
export function configHint(): string {
|
|
30
|
-
if (IS_CONFIGURED || configHintShown) return "";
|
|
31
|
-
configHintShown = true;
|
|
32
|
-
return (
|
|
33
|
-
"⚠️ No DEEPTUTOR_* env vars detected; running in local-deployment mode (local serve / local CLI).\n" +
|
|
34
|
-
' Local deployment: export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (serve port; optional DEEPTUTOR_LOCAL_BIN)\n' +
|
|
35
|
-
' Remote deployment: export DEEPTUTOR_SSH_HOST="your-ssh-alias" plus DEEPTUTOR_API_BASE\n' +
|
|
36
|
-
" (See the SKILL's Configuration section; changing server/port only requires env vars)\n"
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function configGuide(): string {
|
|
41
|
-
return (
|
|
42
|
-
"\n--- Configuration troubleshooting ---\n" +
|
|
43
|
-
"Local deployment (deeptutor runs on this machine):\n" +
|
|
44
|
-
' export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (serve port)\n' +
|
|
45
|
-
' export DEEPTUTOR_LOCAL_BIN="deeptutor" (local CLI path, optional)\n' +
|
|
46
|
-
" Note: do NOT set DEEPTUTOR_SSH_HOST (empty = local mode)\n" +
|
|
47
|
-
" Verify serve is running: curl http://127.0.0.1:8001/api/v1/system/status\n" +
|
|
48
|
-
"Remote deployment (deeptutor on a server):\n" +
|
|
49
|
-
' export DEEPTUTOR_SSH_HOST="your-ssh-alias"\n' +
|
|
50
|
-
' export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (local tunnel address; port must match the server\'s serve)\n' +
|
|
51
|
-
' export DEEPTUTOR_REMOTE_BIN="remote-deeptutor-binary-path"\n' +
|
|
52
|
-
' export DEEPTUTOR_REMOTE_HOME="remote-DEEPTUTOR_HOME-workspace"\n' +
|
|
53
|
-
" Verify serve is running on the server (ssh to it: curl http://127.0.0.1:8001/api/v1/system/status)\n" +
|
|
54
|
-
" Verify passwordless SSH login works (ssh alias + BatchMode)\n" +
|
|
55
|
-
"Restart the agent after configuring.\n"
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const CAPABILITIES = [
|
|
60
|
-
"chat",
|
|
61
|
-
"deep_solve",
|
|
62
|
-
"deep_question",
|
|
63
|
-
"deep_research",
|
|
64
|
-
"visualize",
|
|
65
|
-
"math_animator",
|
|
66
|
-
"mastery_path",
|
|
67
|
-
] as const;
|
|
68
|
-
|
|
69
|
-
export const truncate = (s: string, n: number) =>
|
|
70
|
-
s.length > n ? s.slice(0, n) + `\n…[truncated, ${s.length} chars total]` : s;
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: shared configuration (env vars) and small helpers.
|
|
3
|
+
*
|
|
4
|
+
* Config (optional env vars, agent-agnostic):
|
|
5
|
+
* DEEPTUTOR_SSH_HOST SSH host alias (remote deployment; unset/empty = local deployment)
|
|
6
|
+
* DEEPTUTOR_API_BASE API base URL (default http://127.0.0.1:8001, local or tunnel port)
|
|
7
|
+
* DEEPTUTOR_LOCAL_BIN local deeptutor executable (local CLI fallback; default: deeptutor on PATH)
|
|
8
|
+
* DEEPTUTOR_REMOTE_BIN remote deeptutor binary path (remote CLI fallback)
|
|
9
|
+
* DEEPTUTOR_REMOTE_HOME remote DEEPTUTOR_HOME workspace (remote)
|
|
10
|
+
*/
|
|
11
|
+
export const SSH_HOST = process.env.DEEPTUTOR_SSH_HOST ?? "";
|
|
12
|
+
export const API_BASE = process.env.DEEPTUTOR_API_BASE ?? "http://127.0.0.1:8001";
|
|
13
|
+
export const TUNNEL_PORT = Number(new URL(API_BASE).port || 8001);
|
|
14
|
+
export const LOCAL_BIN = process.env.DEEPTUTOR_LOCAL_BIN ?? "deeptutor";
|
|
15
|
+
export const DEEPTUTOR_BIN =
|
|
16
|
+
process.env.DEEPTUTOR_REMOTE_BIN ?? "/home/ubuntu/my-deeptutor/.venv/bin/deeptutor";
|
|
17
|
+
export const DEEPTUTOR_HOME = process.env.DEEPTUTOR_REMOTE_HOME ?? "/home/ubuntu/my-deeptutor";
|
|
18
|
+
|
|
19
|
+
// Whether the user explicitly configured anything (show a one-time hint when not)
|
|
20
|
+
const IS_CONFIGURED = Boolean(
|
|
21
|
+
process.env.DEEPTUTOR_SSH_HOST ||
|
|
22
|
+
process.env.DEEPTUTOR_API_BASE ||
|
|
23
|
+
process.env.DEEPTUTOR_LOCAL_BIN ||
|
|
24
|
+
process.env.DEEPTUTOR_REMOTE_BIN ||
|
|
25
|
+
process.env.DEEPTUTOR_REMOTE_HOME,
|
|
26
|
+
);
|
|
27
|
+
let configHintShown = false;
|
|
28
|
+
|
|
29
|
+
export function configHint(): string {
|
|
30
|
+
if (IS_CONFIGURED || configHintShown) return "";
|
|
31
|
+
configHintShown = true;
|
|
32
|
+
return (
|
|
33
|
+
"⚠️ No DEEPTUTOR_* env vars detected; running in local-deployment mode (local serve / local CLI).\n" +
|
|
34
|
+
' Local deployment: export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (serve port; optional DEEPTUTOR_LOCAL_BIN)\n' +
|
|
35
|
+
' Remote deployment: export DEEPTUTOR_SSH_HOST="your-ssh-alias" plus DEEPTUTOR_API_BASE\n' +
|
|
36
|
+
" (See the SKILL's Configuration section; changing server/port only requires env vars)\n"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function configGuide(): string {
|
|
41
|
+
return (
|
|
42
|
+
"\n--- Configuration troubleshooting ---\n" +
|
|
43
|
+
"Local deployment (deeptutor runs on this machine):\n" +
|
|
44
|
+
' export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (serve port)\n' +
|
|
45
|
+
' export DEEPTUTOR_LOCAL_BIN="deeptutor" (local CLI path, optional)\n' +
|
|
46
|
+
" Note: do NOT set DEEPTUTOR_SSH_HOST (empty = local mode)\n" +
|
|
47
|
+
" Verify serve is running: curl http://127.0.0.1:8001/api/v1/system/status\n" +
|
|
48
|
+
"Remote deployment (deeptutor on a server):\n" +
|
|
49
|
+
' export DEEPTUTOR_SSH_HOST="your-ssh-alias"\n' +
|
|
50
|
+
' export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (local tunnel address; port must match the server\'s serve)\n' +
|
|
51
|
+
' export DEEPTUTOR_REMOTE_BIN="remote-deeptutor-binary-path"\n' +
|
|
52
|
+
' export DEEPTUTOR_REMOTE_HOME="remote-DEEPTUTOR_HOME-workspace"\n' +
|
|
53
|
+
" Verify serve is running on the server (ssh to it: curl http://127.0.0.1:8001/api/v1/system/status)\n" +
|
|
54
|
+
" Verify passwordless SSH login works (ssh alias + BatchMode)\n" +
|
|
55
|
+
"Restart the agent after configuring.\n"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const CAPABILITIES = [
|
|
60
|
+
"chat",
|
|
61
|
+
"deep_solve",
|
|
62
|
+
"deep_question",
|
|
63
|
+
"deep_research",
|
|
64
|
+
"visualize",
|
|
65
|
+
"math_animator",
|
|
66
|
+
"mastery_path",
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
export const truncate = (s: string, n: number) =>
|
|
70
|
+
s.length > n ? s.slice(0, n) + `\n…[truncated, ${s.length} chars total]` : s;
|