dsh-cloud-experts 0.0.0-stage → 0.1.2
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 +36 -2
- package/cordis.patch.yml +15 -0
- package/lib/index.js +159 -0
- package/package.json +33 -4
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-cloud-experts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把远端服务器上的 Claude Code / Codex CLI 注册为 DSH 云端子智能体:插件经 SSH 调用远端 `fed-expert` wrapper(协议 `fed-expert/v1`),订阅凭据、模型选择、资源围栏全部留在远端服务器,本机零凭据。
|
|
4
|
+
|
|
5
|
+
## 提供的能力
|
|
6
|
+
|
|
7
|
+
- 子智能体 provider:`cloud-claude` / `cloud-codex`(one-shot external provider,多轮靠 prompt 自携带上下文)
|
|
8
|
+
- 工具:`consult_claude` / `consult_codex`(由官方 `@deepseek-ai/dsh-tool-subagent` 挂接)
|
|
9
|
+
|
|
10
|
+
## 前置要求
|
|
11
|
+
|
|
12
|
+
远端服务器需部署配套的 `fed-expert` wrapper(接收 stdin prompt,stdout 返回答案,stderr 首行 `FED_ENVELOPE:{json}` 信封),并为本机配置一把 forced-command + `restrict` 的 SSH 专用钥。
|
|
13
|
+
|
|
14
|
+
## 配置
|
|
15
|
+
|
|
16
|
+
| 键 | 默认 | 说明 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| host | `allen-bwg` | SSH 目标(走本机 ssh config 别名,需已配置 HostName/User) |
|
|
19
|
+
| keyPath | 空 | 专用钥路径(空 = ssh config 默认身份) |
|
|
20
|
+
| remoteEntry | `~/bin/fed-expert` | 远端 wrapper 路径 |
|
|
21
|
+
| origin | `dsh-plugin` | 审计来源标记 |
|
|
22
|
+
| disposeGraceMs | 5000 | 取消时进程回收宽限 |
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
- **DSH Desktop / GO 插件市场**:按包名 `dsh-cloud-experts` 安装(依赖 peers `0.2.2-rc.1`);
|
|
27
|
+
- **开发模式(Desktop)**:本目录先 `npm install`(link 模式不代装依赖),再 Plugins → Install from path。
|
|
28
|
+
|
|
29
|
+
装完新开会话,工具列表出现 `consult_claude` / `consult_codex` 即成功。
|
|
30
|
+
|
|
31
|
+
## 版本
|
|
32
|
+
|
|
33
|
+
- 0.1.2:ssh 内置 ServerAliveInterval=15(NAT 防断)+ 本地 360s 看门狗;0.1.1:对齐官方 inject。
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
- insert:
|
|
2
|
+
- id: subagent-cloud-experts
|
|
3
|
+
name: dsh-cloud-experts
|
|
4
|
+
- id: tool-subagent-cloud-claude
|
|
5
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
6
|
+
config:
|
|
7
|
+
provider: cloud-claude
|
|
8
|
+
toolName: consult_claude
|
|
9
|
+
maxDepth: provider-managed
|
|
10
|
+
- id: tool-subagent-cloud-codex
|
|
11
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
12
|
+
config:
|
|
13
|
+
provider: cloud-codex
|
|
14
|
+
toolName: consult_codex
|
|
15
|
+
maxDepth: provider-managed
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
const name = "cloud-experts";
|
|
6
|
+
// 与官方 bundle 一致:ctx.logger 原生访问、不注入(注入会因服务不存在而永久 pending)
|
|
7
|
+
const inject = ["subagents", "subprocess"];
|
|
8
|
+
|
|
9
|
+
const Config = z.object({
|
|
10
|
+
host: z.string().min(1).default("allen-bwg"),
|
|
11
|
+
keyPath: z.string().default(""),
|
|
12
|
+
remoteEntry: z.string().min(1).default("~/bin/fed-expert"),
|
|
13
|
+
origin: z.string().min(1).default("dsh-plugin"),
|
|
14
|
+
disposeGraceMs: z.number().default(5000)
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const MINIMAL_ENV_KEYS = ["PATH", "SystemRoot", "USERPROFILE", "HOME"];
|
|
18
|
+
|
|
19
|
+
function minimalEnv() {
|
|
20
|
+
const env = {};
|
|
21
|
+
for (const key of MINIMAL_ENV_KEYS) if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
22
|
+
return env;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function promptText(prompt) {
|
|
26
|
+
const parts = Array.isArray(prompt) ? prompt : [prompt];
|
|
27
|
+
const texts = [];
|
|
28
|
+
for (const part of parts) {
|
|
29
|
+
if (part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string") texts.push(part.text);
|
|
30
|
+
else if (typeof part === "string") texts.push(part);
|
|
31
|
+
}
|
|
32
|
+
return texts.join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeAll(stream, data) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const CHUNK = 65536;
|
|
38
|
+
let offset = 0;
|
|
39
|
+
const next = () => {
|
|
40
|
+
while (offset < data.length) {
|
|
41
|
+
const slice = data.subarray(offset, offset + CHUNK);
|
|
42
|
+
offset += slice.length;
|
|
43
|
+
if (!stream.write(slice)) {
|
|
44
|
+
stream.once("drain", next);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
stream.end(resolve);
|
|
49
|
+
};
|
|
50
|
+
stream.on("error", reject);
|
|
51
|
+
next();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class CloudExpertProvider {
|
|
56
|
+
capabilities = NO_START_CAPABILITIES;
|
|
57
|
+
inheritsParentContext = false;
|
|
58
|
+
|
|
59
|
+
constructor(providerName, ctx, config, expert) {
|
|
60
|
+
this.name = providerName;
|
|
61
|
+
this.ctx = ctx;
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.expert = expert;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
start(request) {
|
|
67
|
+
if (request.signal.aborted) throw new Error(`cloud-experts: request aborted before ${this.expert} run`);
|
|
68
|
+
const text = promptText(request.prompt) || `(empty ${this.expert} consult)`;
|
|
69
|
+
const stamp = new Date().toISOString().replace(/[^0-9]/g, "").slice(0, 14);
|
|
70
|
+
const consultId = `PLUGIN-${stamp}-${this.expert}-${randomUUID().slice(0, 8)}`;
|
|
71
|
+
const remoteCmd = [this.config.remoteEntry, this.expert, "--id", consultId, "--origin", this.config.origin].join(" ");
|
|
72
|
+
const argv = ["ssh", "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", "ConnectTimeout=15",
|
|
73
|
+
"-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4", "-o", "LogLevel=ERROR"];
|
|
74
|
+
if (this.config.keyPath) argv.push("-i", this.config.keyPath);
|
|
75
|
+
argv.push(this.config.host, remoteCmd);
|
|
76
|
+
|
|
77
|
+
const child = this.ctx.subprocess.spawn({
|
|
78
|
+
argv,
|
|
79
|
+
cwd: request.cwd,
|
|
80
|
+
stdio: { stdin: "pipe", stdout: "pipe", stderr: "pipe" },
|
|
81
|
+
graceMs: this.config.disposeGraceMs,
|
|
82
|
+
signal: request.signal,
|
|
83
|
+
env: minimalEnv()
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let out = "";
|
|
87
|
+
let err = "";
|
|
88
|
+
child.stdout?.on("data", (chunk) => { out += typeof chunk === "string" ? chunk : chunk.toString("utf8"); });
|
|
89
|
+
child.stdout?.on("error", () => {});
|
|
90
|
+
child.stderr?.on("data", (chunk) => { err += typeof chunk === "string" ? chunk : chunk.toString("utf8"); });
|
|
91
|
+
child.stderr?.on("error", () => {});
|
|
92
|
+
const stdinWrite = writeAll(child.stdin, Buffer.from(text, "utf8")).catch(() => {});
|
|
93
|
+
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const watchdog = setTimeout(() => {
|
|
96
|
+
try { child.stdin?.end(); } catch {}
|
|
97
|
+
try { child.terminate(); } catch {}
|
|
98
|
+
controller.abort(new Error(`cloud-experts: ${this.expert} local watchdog fired (360s)`));
|
|
99
|
+
}, 360000);
|
|
100
|
+
const requestCancel = () => {
|
|
101
|
+
if (!controller.signal.aborted) controller.abort(new Error(`cloud-experts: ${this.expert} run cancelled locally`));
|
|
102
|
+
try { child.stdin?.end(); } catch {}
|
|
103
|
+
try { child.terminate(); } catch {}
|
|
104
|
+
};
|
|
105
|
+
const onAbort = () => requestCancel();
|
|
106
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
107
|
+
|
|
108
|
+
const attempt = async () => {
|
|
109
|
+
try {
|
|
110
|
+
await stdinWrite;
|
|
111
|
+
const info = await child.done;
|
|
112
|
+
const code = info && typeof info.code === "number" ? info.code : 0;
|
|
113
|
+
const envelope = /FED_ENVELOPE:(\{[^\n]*\})/.exec(err);
|
|
114
|
+
if (code === 0 && out.length > 0) {
|
|
115
|
+
return {
|
|
116
|
+
output: [{ type: "text", text: out }],
|
|
117
|
+
stopReason: "completed",
|
|
118
|
+
envelope: envelope ? envelope[1] : undefined
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const failure = /FED_ERROR:([\w:.-]+)/.exec(err);
|
|
122
|
+
throw new Error(`cloud-experts: ${failure?.[1] ?? `exit-${code}`} ${err.slice(0, 200)}`);
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(watchdog);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = settleRunResult({
|
|
129
|
+
attempt,
|
|
130
|
+
collectOutput: () => out,
|
|
131
|
+
cancelled: () => request.signal.aborted,
|
|
132
|
+
onError: (error) => {
|
|
133
|
+
this.ctx.logger?.warn?.(`cloud-experts "${this.name}": run failed: %o`, error);
|
|
134
|
+
},
|
|
135
|
+
signal: request.signal,
|
|
136
|
+
onAbort
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return subprocessRunHandle({
|
|
140
|
+
id: randomUUID(),
|
|
141
|
+
result,
|
|
142
|
+
signal: request.signal,
|
|
143
|
+
onAbort,
|
|
144
|
+
requestCancel,
|
|
145
|
+
teardown: async () => {
|
|
146
|
+
requestCancel();
|
|
147
|
+
try { await child.waitForExit(); } catch {}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function apply(ctx, config) {
|
|
154
|
+
const resolved = Config(config ?? {});
|
|
155
|
+
ctx.subagents.registerProvider(new CloudExpertProvider("cloud-claude", ctx, resolved, "claude"));
|
|
156
|
+
ctx.subagents.registerProvider(new CloudExpertProvider("cloud-codex", ctx, resolved, "codex"));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cloud-experts",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "Cloud expert subagents (claude/codex on the fed VPS) over SSH fed-expert wrapper",
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib/index.js",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dsh": {
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@deepseek-ai/cordis": "~4.1.0",
|
|
25
|
+
"@deepseek-ai/dsh-llm": "0.2.2-rc.1",
|
|
26
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
27
|
+
"@deepseek-ai/dsh-subagent": "0.2.2-rc.1",
|
|
28
|
+
"@deepseek-ai/dsh-subprocess": "0.2.2-rc.1",
|
|
29
|
+
"@deepseek-ai/dsh-timeout": "0.2.2-rc.1"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@deepseek-ai/dsh-tool-subagent": "0.2.2-rc.1",
|
|
33
|
+
"@deepseek-ai/schemastery": "~3.19.0"
|
|
34
|
+
}
|
|
35
|
+
}
|