mu-agent-bridge 0.0.0-stage → 1.0.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 +120 -2
- package/dist/src-ts/agent.js +10 -0
- package/dist/src-ts/bridge.js +23 -0
- package/dist/src-ts/cli.js +182 -0
- package/dist/src-ts/control-server.js +243 -0
- package/dist/src-ts/dsh-agent.js +70 -0
- package/dist/src-ts/dsh-paths.js +15 -0
- package/dist/src-ts/headless-account.patch.yml +5 -0
- package/dist/src-ts/llprobe-qwen-bridge.js +304 -0
- package/dist/src-ts/plugin-manager.js +440 -0
- package/dist/src-ts/plugin-manifest.js +31 -0
- package/dist/src-ts/plugin.js +2 -0
- package/dist/src-ts/plugins/abweb/control-client.js +91 -0
- package/dist/src-ts/plugins/abweb/index.js +132 -0
- package/dist/src-ts/plugins/abweb/public/app.js +213 -0
- package/dist/src-ts/plugins/abweb/public/index.html +24 -0
- package/dist/src-ts/plugins/abweb/public/style.css +43 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +93 -0
- package/dist/src-ts/plugins/doubao/index.js +83 -0
- package/dist/src-ts/plugins/dsh/index.js +61 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +165 -0
- package/dist/src-ts/plugins/qwenwork/index.js +121 -0
- package/dist/src-ts/plugins/qwenwork/openai-adapter.js +146 -0
- package/dist/src-ts/registry.js +30 -0
- package/dist/src-ts/route-registry.js +22 -0
- package/dist/src-ts/version-manager.js +73 -0
- package/dist/tests-ts/abweb-plugin.test.js +86 -0
- package/dist/tests-ts/control-server.test.js +194 -0
- package/dist/tests-ts/dsh-agent.test.js +28 -0
- package/dist/tests-ts/platform-integration.test.js +57 -0
- package/dist/tests-ts/plugin-manager.test.js +132 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +43 -0
- package/dist/tests-ts/qwenwork-sse.test.js +91 -0
- package/dist/tests-ts/registry.test.js +22 -0
- package/dist/tests-ts/route-registry.test.js +19 -0
- package/dist/tests-ts/version-manager.test.js +38 -0
- package/package.json +32 -4
- package/plugins/abweb/config.schema.json +23 -0
- package/plugins/abweb/plugin.json +11 -0
- package/plugins/qwenwork/config.schema.json +14 -0
- package/plugins/qwenwork/plugin.json +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,121 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mu-agent-bridge 动态插件平台
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
运行时使用 TypeScript/Node.js,实现一个可动态发现、启动、停止、重载和安装插件的 daemon。
|
|
4
|
+
当前插件包括千问办公 `qwenwork`、DeepSeek Harness `dsh`、豆包 `doubao`,以及插件化管理后台
|
|
5
|
+
`abweb`。插件之间独立失败,管理 API 和 OpenAI 兼容接口由同一个本地控制服务提供。
|
|
6
|
+
|
|
7
|
+
## 快速开始
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
npm install
|
|
11
|
+
npm test
|
|
12
|
+
npm run start
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
默认监听 `http://127.0.0.1:26107`,管理页面为 `http://127.0.0.1:26107/abweb/`,管理 API 为 `/api/health`、`/api/plugins`,千问办公的
|
|
16
|
+
OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位于 `/abweb`。
|
|
17
|
+
千问办公插件使用 CDP `9226`;没有运行千问办公时,daemon 仍会启动,qwenwork 会显示为失败状态,
|
|
18
|
+
可以通过管理 API 重载。
|
|
19
|
+
|
|
20
|
+
安装后使用 `mab start` 启动。旧桥仍可使用 `17893`,不会与管理页面冲突。局域网访问时设置
|
|
21
|
+
`AGENT_BRIDGE_HOST=0.0.0.0` 和 `AGENT_BRIDGE_TOKEN`,然后访问
|
|
22
|
+
`http://<Windows 主机 IP>:26107/abweb/`。页面输入令牌后可管理插件;请只在可信网络使用。
|
|
23
|
+
已有 AID 从 EvolCore 的 `ec agent list/show` 读取,只显示当前 AUN 已连接且本机有私钥的身份,
|
|
24
|
+
禁用和断线身份不列出。管理页可指定新 AID 与 owner AID,调用 EvolCore 创建 Agent 和身份;
|
|
25
|
+
新身份连接成功后才会进入列表。
|
|
26
|
+
“已连接 AID”按来源分为 EvolCore 和 mu-agent-bridge 两组;后者只包含通过本页面新建、
|
|
27
|
+
且当前已连接的 AID。分组由 mab 状态目录中的创建记录决定,不改变 EvolCore 的身份存储。
|
|
28
|
+
关联 DSH 或千问办公插件时,管理服务会将所选 AID 的 EvolCore 执行基座分别配置为本机
|
|
29
|
+
`/dsh/v1` 或 `/v1` OpenAI 接口,然后重载该 Agent;AUN 入站消息由 EvolCore 转给插件,
|
|
30
|
+
回复仍由 EvolCore 经 AUN 发送。该操作会覆盖所选 AID 当前的执行基座。
|
|
31
|
+
其他插件的 AID 关联目前只保存配置项,不自动改变 EvolCore 执行基座。
|
|
32
|
+
Windows 可运行 `start-mab.ps1` 注册并启动登录时自动运行的计划任务;令牌保存在
|
|
33
|
+
`%USERPROFILE%\.mab\token.txt`,页面输入该令牌即可使用管理 API。
|
|
34
|
+
|
|
35
|
+
本机打开 `http://127.0.0.1:26107/abweb/` 无需配置码。局域网管理页可用一次性配置码配对。
|
|
36
|
+
在 Windows 主机运行 `mab pair`,将显示的 6 位码输入
|
|
37
|
+
abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,连续输错 5 次后需重新执行
|
|
38
|
+
`mab pair`。配对成功后浏览器在当前会话保存访问凭证。配置码只能从 Windows 本机的
|
|
39
|
+
`127.0.0.1` 接口取得,远程浏览器不能读取。
|
|
40
|
+
|
|
41
|
+
环境变量:`AGENT_BRIDGE_PORT`、`AGENT_BRIDGE_HOST`、`AGENT_BRIDGE_TOKEN`、
|
|
42
|
+
`AGENT_BRIDGE_PLUGINS_DIR`、`AGENT_BRIDGE_STATE_DIR`。默认插件目录是仓库的 `plugins/`,状态目录
|
|
43
|
+
是 Windows 的 `%APPDATA%\agent-bridge\state`。
|
|
44
|
+
|
|
45
|
+
## npm 插件
|
|
46
|
+
|
|
47
|
+
默认包 `mu-agent-bridge` 内置千问办公和管理页面;其他插件单独发布为 npm 包。安装与发布说明见
|
|
48
|
+
[`docs/npm-publishing.md`](docs/npm-publishing.md)。
|
|
49
|
+
|
|
50
|
+
`mab start` 启动时会查询核心包和托管插件的 npm 最新版本,检查状态保存于 mab 状态目录的
|
|
51
|
+
`versions.json`,abweb 的“版本管理”区域可查看。托管插件发现新版本后会自动下载、验证并替换;
|
|
52
|
+
升级失败会恢复旧插件并继续启动。Windows 的 `start-mab.ps1` 登录计划任务会在启动 daemon 前
|
|
53
|
+
更新全局核心包,健康检查失败时恢复备份。全局单独安装的插件及 Linux 核心自动升级暂不受管理。
|
|
54
|
+
|
|
55
|
+
运行时会发现核心包依赖和 `AGENT_BRIDGE_PLUGIN_PACKAGES` 中声明的 npm 包。插件包在自己的
|
|
56
|
+
`package.json` 中声明 `agentBridge.plugin`,平台复用统一的 `activate`、`deactivate`、`health`、
|
|
57
|
+
`handle` 生命周期。已加载插件可以通过管理 API 重载;新增 npm 包后重启 daemon 最可靠。
|
|
58
|
+
|
|
59
|
+
插件仍在 daemon 的同一个 Node.js 进程内运行。修改插件入口依赖后应重启 daemon,避免模块缓存
|
|
60
|
+
影响新代码。
|
|
61
|
+
|
|
62
|
+
npm 尚未发布时,核心包内置的千问办公与 abweb 仍会出现在插件下拉框。abweb 的“安装本地插件”
|
|
63
|
+
可填写 **Windows 主机上**包含 `plugin.json` 的目录路径,例如编译后的
|
|
64
|
+
`C:\path\to\agent_bridge\packages\dsh`;安装后可在下拉框中启动。已内置的千问办公无需再次安装。
|
|
65
|
+
“安装 npm 插件”只适用于包已经公开发布的情况。npm 发布令牌仅供发布脚本向 npm 官网认证,
|
|
66
|
+
与 abweb 配置码无关;账号锁定期间无需填写发布令牌。
|
|
67
|
+
|
|
68
|
+
## 开发目录
|
|
69
|
+
|
|
70
|
+
开发 checkout 仍支持 `plugins/<id>/plugin.json` 平铺目录。设置 `AGENT_BRIDGE_PLUGINS_DIR`
|
|
71
|
+
可以使用其他插件目录;这条路径用于本地开发,不是公开 npm 包的安装方式。
|
|
72
|
+
|
|
73
|
+
## 旧桥入口
|
|
74
|
+
|
|
75
|
+
仓库仍保留 Python bridge 作为兼容入口:
|
|
76
|
+
|
|
77
|
+
- `src/llprobe-qwen-bridge.py`:连接千问办公 CDP `9226`,默认身份 `qwenbg.agentid.cn`
|
|
78
|
+
- `src/llprobe-wb-bridge.py`:调用 CodeBuddy CLI
|
|
79
|
+
|
|
80
|
+
## 准备环境
|
|
81
|
+
|
|
82
|
+
在 Windows PowerShell 中执行:
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
.\setup.ps1
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
脚本会创建 `.venv` 并安装 `fastaun` 与 `aiohttp`。CodeBuddy 桥还需要已登录的
|
|
89
|
+
`@tencent-ai/codebuddy-code` 和 PATH 中的 Node.js。若 CodeBuddy 不在默认 npm
|
|
90
|
+
目录,可设置:
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
$env:CODEBUDDY_NPM_DIR = 'C:\path\to\node_modules\@tencent-ai\codebuddy-code'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
千问工作区默认使用 `%APPDATA%\QwenWorkCN\workspace`,可用 `$env:QWEN_WORKSPACE`
|
|
97
|
+
覆盖;AUN 数据默认使用 `$env:EVOLCORE_HOME` 或用户目录下的 `.evolcore`。
|
|
98
|
+
千问 CDP 端口默认为 `9226`,也可用 `$env:QWEN_CDP_PORT` 覆盖。
|
|
99
|
+
|
|
100
|
+
## 自检与运行
|
|
101
|
+
|
|
102
|
+
```powershell
|
|
103
|
+
.\.venv\Scripts\python.exe src\llprobe-wb-bridge.py --test "你好"
|
|
104
|
+
.\.venv\Scripts\python.exe src\llprobe-qwen-bridge.py --probe
|
|
105
|
+
.\.venv\Scripts\python.exe src\llprobe-qwen-bridge.py --direct "hi"
|
|
106
|
+
.\.venv\Scripts\python.exe src\llprobe-wb-bridge.py --aid <AID>
|
|
107
|
+
.\.venv\Scripts\python.exe src\llprobe-qwen-bridge.py --aid <AID>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
两个桥都只加载已有 AID;首次使用前请先在相同的 `EVOLCORE_HOME` 中注册身份。
|
|
111
|
+
|
|
112
|
+
## TypeScript 自检
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
npm install
|
|
116
|
+
npm test
|
|
117
|
+
npm run self-test -- hi
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Agent 通过 `AgentRegistry` 注册;DSH 也可以作为动态 `dsh` 插件加载。DeepSeek Harness 不在默认
|
|
121
|
+
Windows 路径时,可设置 `DSH_INSTALL_DIR`。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentError = void 0;
|
|
4
|
+
class AgentError extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = "AgentError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.AgentError = AgentError;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BridgeRouter = void 0;
|
|
4
|
+
class BridgeRouter {
|
|
5
|
+
registry;
|
|
6
|
+
agentName;
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
constructor(registry, agentName) {
|
|
9
|
+
this.registry = registry;
|
|
10
|
+
this.agentName = agentName;
|
|
11
|
+
}
|
|
12
|
+
async handle(sender, task) {
|
|
13
|
+
const response = await this.registry.create(this.agentName).send({ task, sessionId: this.sessions.get(sender) });
|
|
14
|
+
if (response.sessionId)
|
|
15
|
+
this.sessions.set(sender, response.sessionId);
|
|
16
|
+
return response;
|
|
17
|
+
}
|
|
18
|
+
/** Route a model request through the dynamic plugin registry when available. */
|
|
19
|
+
async handleModel(model, task, routes) {
|
|
20
|
+
return routes.handle(model, { task, sessionId: this.sessions.get(model) });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.BridgeRouter = BridgeRouter;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const node_path_1 = require("node:path");
|
|
38
|
+
const node_http_1 = require("node:http");
|
|
39
|
+
const node_fs_1 = require("node:fs");
|
|
40
|
+
const node_os_1 = require("node:os");
|
|
41
|
+
const control_server_1 = require("./control-server");
|
|
42
|
+
const dsh_agent_1 = require("./dsh-agent");
|
|
43
|
+
const registry_1 = require("./registry");
|
|
44
|
+
const bridge_1 = require("./bridge");
|
|
45
|
+
const plugin_manager_1 = require("./plugin-manager");
|
|
46
|
+
const logger = {
|
|
47
|
+
info(message, fields) { console.log(`[agent-bridge] ${message}`, fields ?? ""); },
|
|
48
|
+
warn(message, fields) { console.warn(`[agent-bridge] ${message}`, fields ?? ""); },
|
|
49
|
+
error(message, fields) { console.error(`[agent-bridge] ${message}`, fields ?? ""); },
|
|
50
|
+
};
|
|
51
|
+
function stateDirectory() {
|
|
52
|
+
if (process.env.AGENT_BRIDGE_STATE_DIR)
|
|
53
|
+
return (0, node_path_1.resolve)(process.env.AGENT_BRIDGE_STATE_DIR);
|
|
54
|
+
const appData = process.env.APPDATA ?? (0, node_path_1.resolve)((0, node_os_1.homedir)(), ".agent-bridge");
|
|
55
|
+
return (0, node_path_1.resolve)(appData, "agent-bridge", "state");
|
|
56
|
+
}
|
|
57
|
+
async function selfTest(task) {
|
|
58
|
+
const registry = new registry_1.AgentRegistry();
|
|
59
|
+
const compiledPatch = (0, node_path_1.resolve)(__dirname, "headless-account.patch.yml");
|
|
60
|
+
const sourcePatch = (0, node_path_1.resolve)(process.cwd(), "src-ts", "headless-account.patch.yml");
|
|
61
|
+
const patchPath = (0, node_fs_1.existsSync)(compiledPatch) ? compiledPatch : sourcePatch;
|
|
62
|
+
registry.register("dsh", () => new dsh_agent_1.DshAgent({ patchPath }));
|
|
63
|
+
const response = await new bridge_1.BridgeRouter(registry, "dsh").handle("self-test", task);
|
|
64
|
+
console.log(response.text);
|
|
65
|
+
if (response.sessionId)
|
|
66
|
+
console.error(`session=${response.sessionId}`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
async function pair() {
|
|
70
|
+
const port = Number(process.env.AGENT_BRIDGE_PORT ?? "26107");
|
|
71
|
+
const result = await new Promise((resolve, reject) => {
|
|
72
|
+
const req = (0, node_http_1.request)({ host: "127.0.0.1", port, path: "/api/pair-code", method: "GET" }, (res) => {
|
|
73
|
+
let body = "";
|
|
74
|
+
res.setEncoding("utf8");
|
|
75
|
+
res.on("data", (chunk) => body += chunk);
|
|
76
|
+
res.on("end", () => {
|
|
77
|
+
let value;
|
|
78
|
+
try {
|
|
79
|
+
value = JSON.parse(body);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
reject(new Error("配对码响应格式错误"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (res.statusCode === 200)
|
|
86
|
+
resolve(value);
|
|
87
|
+
else
|
|
88
|
+
reject(new Error(value.error ?? `HTTP ${res.statusCode}`));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
req.on("error", reject);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
const minutes = Math.max(0, Math.round(((result.expiresAt ?? Date.now()) - Date.now()) / 60000));
|
|
95
|
+
console.log(`配对码: ${result.code}(约 ${minutes} 分钟内有效,仅可使用一次)`);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
async function serve() {
|
|
99
|
+
const manager = new plugin_manager_1.PluginManager({
|
|
100
|
+
pluginsDir: (0, node_path_1.resolve)(process.env.AGENT_BRIDGE_PLUGINS_DIR ?? (0, node_path_1.resolve)(__dirname, "..", "..", "plugins")),
|
|
101
|
+
stateDir: stateDirectory(),
|
|
102
|
+
logger,
|
|
103
|
+
});
|
|
104
|
+
await manager.discover();
|
|
105
|
+
await manager.versions.load();
|
|
106
|
+
const coreVersion = await Promise.resolve().then(() => __importStar(require("node:fs/promises"))).then(({ readFile }) => readFile((0, node_path_1.resolve)(__dirname, "../../package.json"), "utf8").then((value) => JSON.parse(value).version).catch(() => "0.0.0"));
|
|
107
|
+
const checkedVersions = await Promise.all([manager.versions.checkPackage("mu-agent-bridge", coreVersion), ...manager.list().map((plugin) => {
|
|
108
|
+
const info = manager.packageInfo(plugin.id);
|
|
109
|
+
return info.packageName && info.managed ? manager.versions.checkPackage(info.packageName, plugin.version) : Promise.resolve(undefined);
|
|
110
|
+
})]);
|
|
111
|
+
for (const status of checkedVersions.slice(1)) {
|
|
112
|
+
if (status?.state !== "update-available")
|
|
113
|
+
continue;
|
|
114
|
+
const plugin = manager.list().find((record) => manager.packageInfo(record.id).packageName === status.packageName);
|
|
115
|
+
if (!plugin)
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
await manager.upgradePackage(plugin.id, status.packageName, status.latestVersion);
|
|
119
|
+
logger.info(`插件已升级:${plugin.id}`, { version: status.latestVersion });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.error(`插件升级失败,保留旧版本:${plugin.id}`, { error: error instanceof Error ? error.message : String(error) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const port = Number(process.env.AGENT_BRIDGE_PORT ?? "26107");
|
|
126
|
+
const host = process.env.AGENT_BRIDGE_HOST ?? "127.0.0.1";
|
|
127
|
+
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1" && !process.env.AGENT_BRIDGE_TOKEN) {
|
|
128
|
+
throw new Error("局域网监听必须设置 AGENT_BRIDGE_TOKEN");
|
|
129
|
+
}
|
|
130
|
+
const server = new control_server_1.ControlServer({ manager, host, port, token: process.env.AGENT_BRIDGE_TOKEN });
|
|
131
|
+
const address = await server.start();
|
|
132
|
+
logger.info(`daemon listening on http://${address.host}:${address.port}`);
|
|
133
|
+
const order = ["abweb", ...manager.list().map((plugin) => plugin.id).filter((id) => id !== "abweb")];
|
|
134
|
+
for (const id of order) {
|
|
135
|
+
try {
|
|
136
|
+
await manager.start(id);
|
|
137
|
+
logger.info(`plugin ready: ${id}`);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error(`plugin failed: ${id}`, { error: error instanceof Error ? error.message : String(error) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let stopping = false;
|
|
144
|
+
const shutdown = async () => {
|
|
145
|
+
if (stopping)
|
|
146
|
+
return;
|
|
147
|
+
stopping = true;
|
|
148
|
+
for (const plugin of manager.list().reverse()) {
|
|
149
|
+
try {
|
|
150
|
+
await manager.stop(plugin.id);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.warn(`plugin stop failed: ${plugin.id}`, { error: String(error) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await server.stop();
|
|
157
|
+
};
|
|
158
|
+
process.once("SIGINT", () => void shutdown().finally(() => process.exit(0)));
|
|
159
|
+
process.once("SIGTERM", () => void shutdown().finally(() => process.exit(0)));
|
|
160
|
+
await new Promise(() => undefined);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
async function main() {
|
|
164
|
+
const args = process.argv.slice(2);
|
|
165
|
+
if (args.includes("pair"))
|
|
166
|
+
return pair();
|
|
167
|
+
if (args[0] === "update" && args.includes("--startup")) {
|
|
168
|
+
console.log("核心升级由 Windows 启动器执行;请更新全局 mu-agent-bridge 后重新启动。");
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
const selfTestIndex = args.indexOf("--self-test");
|
|
172
|
+
if (selfTestIndex >= 0)
|
|
173
|
+
return selfTest(args[selfTestIndex + 1] ?? "hi");
|
|
174
|
+
if (args.includes("--serve") || args.includes("start"))
|
|
175
|
+
return serve();
|
|
176
|
+
console.error("用法:mab start | mab --self-test <任务>");
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
main().then((code) => { process.exitCode = code; }).catch((error) => {
|
|
180
|
+
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ControlServer = void 0;
|
|
4
|
+
exports.configureAidForPlugin = configureAidForPlugin;
|
|
5
|
+
exports.listExistingAids = listExistingAids;
|
|
6
|
+
exports.listConnectedAids = listConnectedAids;
|
|
7
|
+
const node_http_1 = require("node:http");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_util_1 = require("node:util");
|
|
11
|
+
const node_os_1 = require("node:os");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
15
|
+
const AID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?){2,}$/;
|
|
16
|
+
async function runEc(args) {
|
|
17
|
+
const command = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : "ec";
|
|
18
|
+
const argv = process.platform === "win32" ? ["/d", "/s", "/c", `ec ${args.join(" ")}`] : args;
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execFileAsync(command, argv, { timeout: 120_000, maxBuffer: 1024 * 1024 });
|
|
21
|
+
return JSON.parse(stdout);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const stdout = error.stdout;
|
|
25
|
+
if (stdout) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(stdout);
|
|
28
|
+
}
|
|
29
|
+
catch { /* 保留原始命令错误 */ }
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function configureAidForPlugin(pluginId, aid, port, run = runEc) {
|
|
35
|
+
if (!aid || (pluginId !== "dsh" && pluginId !== "qwenwork"))
|
|
36
|
+
return;
|
|
37
|
+
const model = pluginId === "dsh" ? "dsh" : "qwenwork";
|
|
38
|
+
const path = pluginId === "dsh" ? "/dsh/v1" : "/v1";
|
|
39
|
+
const settings = [
|
|
40
|
+
["baseagents.ecagent.baseUrl", `http://127.0.0.1:${port}${path}`],
|
|
41
|
+
["baseagents.ecagent.apiKey", "mab-local"],
|
|
42
|
+
["baseagents.ecagent.model", model],
|
|
43
|
+
["active_baseagent", "ecagent"],
|
|
44
|
+
];
|
|
45
|
+
for (const [key, value] of settings) {
|
|
46
|
+
const result = await run(["agent", "set", aid, key, value, "--format", "json"]);
|
|
47
|
+
if (!result?.ok)
|
|
48
|
+
throw new Error(`配置 AID ${aid} 失败:${key}`);
|
|
49
|
+
}
|
|
50
|
+
const reloaded = await run(["agent", "reload", aid, "--format", "json"]);
|
|
51
|
+
if (!reloaded?.ok)
|
|
52
|
+
throw new Error(`重载 AID ${aid} 失败:${reloaded?.error ?? reloaded?.code ?? "未知错误"}`);
|
|
53
|
+
}
|
|
54
|
+
class ControlServer {
|
|
55
|
+
options;
|
|
56
|
+
server;
|
|
57
|
+
address;
|
|
58
|
+
pairingCode = String((0, node_crypto_1.randomInt)(0, 1_000_000)).padStart(6, "0");
|
|
59
|
+
pairingExpiresAt = Date.now() + 10 * 60 * 1000;
|
|
60
|
+
pairedTokens = new Set();
|
|
61
|
+
pairingUsed = false;
|
|
62
|
+
pairingFailures = 0;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.options = options;
|
|
65
|
+
this.server = (0, node_http_1.createServer)((req, res) => { void this.handle(req, res); });
|
|
66
|
+
}
|
|
67
|
+
async start() { return new Promise((resolve, reject) => { const host = this.options.host ?? "127.0.0.1"; this.server.once("error", reject); this.server.listen(this.options.port ?? 0, host, () => { this.server.removeListener("error", reject); const info = this.server.address(); if (!info || typeof info === "string")
|
|
68
|
+
return reject(new Error("failed to bind control server")); this.address = { host, port: info.port }; resolve(this.address); }); }); }
|
|
69
|
+
async stop() { if (!this.server.listening)
|
|
70
|
+
return; await new Promise((resolve, reject) => this.server.close((error) => error ? reject(error) : resolve())); }
|
|
71
|
+
async handle(req, res) {
|
|
72
|
+
try {
|
|
73
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
74
|
+
const method = req.method ?? "GET";
|
|
75
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
76
|
+
if (method === "GET" && url.pathname === "/api/pair-code") {
|
|
77
|
+
if (!isLoopback(req.socket.remoteAddress))
|
|
78
|
+
return this.json(res, 403, { error: "配对码只能在本机获取" });
|
|
79
|
+
if (Date.now() >= this.pairingExpiresAt || this.pairingUsed || this.pairingFailures >= 5) {
|
|
80
|
+
this.pairingCode = String((0, node_crypto_1.randomInt)(0, 1_000_000)).padStart(6, "0");
|
|
81
|
+
this.pairingExpiresAt = Date.now() + 10 * 60 * 1000;
|
|
82
|
+
this.pairingUsed = false;
|
|
83
|
+
this.pairingFailures = 0;
|
|
84
|
+
}
|
|
85
|
+
return this.json(res, 200, { code: this.pairingCode, expiresAt: this.pairingExpiresAt });
|
|
86
|
+
}
|
|
87
|
+
if (method === "POST" && url.pathname === "/api/pair") {
|
|
88
|
+
const body = await this.body(req);
|
|
89
|
+
if (this.pairingUsed || Date.now() >= this.pairingExpiresAt || this.pairingFailures >= 5)
|
|
90
|
+
return this.json(res, 403, { ok: false, reason: "配对码无效或已过期" });
|
|
91
|
+
if (body.code !== this.pairingCode) {
|
|
92
|
+
this.pairingFailures++;
|
|
93
|
+
return this.json(res, 403, { ok: false, reason: "配对码无效或已过期" });
|
|
94
|
+
}
|
|
95
|
+
this.pairingUsed = true;
|
|
96
|
+
const token = (0, node_crypto_1.randomBytes)(32).toString("hex");
|
|
97
|
+
this.pairedTokens.add(token);
|
|
98
|
+
return this.json(res, 200, { ok: true, token });
|
|
99
|
+
}
|
|
100
|
+
const authorization = req.headers.authorization;
|
|
101
|
+
const authorized = authorization === `Bearer ${this.options.token}` || (authorization?.startsWith("Bearer ") && this.pairedTokens.has(authorization.slice(7)));
|
|
102
|
+
if (this.options.token && !isLoopback(req.socket.remoteAddress) && !authorized && !(method === "GET" && url.pathname.startsWith("/abweb")))
|
|
103
|
+
return this.json(res, 401, { error: "unauthorized" });
|
|
104
|
+
if (method === "GET" && url.pathname === "/api/health")
|
|
105
|
+
return this.json(res, 200, { status: "ok", plugins: this.options.manager.list().length });
|
|
106
|
+
if (method === "GET" && url.pathname === "/api/versions")
|
|
107
|
+
return this.json(res, 200, { core: this.options.manager.versions.get("mu-agent-bridge") ?? null, plugins: this.options.manager.list().map((plugin) => ({ ...plugin, versionStatus: this.options.manager.versions.get(`agent-bridge-${plugin.id}`) ?? null })) });
|
|
108
|
+
if (method === "POST" && url.pathname === "/api/versions/check")
|
|
109
|
+
return this.json(res, 200, { versions: await this.options.manager.checkPackageUpdates() });
|
|
110
|
+
if (method === "GET" && url.pathname === "/api/messages")
|
|
111
|
+
return this.json(res, 200, { messages: this.options.manager.recentMessages() });
|
|
112
|
+
if (method === "GET" && url.pathname === "/api/aids") {
|
|
113
|
+
const aids = await listConnectedAids();
|
|
114
|
+
const created = new Set(await this.options.manager.createdAids?.() ?? []);
|
|
115
|
+
return this.json(res, 200, { aids, groups: { evolcore: aids.filter((aid) => !created.has(aid)), muAgentBridge: aids.filter((aid) => created.has(aid)) } });
|
|
116
|
+
}
|
|
117
|
+
if (method === "POST" && url.pathname === "/api/aids") {
|
|
118
|
+
const body = await this.body(req);
|
|
119
|
+
if (typeof body.aid !== "string" || !AID_PATTERN.test(body.aid) || typeof body.ownerAid !== "string" || !AID_PATTERN.test(body.ownerAid))
|
|
120
|
+
return this.json(res, 400, { error: "需要有效的 AID 和 owner AID" });
|
|
121
|
+
const project = (0, node_path_1.join)((0, node_os_1.homedir)(), ".mab", "projects", body.aid);
|
|
122
|
+
const result = await runEc(["agent", "new", body.aid, "--non-interactive", "--owner", body.ownerAid, "--project", project, "--name", body.aid, "--description", `mu-agent-bridge 的 ${body.aid} Agent`, "--format", "json"]);
|
|
123
|
+
if (result.ok)
|
|
124
|
+
await this.options.manager.recordCreatedAid(body.aid);
|
|
125
|
+
return this.json(res, result.ok ? 201 : 400, result);
|
|
126
|
+
}
|
|
127
|
+
if (method === "GET" && url.pathname === "/api/plugins")
|
|
128
|
+
return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
|
|
129
|
+
if (method === "GET" && url.pathname === "/api/routes")
|
|
130
|
+
return this.json(res, 200, []);
|
|
131
|
+
if (parts[0] === "api" && parts[1] === "plugins" && parts[2]) {
|
|
132
|
+
const id = decodeURIComponent(parts[2]);
|
|
133
|
+
if (method === "POST" && parts[3] === "start") {
|
|
134
|
+
await this.options.manager.start(id);
|
|
135
|
+
return this.json(res, 200, { ok: true });
|
|
136
|
+
}
|
|
137
|
+
if (method === "POST" && parts[3] === "stop") {
|
|
138
|
+
await this.options.manager.stop(id);
|
|
139
|
+
return this.json(res, 200, { ok: true });
|
|
140
|
+
}
|
|
141
|
+
if (method === "POST" && parts[3] === "reload") {
|
|
142
|
+
await this.options.manager.reload(id);
|
|
143
|
+
return this.json(res, 200, { ok: true });
|
|
144
|
+
}
|
|
145
|
+
if (method === "POST" && parts[3] === "update") {
|
|
146
|
+
const body = await this.body(req);
|
|
147
|
+
return this.json(res, 200, { ok: true, plugin: await this.options.manager.upgradePackage(id, body.packageName, body.version) });
|
|
148
|
+
}
|
|
149
|
+
if (method === "DELETE" && parts.length === 3) {
|
|
150
|
+
await this.options.manager.uninstall(id);
|
|
151
|
+
return this.json(res, 200, { ok: true });
|
|
152
|
+
}
|
|
153
|
+
if (method === "GET" && parts[3] === "config")
|
|
154
|
+
return this.json(res, 200, maskSecrets(await this.options.manager.config(id)));
|
|
155
|
+
if (method === "PATCH" && parts[3] === "aid") {
|
|
156
|
+
const body = await this.body(req);
|
|
157
|
+
if (typeof body.aid !== "string" || (body.aid && !(await listConnectedAids()).includes(body.aid)))
|
|
158
|
+
return this.json(res, 400, { error: "AID 当前未连接" });
|
|
159
|
+
if (body.aid && (id === "dsh" || id === "qwenwork") && this.options.manager.get(id)?.record.status !== "ready")
|
|
160
|
+
return this.json(res, 409, { error: "插件未启动" });
|
|
161
|
+
await configureAidForPlugin(id, body.aid, this.address.port);
|
|
162
|
+
await this.options.manager.setAid(id, body.aid);
|
|
163
|
+
return this.json(res, 200, { ok: true });
|
|
164
|
+
}
|
|
165
|
+
if (method === "PATCH" && parts[3] === "config") {
|
|
166
|
+
await this.options.manager.saveConfig(id, await this.body(req));
|
|
167
|
+
return this.json(res, 200, { ok: true });
|
|
168
|
+
}
|
|
169
|
+
if (method === "GET" && parts[3] === "health")
|
|
170
|
+
return this.json(res, 200, await this.options.manager.health(id));
|
|
171
|
+
if (method === "GET" && parts[3] === "logs")
|
|
172
|
+
return this.json(res, 200, { logs: this.options.manager.recentMessages(id) });
|
|
173
|
+
}
|
|
174
|
+
if (method === "POST" && url.pathname === "/api/plugins/install") {
|
|
175
|
+
const body = await this.body(req);
|
|
176
|
+
if (body.packageName)
|
|
177
|
+
return this.json(res, 201, await this.options.manager.installPackage(body.packageName));
|
|
178
|
+
if (body.source)
|
|
179
|
+
return this.json(res, 201, await this.options.manager.install(body.source));
|
|
180
|
+
return this.json(res, 400, { error: "需要 packageName 或 source" });
|
|
181
|
+
}
|
|
182
|
+
if (await this.options.manager.dispatchHttp(req, res))
|
|
183
|
+
return;
|
|
184
|
+
return this.json(res, 404, { error: "not found" });
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const status = error.statusCode ?? 400;
|
|
188
|
+
return this.json(res, status, { error: error instanceof Error ? error.message : String(error) });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async body(req) { let raw = ""; for await (const chunk of req)
|
|
192
|
+
raw += chunk; if (!raw)
|
|
193
|
+
return {}; try {
|
|
194
|
+
return JSON.parse(raw);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new Error("invalid JSON body");
|
|
198
|
+
} }
|
|
199
|
+
json(res, status, value) { res.statusCode = status; res.setHeader("content-type", "application/json; charset=utf-8"); res.end(JSON.stringify(value)); }
|
|
200
|
+
}
|
|
201
|
+
exports.ControlServer = ControlServer;
|
|
202
|
+
function isLoopback(address) {
|
|
203
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
204
|
+
}
|
|
205
|
+
async function listExistingAids() {
|
|
206
|
+
const root = (0, node_path_1.resolve)(process.env.EVOLCORE_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".evolcore"), "AIDs");
|
|
207
|
+
let names;
|
|
208
|
+
try {
|
|
209
|
+
names = await (0, promises_1.readdir)(root);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
const available = await Promise.all(names.filter((name) => /^[a-zA-Z0-9.-]+$/.test(name)).map(async (name) => {
|
|
215
|
+
try {
|
|
216
|
+
await (0, promises_1.access)((0, node_path_1.join)(root, name, "private", "key.json"));
|
|
217
|
+
return name;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}));
|
|
223
|
+
return available.filter((name) => Boolean(name)).sort();
|
|
224
|
+
}
|
|
225
|
+
async function listConnectedAids(run = runEc) {
|
|
226
|
+
const result = await run(["agent", "list", "--format", "json"]);
|
|
227
|
+
if (!result.ok || !Array.isArray(result.agents))
|
|
228
|
+
throw new Error("读取 EvolCore Agent 列表失败");
|
|
229
|
+
const candidates = result.agents.filter((agent) => agent.status === "running" && typeof agent.aid === "string" && AID_PATTERN.test(agent.aid) && agent.channels?.some((channel) => channel.startsWith("aun#")));
|
|
230
|
+
const details = await Promise.all(candidates.map((agent) => run(["agent", "show", agent.aid, "--format", "json"]).catch(() => null)));
|
|
231
|
+
const local = new Set(await listExistingAids());
|
|
232
|
+
return details.filter((detail) => detail?.ok && detail.status === "running" && detail.connection?.status === "connected" && local.has(detail.aid)).map((detail) => detail.aid).sort();
|
|
233
|
+
}
|
|
234
|
+
function maskSecrets(value) {
|
|
235
|
+
if (Array.isArray(value))
|
|
236
|
+
return value.map(maskSecrets);
|
|
237
|
+
if (!value || typeof value !== "object")
|
|
238
|
+
return value;
|
|
239
|
+
const result = {};
|
|
240
|
+
for (const [key, item] of Object.entries(value))
|
|
241
|
+
result[key] = /token|secret|password|passwd|api[-_]?key/i.test(key) ? "******" : maskSecrets(item);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DshAgent = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const agent_1 = require("./agent");
|
|
6
|
+
const dsh_paths_1 = require("./dsh-paths");
|
|
7
|
+
class DshAgent {
|
|
8
|
+
name = "dsh";
|
|
9
|
+
paths;
|
|
10
|
+
spawnProcess;
|
|
11
|
+
defaultTimeoutMs;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.paths = options.paths ?? (0, dsh_paths_1.resolveDshPaths)(options.patchPath);
|
|
14
|
+
this.spawnProcess = options.spawn ?? node_child_process_1.spawn;
|
|
15
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 300_000;
|
|
16
|
+
}
|
|
17
|
+
async send(request) {
|
|
18
|
+
if (!request.task.trim())
|
|
19
|
+
throw new agent_1.AgentError("DSH task must not be empty");
|
|
20
|
+
const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
|
|
21
|
+
if (request.sessionId)
|
|
22
|
+
args.push("--session-id", request.sessionId);
|
|
23
|
+
args.push("-");
|
|
24
|
+
const child = this.spawnProcess(this.paths.executable, args, {
|
|
25
|
+
cwd: request.cwd,
|
|
26
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
});
|
|
29
|
+
child.stdin.end(request.task, "utf8");
|
|
30
|
+
let output = "";
|
|
31
|
+
let errors = "";
|
|
32
|
+
let sessionId = request.sessionId;
|
|
33
|
+
let finalText = "";
|
|
34
|
+
child.stdout.setEncoding?.("utf8");
|
|
35
|
+
child.stderr.setEncoding?.("utf8");
|
|
36
|
+
child.stdout.on("data", (chunk) => {
|
|
37
|
+
output += chunk;
|
|
38
|
+
for (const line of output.split(/\r?\n/).slice(0, -1)) {
|
|
39
|
+
try {
|
|
40
|
+
const event = JSON.parse(line);
|
|
41
|
+
if (event.type === "session" && event.sessionId)
|
|
42
|
+
sessionId = event.sessionId;
|
|
43
|
+
if (event.type === "final" && event.text)
|
|
44
|
+
finalText = event.text.trim();
|
|
45
|
+
}
|
|
46
|
+
catch { /* diagnostics and partial lines are ignored */ }
|
|
47
|
+
}
|
|
48
|
+
output = output.split(/\r?\n/).at(-1) ?? "";
|
|
49
|
+
});
|
|
50
|
+
child.stderr.on("data", (chunk) => { errors += chunk; });
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
child.kill();
|
|
54
|
+
reject(new agent_1.AgentError(`DSH task timed out after ${request.timeoutMs ?? this.defaultTimeoutMs}ms`));
|
|
55
|
+
}, request.timeoutMs ?? this.defaultTimeoutMs);
|
|
56
|
+
child.once("error", (error) => { clearTimeout(timer); reject(new agent_1.AgentError("failed to start DSH", { cause: error })); });
|
|
57
|
+
child.once("close", (code) => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
if (code !== 0 && !finalText)
|
|
60
|
+
reject(new agent_1.AgentError(errors.trim() || `DSH exited with code ${code}`));
|
|
61
|
+
else
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
if (!finalText)
|
|
66
|
+
throw new agent_1.AgentError("DSH returned no final text");
|
|
67
|
+
return { text: finalText, sessionId };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.DshAgent = DshAgent;
|