dsh-deeptutor 0.1.0
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 +101 -0
- package/README.zh.md +85 -0
- package/cordis.patch.yml +17 -0
- package/lib/cli-exec.d.ts +28 -0
- package/lib/cli-exec.js +188 -0
- package/lib/config.d.ts +20 -0
- package/lib/config.js +58 -0
- package/lib/html-render.d.ts +14 -0
- package/lib/html-render.js +41 -0
- package/lib/http-api.d.ts +10 -0
- package/lib/http-api.js +77 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +384 -0
- package/lib/turn.d.ts +24 -0
- package/lib/turn.js +159 -0
- package/package.json +53 -0
- package/scripts/md-to-html.js +353 -0
- package/src/cli-exec.ts +251 -0
- package/src/config.ts +70 -0
- package/src/html-render.ts +53 -0
- package/src/http-api.ts +83 -0
- package/src/index.ts +449 -0
- package/src/turn.ts +174 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# dsh-deeptutor
|
|
2
|
+
|
|
3
|
+
DeepTutor bridge **bundle** for **DeepSeek Harness (dsh)**, migrated from the
|
|
4
|
+
pi coding-agent extension (`TecFancy/pi-extensions`, `extensions/deeptutor` +
|
|
5
|
+
`skills/deeptutor`).
|
|
6
|
+
|
|
7
|
+
Registers three model-facing tools that drive the
|
|
8
|
+
[HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor) tutoring service:
|
|
9
|
+
|
|
10
|
+
| Tool | Purpose |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `deeptutor_run` | Run a learning capability: `deep_solve` / `deep_question` / `deep_research` / `chat` / `mastery_path` / `visualize` / `math_animator` (HTTP/WS first, CLI fallback) |
|
|
13
|
+
| `deeptutor_kb` | List / search / info the user's personal knowledge bases (RAG) |
|
|
14
|
+
| `deeptutor_note` | Archive Markdown learning notes to a server notebook |
|
|
15
|
+
|
|
16
|
+
Deployment auto-detects local vs. remote: local `serve` (or local CLI) on this
|
|
17
|
+
machine, or a server reached through an auto-started SSH tunnel (with SSH CLI
|
|
18
|
+
fallback).
|
|
19
|
+
|
|
20
|
+
## Install into a profile (bundle)
|
|
21
|
+
|
|
22
|
+
Publish it (or point `dependencies` at a registry you control), then either:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
# one-shot: install the package into the profile and add it to the bundle list
|
|
26
|
+
dsh plugin --profile web add dsh-deeptutor
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
or add it manually to the profile manifest
|
|
30
|
+
(`~/.dsh/profiles/<name>/package.json`):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"dependencies": { "dsh-deeptutor": "^0.1.0" },
|
|
35
|
+
"dsh": {
|
|
36
|
+
"profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-deeptutor"] }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The bundle manifest (`dsh.bundle.patch → cordis.patch.yml`) inserts the plugin
|
|
42
|
+
row; later patch layers can override or disable it by id.
|
|
43
|
+
|
|
44
|
+
## Develop against a checkout (no publish needed)
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
dsh web --patch /path/to/dsh-deeptutor/cordis.yml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Build & publish
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install
|
|
54
|
+
npm run build # tsc → lib/ (relative .ts imports rewritten to .js)
|
|
55
|
+
npm run typecheck
|
|
56
|
+
npm pack # inspect dsh-deeptutor-0.1.0.tgz
|
|
57
|
+
npm publish # set a scope/registry of your choice first
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Node ≥ 22.6 (type stripping) is needed to load the raw `src/*.ts` via
|
|
61
|
+
`--patch`; the published bundle ships compiled `lib/`, so installed profiles
|
|
62
|
+
run on plain Node ≥ 20 ESM.
|
|
63
|
+
|
|
64
|
+
## Skills
|
|
65
|
+
|
|
66
|
+
The agent-facing workflow lives in the `deeptutor` skill, installed at
|
|
67
|
+
`~/.dsh/skills/deeptutor/SKILL.md` (auto-discovered by dsh). The companion
|
|
68
|
+
`html-doc` skill (`~/.dsh/skills/html-doc/`) renders study answers to
|
|
69
|
+
self-contained HTML pages; this plugin bundles the same converter under
|
|
70
|
+
`scripts/md-to-html.js` and uses it when `deeptutor_run` gets an `html` path.
|
|
71
|
+
|
|
72
|
+
## Configuration (env vars, agent-agnostic)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Remote deployment (DeepTutor on a server, reached through an SSH tunnel)
|
|
76
|
+
export DEEPTUTOR_SSH_HOST="tencent-cloud" # SSH host alias (set = remote mode)
|
|
77
|
+
export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" # local tunnel address
|
|
78
|
+
export DEEPTUTOR_REMOTE_BIN="/home/ubuntu/my-deeptutor/.venv/bin/deeptutor"
|
|
79
|
+
export DEEPTUTOR_REMOTE_HOME="/home/ubuntu/my-deeptutor"
|
|
80
|
+
|
|
81
|
+
# Local deployment — leave DEEPTUTOR_SSH_HOST unset
|
|
82
|
+
# export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" # local serve port
|
|
83
|
+
# export DEEPTUTOR_LOCAL_BIN="deeptutor" # local CLI path (default: deeptutor on PATH)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Restart dsh after changing env vars.
|
|
87
|
+
|
|
88
|
+
## Layout
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
src/ # TypeScript sources (dev loading, typecheck)
|
|
92
|
+
lib/ # compiled ESM (published entry, from `npm run build`)
|
|
93
|
+
scripts/md-to-html.js # zero-dependency Markdown → HTML converter
|
|
94
|
+
cordis.yml # dev overlay: insert src/index.ts by absolute path
|
|
95
|
+
cordis.patch.yml # bundle patch: insert the package by name
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`src/index.ts` registers the tools; `config.ts` holds env config;
|
|
99
|
+
`cli-exec.ts` local/SSH CLI execution; `http-api.ts` API probing + SSH tunnel;
|
|
100
|
+
`turn.ts` one learning turn (WebSocket / CLI event folding + formatting);
|
|
101
|
+
`html-render.ts` answer → self-contained HTML.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# dsh-deeptutor
|
|
2
|
+
|
|
3
|
+
面向 **DeepSeek Harness (dsh)** 的 DeepTutor 桥接 **bundle**,从 pi 编码 agent 扩展迁移而来(`TecFancy/pi-extensions`,`extensions/deeptutor` + `skills/deeptutor`)。
|
|
4
|
+
|
|
5
|
+
注册三个模型可见工具,驱动 [HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor) 辅导服务:
|
|
6
|
+
|
|
7
|
+
| 工具 | 用途 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `deeptutor_run` | 运行学习能力: `deep_solve` / `deep_question` / `deep_research` / `chat` / `mastery_path` / `visualize` / `math_animator`(优先 HTTP/WS,CLI 回退) |
|
|
10
|
+
| `deeptutor_kb` | 列出 / 搜索 / 查看个人知识库(RAG)信息 |
|
|
11
|
+
| `deeptutor_note` | 将 Markdown 学习笔记归档到服务端 notebook |
|
|
12
|
+
|
|
13
|
+
部署模式自动适配:本机本地 `serve`(或本地 CLI),或通过自动建立的 SSH 隧道访问服务器(含 SSH CLI 回退)。
|
|
14
|
+
|
|
15
|
+
## 安装到 profile(bundle 方式)
|
|
16
|
+
|
|
17
|
+
发布后(或指向你控制的 registry),任选其一:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# 一键:将包安装进 profile 并加入 bundle 列表
|
|
21
|
+
dsh plugin --profile web add dsh-deeptutor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
或手动编辑 profile 清单(`~/.dsh/profiles/<name>/package.json`):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"dependencies": { "dsh-deeptutor": "^0.1.0" },
|
|
29
|
+
"dsh": {
|
|
30
|
+
"profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-deeptutor"] }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
bundle 清单(`dsh.bundle.patch → cordis.patch.yml`)负责插入插件行;上层 patch 层可按 id 覆盖或禁用。
|
|
36
|
+
|
|
37
|
+
## 源码开发(无需发布)
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
dsh web --patch /path/to/dsh-deeptutor/cordis.yml
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 构建与发布
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install
|
|
47
|
+
npm run build # tsc → lib/(相对 .ts import 改写为 .js)
|
|
48
|
+
npm run typecheck
|
|
49
|
+
npm pack # 检查 dsh-deeptutor-0.1.0.tgz
|
|
50
|
+
npm publish
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
通过 `--patch` 直接加载 `src/*.ts` 需要 Node ≥ 22.6(type stripping);发布的 bundle 自带编译后的 `lib/`,安装到 profile 后仅需普通 Node ≥ 20 ESM。
|
|
54
|
+
|
|
55
|
+
## Skills
|
|
56
|
+
|
|
57
|
+
面向 agent 的工作流位于 `deeptutor` skill,安装于 `~/.dsh/skills/deeptutor/SKILL.md`(dsh 自动发现)。配套的 `html-doc` skill(`~/.dsh/skills/html-doc/`)将学习内容渲染为自包含 HTML 页面;本插件内置同一转换器(`scripts/md-to-html.js`),当 `deeptutor_run` 传入 `html` 路径时自动使用。
|
|
58
|
+
|
|
59
|
+
## 配置(环境变量,与 agent 无关)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 远程部署(DeepTutor 在服务器上,通过 SSH 隧道访问)
|
|
63
|
+
export DEEPTUTOR_SSH_HOST="tencent-cloud" # SSH 主机别名(设置 = 远程模式)
|
|
64
|
+
export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" # 本地隧道地址
|
|
65
|
+
export DEEPTUTOR_REMOTE_BIN="/home/ubuntu/my-deeptutor/.venv/bin/deeptutor"
|
|
66
|
+
export DEEPTUTOR_REMOTE_HOME="/home/ubuntu/my-deeptutor"
|
|
67
|
+
|
|
68
|
+
# 本地部署 —— 不要设置 DEEPTUTOR_SSH_HOST
|
|
69
|
+
# export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" # 本地 serve 端口
|
|
70
|
+
# export DEEPTUTOR_LOCAL_BIN="deeptutor" # 本地 CLI 路径(默认: PATH 上的 deeptutor)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
修改环境变量后需重启 dsh。
|
|
74
|
+
|
|
75
|
+
## 目录结构
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
src/ # TypeScript 源码(开发加载、类型检查)
|
|
79
|
+
lib/ # 编译产物(发布入口,由 npm run build 生成)
|
|
80
|
+
scripts/md-to-html.js # 零依赖 Markdown → HTML 转换器
|
|
81
|
+
cordis.yml # 开发 overlay:按绝对路径插入 src/index.ts
|
|
82
|
+
cordis.patch.yml # bundle patch:按包名插入插件
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`src/index.ts` 注册工具;`config.ts` 环境配置;`cli-exec.ts` 本地/SSH CLI 执行;`http-api.ts` API 探测 + SSH 隧道;`turn.ts` 单次学习回合(WebSocket / CLI 事件折叠与格式化);`html-render.ts` 答案 → 自包含 HTML。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# dsh-deeptutor bundle patch: inserts the DeepTutor bridge plugin.
|
|
2
|
+
#
|
|
3
|
+
# Mount into a profile either by adding "dsh-deeptutor" to the profile's
|
|
4
|
+
# `dsh.profile.bundles` list (recommended), or by inserting the same row
|
|
5
|
+
# through a user patch layer:
|
|
6
|
+
#
|
|
7
|
+
# - insert:
|
|
8
|
+
# - id: dsh-deeptutor
|
|
9
|
+
# name: 'dsh-deeptutor'
|
|
10
|
+
#
|
|
11
|
+
# The `name` is the npm package itself; the loader resolves it from the
|
|
12
|
+
# profile's node_modules (installed via `dsh plugin` or the profile's own
|
|
13
|
+
# package manager).
|
|
14
|
+
|
|
15
|
+
- insert:
|
|
16
|
+
- id: dsh-deeptutor
|
|
17
|
+
name: 'dsh-deeptutor'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CliResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
code: number;
|
|
5
|
+
killed: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Unified CLI entry:
|
|
9
|
+
* - Remote deployment (SSH_HOST set) → execute on the server over SSH;
|
|
10
|
+
* - Local deployment (SSH_HOST empty) → execute DEEPTUTOR_LOCAL_BIN locally.
|
|
11
|
+
* args are deeptutor subcommand args (without the program name).
|
|
12
|
+
*/
|
|
13
|
+
export declare function runCli(args: string[], opts?: {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}): Promise<CliResult>;
|
|
17
|
+
/** Local-deployment notebook archiver: list → parse ID → create → add-md */
|
|
18
|
+
export declare function cliNoteLocal(nbName: string, title: string, recordType: string, content: string, signal?: AbortSignal): Promise<{
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
code: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** Remote-deployment notebook archiver (SSH CLI fallback): runs PY_NOTE on the server. */
|
|
24
|
+
export declare function cliNoteRemote(nbName: string, title: string, recordType: string, content: string, signal?: AbortSignal): Promise<{
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
code: number;
|
|
28
|
+
}>;
|
package/lib/cli-exec.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
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.js";
|
|
10
|
+
// Remote runner: reads argv from a JSON file and executes without a shell (avoids all quoting/escaping).
|
|
11
|
+
const PY_RUNNER = [
|
|
12
|
+
"import json, subprocess, sys",
|
|
13
|
+
"a = json.load(open(sys.argv[1]))",
|
|
14
|
+
"r = subprocess.run(a['argv'], capture_output=True, text=True, cwd=a.get('cwd'))",
|
|
15
|
+
"sys.stdout.write(r.stdout)",
|
|
16
|
+
"if r.stderr.strip():",
|
|
17
|
+
" sys.stdout.write('__DT_STDERR__' + r.stderr[-3000:])",
|
|
18
|
+
"sys.exit(0 if r.returncode == 0 else 1)",
|
|
19
|
+
].join("\n");
|
|
20
|
+
// Remote notebook archiver (CLI fallback): ensure notebook exists → write temp md → add-md.
|
|
21
|
+
// NOTE: add-md takes the notebook ID as its first arg (passing a name silently "succeeds" without writing).
|
|
22
|
+
const PY_NOTE = [
|
|
23
|
+
"import json, subprocess, sys",
|
|
24
|
+
"a = json.load(open(sys.argv[1]))",
|
|
25
|
+
"dt = a['dt']",
|
|
26
|
+
"nb_id = None",
|
|
27
|
+
"r = subprocess.run([dt, 'notebook', 'list'], capture_output=True, text=True)",
|
|
28
|
+
"for line in r.stdout.splitlines():",
|
|
29
|
+
" parts = line.split('│')",
|
|
30
|
+
" if len(parts) >= 3 and parts[2].strip() == a['notebook']:",
|
|
31
|
+
" nb_id = parts[1].strip()",
|
|
32
|
+
" break",
|
|
33
|
+
"if not nb_id:",
|
|
34
|
+
" r = subprocess.run([dt, 'notebook', 'create', a['notebook'], '--description', a['description']], capture_output=True, text=True)",
|
|
35
|
+
" try:",
|
|
36
|
+
" nb_id = json.loads(r.stdout)['id']",
|
|
37
|
+
" except Exception:",
|
|
38
|
+
" sys.stdout.write(r.stdout)",
|
|
39
|
+
" sys.exit(1)",
|
|
40
|
+
"open(a['path'], 'w').write(a['content'])",
|
|
41
|
+
"r = subprocess.run([dt, 'notebook', 'add-md', nb_id, a['path'], '--title', a['title'], '--type', a['type']], capture_output=True, text=True)",
|
|
42
|
+
"sys.stdout.write(r.stdout)",
|
|
43
|
+
"if r.stderr.strip():",
|
|
44
|
+
" sys.stdout.write('__DT_STDERR__' + r.stderr[-2000:])",
|
|
45
|
+
"sys.exit(0 if r.returncode == 0 else 1)",
|
|
46
|
+
].join("\n");
|
|
47
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
48
|
+
/** Timeout for each notebook-archiver CLI step (list / create / add-md). */
|
|
49
|
+
const NOTE_STEP_TIMEOUT_MS = 30_000;
|
|
50
|
+
/** Exec one command with optional abort + timeout, resolving instead of throwing. */
|
|
51
|
+
function execFileResolved(file, args, opts = {}) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const child = execFile(file, args, {
|
|
54
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
timeout: opts.timeoutMs,
|
|
57
|
+
}, (err, stdout, stderr) => {
|
|
58
|
+
const e = err;
|
|
59
|
+
resolve({
|
|
60
|
+
stdout: stdout ?? "",
|
|
61
|
+
stderr: stderr ?? "",
|
|
62
|
+
code: e ? (typeof e.code === "number" ? e.code : e.code === "ENOENT" ? 127 : 1) : 0,
|
|
63
|
+
killed: e?.killed ?? false,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
opts.signal?.addEventListener("abort", () => {
|
|
67
|
+
if (process.platform === "win32") {
|
|
68
|
+
// child.kill() on Windows only kills the shell, not the process tree
|
|
69
|
+
// (deeptutor CLI is a Python app; its subprocesses would linger)
|
|
70
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
child.kill();
|
|
74
|
+
}
|
|
75
|
+
}, { once: true });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function sshExec(argv, opts = {}) {
|
|
79
|
+
const payload = JSON.stringify({ argv, cwd: DEEPTUTOR_HOME });
|
|
80
|
+
const script = [
|
|
81
|
+
`export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
|
|
82
|
+
"export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
|
|
83
|
+
"F=/tmp/dt_pi_$$",
|
|
84
|
+
`echo '${b64(payload)}' | base64 -d > "$F.json"`,
|
|
85
|
+
`echo '${b64(PY_RUNNER)}' | base64 -d > "$F.py"`,
|
|
86
|
+
`python3 "$F.py" "$F.json"`,
|
|
87
|
+
"rc=$?",
|
|
88
|
+
'rm -f "$F.json" "$F.py"',
|
|
89
|
+
"exit $rc",
|
|
90
|
+
].join("\n");
|
|
91
|
+
const remote = `echo '${b64(script)}' | base64 -d | bash`;
|
|
92
|
+
const res = await execFileResolved("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST, remote], { signal: opts.signal, timeoutMs: opts.timeoutMs ?? 600_000 });
|
|
93
|
+
const out = res.stdout ?? "";
|
|
94
|
+
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
95
|
+
return {
|
|
96
|
+
stdout: stdout ?? "",
|
|
97
|
+
stderr: (stderr ?? "").trim(),
|
|
98
|
+
code: res.code ?? -1,
|
|
99
|
+
killed: res.killed,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Unified CLI entry:
|
|
104
|
+
* - Remote deployment (SSH_HOST set) → execute on the server over SSH;
|
|
105
|
+
* - Local deployment (SSH_HOST empty) → execute DEEPTUTOR_LOCAL_BIN locally.
|
|
106
|
+
* args are deeptutor subcommand args (without the program name).
|
|
107
|
+
*/
|
|
108
|
+
export async function runCli(args, opts = {}) {
|
|
109
|
+
if (SSH_HOST) {
|
|
110
|
+
return sshExec([DEEPTUTOR_BIN, ...args], opts);
|
|
111
|
+
}
|
|
112
|
+
// Local mode: execute directly (force UTF-8 output to avoid Windows cp1252 console crashes)
|
|
113
|
+
const res = await execFileResolved(LOCAL_BIN, args, { signal: opts.signal, timeoutMs: opts.timeoutMs, env: { PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" } });
|
|
114
|
+
const out = res.stdout ?? "";
|
|
115
|
+
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
116
|
+
return {
|
|
117
|
+
stdout: stdout ?? "",
|
|
118
|
+
stderr: (stderr ?? "").trim(),
|
|
119
|
+
code: res.code,
|
|
120
|
+
killed: res.killed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Local-deployment notebook archiver: list → parse ID → create → add-md */
|
|
124
|
+
export async function cliNoteLocal(nbName, title, recordType, content, signal) {
|
|
125
|
+
const list = await runCli(["notebook", "list"], { signal, timeoutMs: NOTE_STEP_TIMEOUT_MS });
|
|
126
|
+
let nbId;
|
|
127
|
+
for (const line of (list.stdout ?? "").split("\n")) {
|
|
128
|
+
const parts = line.split("│");
|
|
129
|
+
if (parts.length >= 3 && parts[2].trim() === nbName) {
|
|
130
|
+
nbId = parts[1].trim();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!nbId) {
|
|
135
|
+
const created = await runCli([
|
|
136
|
+
"notebook",
|
|
137
|
+
"create",
|
|
138
|
+
nbName,
|
|
139
|
+
"--description",
|
|
140
|
+
`Archived by dsh agent (${new Date().toISOString().slice(0, 10)})`,
|
|
141
|
+
], { signal, timeoutMs: NOTE_STEP_TIMEOUT_MS });
|
|
142
|
+
let parsedId;
|
|
143
|
+
try {
|
|
144
|
+
parsedId = JSON.parse(created.stdout ?? "{}").id;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { stdout: created.stdout ?? "", stderr: created.stderr, code: created.code };
|
|
148
|
+
}
|
|
149
|
+
if (typeof parsedId !== "string") {
|
|
150
|
+
return { stdout: created.stdout ?? "", stderr: created.stderr, code: created.code };
|
|
151
|
+
}
|
|
152
|
+
nbId = parsedId;
|
|
153
|
+
}
|
|
154
|
+
const tmp = join(tmpdir(), `dt_pi_note_${Date.now()}.md`);
|
|
155
|
+
writeFileSync(tmp, content, "utf8");
|
|
156
|
+
const add = await runCli(["notebook", "add-md", nbId, tmp, "--title", title, "--type", recordType], { signal, timeoutMs: NOTE_STEP_TIMEOUT_MS });
|
|
157
|
+
rmSync(tmp, { force: true });
|
|
158
|
+
return { stdout: add.stdout ?? "", stderr: add.stderr, code: add.code };
|
|
159
|
+
}
|
|
160
|
+
/** Remote-deployment notebook archiver (SSH CLI fallback): runs PY_NOTE on the server. */
|
|
161
|
+
export async function cliNoteRemote(nbName, title, recordType, content, signal) {
|
|
162
|
+
const path = `/tmp/dt_pi_note_${Date.now()}.md`;
|
|
163
|
+
const payload = JSON.stringify({
|
|
164
|
+
dt: DEEPTUTOR_BIN,
|
|
165
|
+
notebook: nbName,
|
|
166
|
+
description: `Archived by dsh agent (${new Date().toISOString().slice(0, 10)})`,
|
|
167
|
+
title,
|
|
168
|
+
type: recordType,
|
|
169
|
+
content,
|
|
170
|
+
path,
|
|
171
|
+
});
|
|
172
|
+
const script = [
|
|
173
|
+
`export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
|
|
174
|
+
"export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
|
|
175
|
+
"F=/tmp/dt_pi_note_$$",
|
|
176
|
+
`echo '${b64(payload)}' | base64 -d > "$F.json"`,
|
|
177
|
+
`echo '${b64(PY_NOTE)}' | base64 -d > "$F.py"`,
|
|
178
|
+
`python3 "$F.py" "$F.json"`,
|
|
179
|
+
"rc=$?",
|
|
180
|
+
'rm -f "$F.json" "$F.py"',
|
|
181
|
+
"exit $rc",
|
|
182
|
+
].join("\n");
|
|
183
|
+
const remote = `echo '${b64(script)}' | base64 -d | bash`;
|
|
184
|
+
const res = await execFileResolved("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST, remote], { signal, timeoutMs: 60_000 });
|
|
185
|
+
const out = res.stdout ?? "";
|
|
186
|
+
const [stdout, stderr] = out.split("__DT_STDERR__");
|
|
187
|
+
return { stdout: stdout ?? "", stderr: (stderr ?? "").trim(), code: res.code ?? -1 };
|
|
188
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 declare const SSH_HOST: string;
|
|
12
|
+
export declare const API_BASE: string;
|
|
13
|
+
export declare const TUNNEL_PORT: number;
|
|
14
|
+
export declare const LOCAL_BIN: string;
|
|
15
|
+
export declare const DEEPTUTOR_BIN: string;
|
|
16
|
+
export declare const DEEPTUTOR_HOME: string;
|
|
17
|
+
export declare function configHint(): string;
|
|
18
|
+
export declare function configGuide(): string;
|
|
19
|
+
export declare const CAPABILITIES: readonly ["chat", "deep_solve", "deep_question", "deep_research", "visualize", "math_animator", "mastery_path"];
|
|
20
|
+
export declare const truncate: (s: string, n: number) => string;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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 = process.env.DEEPTUTOR_REMOTE_BIN ?? "/home/ubuntu/my-deeptutor/.venv/bin/deeptutor";
|
|
16
|
+
export const DEEPTUTOR_HOME = process.env.DEEPTUTOR_REMOTE_HOME ?? "/home/ubuntu/my-deeptutor";
|
|
17
|
+
// Whether the user explicitly configured anything (show a one-time hint when not)
|
|
18
|
+
const IS_CONFIGURED = Boolean(process.env.DEEPTUTOR_SSH_HOST ||
|
|
19
|
+
process.env.DEEPTUTOR_API_BASE ||
|
|
20
|
+
process.env.DEEPTUTOR_LOCAL_BIN ||
|
|
21
|
+
process.env.DEEPTUTOR_REMOTE_BIN ||
|
|
22
|
+
process.env.DEEPTUTOR_REMOTE_HOME);
|
|
23
|
+
let configHintShown = false;
|
|
24
|
+
export function configHint() {
|
|
25
|
+
if (IS_CONFIGURED || configHintShown)
|
|
26
|
+
return "";
|
|
27
|
+
configHintShown = true;
|
|
28
|
+
return ("⚠️ No DEEPTUTOR_* env vars detected; running in local-deployment mode (local serve / local CLI).\n" +
|
|
29
|
+
' Local deployment: export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (serve port; optional DEEPTUTOR_LOCAL_BIN)\n' +
|
|
30
|
+
' Remote deployment: export DEEPTUTOR_SSH_HOST="your-ssh-alias" plus DEEPTUTOR_API_BASE\n' +
|
|
31
|
+
" (See the SKILL's Configuration section; changing server/port only requires env vars)\n");
|
|
32
|
+
}
|
|
33
|
+
export function configGuide() {
|
|
34
|
+
return ("\n--- Configuration troubleshooting ---\n" +
|
|
35
|
+
"Local deployment (deeptutor runs on this machine):\n" +
|
|
36
|
+
' export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (serve port)\n' +
|
|
37
|
+
' export DEEPTUTOR_LOCAL_BIN="deeptutor" (local CLI path, optional)\n' +
|
|
38
|
+
" Note: do NOT set DEEPTUTOR_SSH_HOST (empty = local mode)\n" +
|
|
39
|
+
" Verify serve is running: curl http://127.0.0.1:8001/api/v1/system/status\n" +
|
|
40
|
+
"Remote deployment (deeptutor on a server):\n" +
|
|
41
|
+
' export DEEPTUTOR_SSH_HOST="your-ssh-alias"\n' +
|
|
42
|
+
' export DEEPTUTOR_API_BASE="http://127.0.0.1:8001" (local tunnel address; port must match the server\'s serve)\n' +
|
|
43
|
+
' export DEEPTUTOR_REMOTE_BIN="remote-deeptutor-binary-path"\n' +
|
|
44
|
+
' export DEEPTUTOR_REMOTE_HOME="remote-DEEPTUTOR_HOME-workspace"\n' +
|
|
45
|
+
" Verify serve is running on the server (ssh to it: curl http://127.0.0.1:8001/api/v1/system/status)\n" +
|
|
46
|
+
" Verify passwordless SSH login works (ssh alias + BatchMode)\n" +
|
|
47
|
+
"Restart the agent after configuring.\n");
|
|
48
|
+
}
|
|
49
|
+
export const CAPABILITIES = [
|
|
50
|
+
"chat",
|
|
51
|
+
"deep_solve",
|
|
52
|
+
"deep_question",
|
|
53
|
+
"deep_research",
|
|
54
|
+
"visualize",
|
|
55
|
+
"math_animator",
|
|
56
|
+
"mastery_path",
|
|
57
|
+
];
|
|
58
|
+
export const truncate = (s, n) => s.length > n ? s.slice(0, n) + `\n…[truncated, ${s.length} chars total]` : s;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Absolute path to the bundled md-to-html converter script. */
|
|
2
|
+
export declare const HTML_DOC_SCRIPT: string;
|
|
3
|
+
/**
|
|
4
|
+
* Render a learning answer to a self-contained local HTML file (dark/light toggle,
|
|
5
|
+
* TOC, collapsible quiz answers, print-friendly). Also writes the companion .md
|
|
6
|
+
* source next to the html file.
|
|
7
|
+
*
|
|
8
|
+
* Returns a short status note to append to the tool result ("" when no html path
|
|
9
|
+
* given; "[html] ✗ …" note when rendering fails — never throws).
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderHtml(params: {
|
|
12
|
+
html?: string;
|
|
13
|
+
html_title?: string;
|
|
14
|
+
}, answer: string): Promise<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: render a learning answer to a self-contained local HTML file
|
|
3
|
+
* via the bundled zero-dependency Node converter (scripts/md-to-html.js, from
|
|
4
|
+
* the html-doc skill). Kept as a standalone module (node built-ins only).
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
/** Absolute path to the bundled md-to-html converter script. */
|
|
11
|
+
export const HTML_DOC_SCRIPT = join(fileURLToPath(new URL("..", import.meta.url)), "scripts", "md-to-html.js");
|
|
12
|
+
/**
|
|
13
|
+
* Render a learning answer to a self-contained local HTML file (dark/light toggle,
|
|
14
|
+
* TOC, collapsible quiz answers, print-friendly). Also writes the companion .md
|
|
15
|
+
* source next to the html file.
|
|
16
|
+
*
|
|
17
|
+
* Returns a short status note to append to the tool result ("" when no html path
|
|
18
|
+
* given; "[html] ✗ …" note when rendering fails — never throws).
|
|
19
|
+
*/
|
|
20
|
+
export async function renderHtml(params, answer) {
|
|
21
|
+
const out = params.html;
|
|
22
|
+
if (!out)
|
|
23
|
+
return "";
|
|
24
|
+
const htmlPath = extname(out).toLowerCase() === ".html" ? resolve(out) : resolve(out + ".html");
|
|
25
|
+
const mdPath = htmlPath.slice(0, -extname(htmlPath).length) + ".md";
|
|
26
|
+
const title = params.html_title || basename(htmlPath, extname(htmlPath));
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(HTML_DOC_SCRIPT)) {
|
|
29
|
+
return `\n[html] ✗ html-doc converter not found: ${HTML_DOC_SCRIPT}`;
|
|
30
|
+
}
|
|
31
|
+
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
32
|
+
writeFileSync(mdPath, answer || "(no answer)", "utf8");
|
|
33
|
+
await new Promise((res, rej) => {
|
|
34
|
+
execFile(process.execPath, [HTML_DOC_SCRIPT, mdPath, "--out", htmlPath, "--title", title, "--toc"], { windowsHide: true, timeout: 60_000 }, (err, _stdout, stderr) => (err ? rej(new Error(stderr || err.message)) : res()));
|
|
35
|
+
});
|
|
36
|
+
return `\n[html] ✓ rendered (html-doc skill)\n[html] page: ${htmlPath}\n[html] source: ${mdPath}`;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return `\n[html] ✗ rendering failed: ${err?.message ?? String(err)}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Kill the SSH tunnel process, if one is running (call on plugin disposal). */
|
|
2
|
+
export declare function killTunnel(): void;
|
|
3
|
+
export declare function apiProbe(timeoutMs?: number): Promise<boolean>;
|
|
4
|
+
/** Ensure the local API is reachable: probe → start SSH tunnel if needed (remote only) → wait. */
|
|
5
|
+
export declare function ensureApi(): Promise<boolean>;
|
|
6
|
+
/** JSON fetch against the DeepTutor API. Combines a fixed timeout with an optional caller-supplied abort signal. */
|
|
7
|
+
export declare function apiJson(path: string, init?: RequestInit, opts?: {
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}): Promise<any>;
|
package/lib/http-api.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: HTTP/WS transport (API probing, SSH tunnel management, JSON calls).
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { API_BASE, SSH_HOST, TUNNEL_PORT } from "./config.js";
|
|
6
|
+
let tunnelProc = null;
|
|
7
|
+
/** Kill the SSH tunnel process, if one is running (call on plugin disposal). */
|
|
8
|
+
export function killTunnel() {
|
|
9
|
+
if (tunnelProc && tunnelProc.exitCode === null) {
|
|
10
|
+
try {
|
|
11
|
+
tunnelProc.kill();
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
tunnelProc = null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function apiProbe(timeoutMs = 1500) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${API_BASE}/api/v1/system/status`, {
|
|
20
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
21
|
+
});
|
|
22
|
+
return res.ok;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Ensure the local API is reachable: probe → start SSH tunnel if needed (remote only) → wait. */
|
|
29
|
+
export async function ensureApi() {
|
|
30
|
+
if (await apiProbe(1200))
|
|
31
|
+
return true;
|
|
32
|
+
if (!SSH_HOST)
|
|
33
|
+
return false; // Local deployment: fall back to local CLI when serve is down, no tunnel needed
|
|
34
|
+
if (!tunnelProc || tunnelProc.exitCode !== null) {
|
|
35
|
+
tunnelProc = spawn("ssh", [
|
|
36
|
+
"-N",
|
|
37
|
+
"-L",
|
|
38
|
+
`${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT}`,
|
|
39
|
+
"-o",
|
|
40
|
+
"BatchMode=yes",
|
|
41
|
+
"-o",
|
|
42
|
+
"ExitOnForwardFailure=yes",
|
|
43
|
+
"-o",
|
|
44
|
+
"ServerAliveInterval=30",
|
|
45
|
+
"-o",
|
|
46
|
+
"ServerAliveCountMax=3",
|
|
47
|
+
"-o",
|
|
48
|
+
"ConnectTimeout=10",
|
|
49
|
+
SSH_HOST,
|
|
50
|
+
], { stdio: "ignore", windowsHide: true });
|
|
51
|
+
tunnelProc.unref();
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < 20; i++) {
|
|
54
|
+
if (await apiProbe(1000))
|
|
55
|
+
return true;
|
|
56
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/** JSON fetch against the DeepTutor API. Combines a fixed timeout with an optional caller-supplied abort signal. */
|
|
61
|
+
export async function apiJson(path, init, opts = {}) {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 60_000);
|
|
64
|
+
const onAbort = () => controller.abort();
|
|
65
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${API_BASE}${path}`, { ...init, signal: controller.signal });
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
return text ? JSON.parse(text) : null;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
76
|
+
}
|
|
77
|
+
}
|