openilink-app-runner 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 +154 -0
- package/dist/bin/runner.d.ts +2 -0
- package/dist/bin/runner.js +138 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +74 -0
- package/dist/src/executor.d.ts +2 -0
- package/dist/src/executor.js +33 -0
- package/dist/src/handler.d.ts +2 -0
- package/dist/src/handler.js +29 -0
- package/dist/src/hub.d.ts +15 -0
- package/dist/src/hub.js +89 -0
- package/dist/src/sync.d.ts +2 -0
- package/dist/src/sync.js +30 -0
- package/dist/src/types.d.ts +26 -0
- package/dist/src/types.js +2 -0
- package/package.json +31 -0
- package/runner.example.yaml +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# openilink-app-runner
|
|
2
|
+
|
|
3
|
+
将本地命令行工具桥接到微信 —— 通过 OpeniLink Hub 接收微信命令,在本地执行,返回结果。
|
|
4
|
+
|
|
5
|
+
## 工作原理
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
微信用户 → /weather 北京 → Hub → WebSocket → Runner → curl wttr.in/北京 → 结果返回微信
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
1. 在 YAML 配置中定义命令(名称 + shell 命令)
|
|
12
|
+
2. Runner 启动时自动同步命令到 Hub
|
|
13
|
+
3. 通过 WebSocket 保持连接,接收来自微信的命令事件
|
|
14
|
+
4. 在本地执行对应 shell 命令,将输出返回给微信用户
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g openilink-app-runner
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
### 1. 初始化配置
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
openilink-app-runner init --hub-url https://hub.openilink.com --token app_xxx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
这会在当前目录创建 `runner.yaml` 配置文件。
|
|
31
|
+
|
|
32
|
+
### 2. 添加命令
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 添加查天气命令
|
|
36
|
+
openilink-app-runner add weather "curl -s 'wttr.in/\${args}?format=3'" -d "查天气" -t 5
|
|
37
|
+
|
|
38
|
+
# 添加查 IP 命令
|
|
39
|
+
openilink-app-runner add ip "curl -s ifconfig.me" -d "查公网 IP" -t 5
|
|
40
|
+
|
|
41
|
+
# 查看已添加的命令
|
|
42
|
+
openilink-app-runner list
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
添加命令时会自动同步到 Hub。
|
|
46
|
+
|
|
47
|
+
### 3. 启动
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
openilink-app-runner start
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
现在在微信中发送 `/weather 北京` 即可看到天气结果。
|
|
54
|
+
|
|
55
|
+
## CLI 命令
|
|
56
|
+
|
|
57
|
+
| 命令 | 说明 |
|
|
58
|
+
|------|------|
|
|
59
|
+
| `init --hub-url <url> --token <token>` | 初始化配置文件 |
|
|
60
|
+
| `add <name> <exec> [-d desc] [-t timeout]` | 添加命令 |
|
|
61
|
+
| `remove <name>` | 删除命令 |
|
|
62
|
+
| `list` | 查看已配置的命令 |
|
|
63
|
+
| `sync` | 手动同步命令到 Hub |
|
|
64
|
+
| `start` | 启动 runner,连接 Hub 并监听命令 |
|
|
65
|
+
|
|
66
|
+
所有命令支持 `-c, --config <path>` 指定配置文件路径,默认为 `runner.yaml`。
|
|
67
|
+
|
|
68
|
+
## 配置文件格式
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
hub_url: "https://hub.openilink.com"
|
|
72
|
+
app_token: "app_your_token_here"
|
|
73
|
+
max_output: 2000 # 输出最大字符数
|
|
74
|
+
|
|
75
|
+
commands:
|
|
76
|
+
weather:
|
|
77
|
+
description: "查天气"
|
|
78
|
+
exec: "curl -s 'wttr.in/${args}?format=3'"
|
|
79
|
+
timeout: 5 # 秒,默认 30
|
|
80
|
+
ip:
|
|
81
|
+
description: "查公网 IP"
|
|
82
|
+
exec: "curl -s ifconfig.me"
|
|
83
|
+
timeout: 5
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 字段说明
|
|
87
|
+
|
|
88
|
+
- `hub_url` — Hub 服务地址(必填)
|
|
89
|
+
- `app_token` — App Token,在 Hub 中创建 App 后获取(必填)
|
|
90
|
+
- `max_output` — 命令输出最大字符数,超出截断(默认 2000)
|
|
91
|
+
- `commands` — 命令映射
|
|
92
|
+
- `description` — 命令描述,会显示在微信中
|
|
93
|
+
- `exec` — 要执行的 shell 命令,支持 `${args}` 占位符接收用户输入
|
|
94
|
+
- `timeout` — 超时秒数(默认 30)
|
|
95
|
+
|
|
96
|
+
## 安全提示
|
|
97
|
+
|
|
98
|
+
`${args}` 会直接替换到 shell 命令中,存在命令注入风险。请注意:
|
|
99
|
+
|
|
100
|
+
- 仅在信任的微信群/用户中使用
|
|
101
|
+
- 避免在 `exec` 中使用 `${args}` 执行危险操作(如 `rm`、`sudo` 等)
|
|
102
|
+
- 设置合理的 `timeout` 防止命令卡住
|
|
103
|
+
- `max_output` 限制输出大小,防止刷屏
|
|
104
|
+
|
|
105
|
+
## 示例:集成 opencli
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
commands:
|
|
109
|
+
hn:
|
|
110
|
+
description: "HackerNews 热门"
|
|
111
|
+
exec: "opencli hackernews top --format json"
|
|
112
|
+
timeout: 10
|
|
113
|
+
github:
|
|
114
|
+
description: "GitHub 趋势"
|
|
115
|
+
exec: "opencli github trending ${args}"
|
|
116
|
+
timeout: 10
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 自动重连
|
|
120
|
+
|
|
121
|
+
Runner 会自动维护 WebSocket 连接。断开后每 5 秒自动重连,同时通过 ping/pong 保持心跳。
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## English
|
|
126
|
+
|
|
127
|
+
### What is this?
|
|
128
|
+
|
|
129
|
+
`openilink-app-runner` bridges local CLI commands to WeChat via [OpeniLink Hub](https://github.com/openilink/openilink-hub). Define commands in a YAML config, and WeChat users can invoke them by sending `/command args`.
|
|
130
|
+
|
|
131
|
+
### How it works
|
|
132
|
+
|
|
133
|
+
1. Define commands in `runner.yaml` (name + shell command)
|
|
134
|
+
2. Runner syncs commands to Hub on startup
|
|
135
|
+
3. Stays connected via WebSocket, receives command events from WeChat
|
|
136
|
+
4. Executes shell commands locally, returns output to WeChat
|
|
137
|
+
|
|
138
|
+
### Quick start
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm install -g openilink-app-runner
|
|
142
|
+
|
|
143
|
+
openilink-app-runner init --hub-url https://hub.openilink.com --token app_xxx
|
|
144
|
+
openilink-app-runner add weather "curl -s 'wttr.in/\${args}?format=3'" -d "Weather" -t 5
|
|
145
|
+
openilink-app-runner start
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Security
|
|
149
|
+
|
|
150
|
+
`${args}` is interpolated directly into shell commands. Only use in trusted environments. Set reasonable timeouts and output limits.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const config_1 = require("../src/config");
|
|
6
|
+
const sync_1 = require("../src/sync");
|
|
7
|
+
const hub_1 = require("../src/hub");
|
|
8
|
+
const handler_1 = require("../src/handler");
|
|
9
|
+
const program = new commander_1.Command();
|
|
10
|
+
program
|
|
11
|
+
.name("openilink-app-runner")
|
|
12
|
+
.description("Run local commands as OpeniLink Hub App tools")
|
|
13
|
+
.version("0.1.0");
|
|
14
|
+
program
|
|
15
|
+
.command("init")
|
|
16
|
+
.description("初始化配置文件")
|
|
17
|
+
.option("-c, --config <path>", "配置文件路径", "runner.yaml")
|
|
18
|
+
.requiredOption("--hub-url <url>", "Hub URL")
|
|
19
|
+
.requiredOption("--token <token>", "App Token")
|
|
20
|
+
.action((opts) => {
|
|
21
|
+
(0, config_1.initConfig)(opts.config, opts.hubUrl, opts.token);
|
|
22
|
+
console.log(`✓ 配置已写入 ${opts.config}`);
|
|
23
|
+
});
|
|
24
|
+
program
|
|
25
|
+
.command("add <name> <exec>")
|
|
26
|
+
.description("添加命令")
|
|
27
|
+
.option("-c, --config <path>", "配置文件路径", "runner.yaml")
|
|
28
|
+
.option("-d, --desc <description>", "命令描述")
|
|
29
|
+
.option("-t, --timeout <seconds>", "超时时间", "30")
|
|
30
|
+
.action(async (name, exec, opts) => {
|
|
31
|
+
const configPath = (0, config_1.getConfigPath)(opts.config);
|
|
32
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
33
|
+
config.commands[name] = {
|
|
34
|
+
exec,
|
|
35
|
+
description: opts.desc || name,
|
|
36
|
+
timeout: parseInt(opts.timeout, 10),
|
|
37
|
+
};
|
|
38
|
+
(0, config_1.saveConfig)(configPath, config);
|
|
39
|
+
console.log(`✓ 已添加命令 /${name}`);
|
|
40
|
+
// Auto-sync to Hub
|
|
41
|
+
try {
|
|
42
|
+
await (0, sync_1.syncTools)(config);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(`⚠ 同步到 Hub 失败: ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
program
|
|
49
|
+
.command("remove <name>")
|
|
50
|
+
.description("删除命令")
|
|
51
|
+
.option("-c, --config <path>", "配置文件路径", "runner.yaml")
|
|
52
|
+
.action(async (name, opts) => {
|
|
53
|
+
const configPath = (0, config_1.getConfigPath)(opts.config);
|
|
54
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
55
|
+
if (!config.commands[name]) {
|
|
56
|
+
console.error(`命令 /${name} 不存在`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
delete config.commands[name];
|
|
60
|
+
(0, config_1.saveConfig)(configPath, config);
|
|
61
|
+
console.log(`✓ 已删除命令 /${name}`);
|
|
62
|
+
// Auto-sync to Hub
|
|
63
|
+
try {
|
|
64
|
+
await (0, sync_1.syncTools)(config);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`⚠ 同步到 Hub 失败: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
program
|
|
71
|
+
.command("list")
|
|
72
|
+
.description("查看已配置的命令")
|
|
73
|
+
.option("-c, --config <path>", "配置文件路径", "runner.yaml")
|
|
74
|
+
.action((opts) => {
|
|
75
|
+
const config = (0, config_1.loadConfig)((0, config_1.getConfigPath)(opts.config));
|
|
76
|
+
const cmds = Object.entries(config.commands);
|
|
77
|
+
if (cmds.length === 0) {
|
|
78
|
+
console.log("暂无命令。使用 add <name> <exec> 添加。");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(`Hub: ${config.hub_url}`);
|
|
82
|
+
console.log(`命令 (${cmds.length}):`);
|
|
83
|
+
for (const [name, cmd] of cmds) {
|
|
84
|
+
console.log(` /${name} — ${cmd.description || "(无描述)"}`);
|
|
85
|
+
console.log(` exec: ${cmd.exec}`);
|
|
86
|
+
if (cmd.timeout)
|
|
87
|
+
console.log(` timeout: ${cmd.timeout}s`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
program
|
|
91
|
+
.command("sync")
|
|
92
|
+
.description("手动同步命令到 Hub")
|
|
93
|
+
.option("-c, --config <path>", "配置文件路径", "runner.yaml")
|
|
94
|
+
.action(async (opts) => {
|
|
95
|
+
const config = (0, config_1.loadConfig)((0, config_1.getConfigPath)(opts.config));
|
|
96
|
+
await (0, sync_1.syncTools)(config);
|
|
97
|
+
});
|
|
98
|
+
program
|
|
99
|
+
.command("start")
|
|
100
|
+
.description("启动 runner,连接 Hub 并监听命令")
|
|
101
|
+
.option("-c, --config <path>", "配置文件路径", "runner.yaml")
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
const config = (0, config_1.loadConfig)((0, config_1.getConfigPath)(opts.config));
|
|
104
|
+
const cmdCount = Object.keys(config.commands).length;
|
|
105
|
+
console.log(`openilink-app-runner v0.1.0`);
|
|
106
|
+
console.log(`Hub: ${config.hub_url}`);
|
|
107
|
+
console.log(`命令: ${cmdCount} 个`);
|
|
108
|
+
if (cmdCount === 0) {
|
|
109
|
+
console.log("⚠ 没有配置命令。使用 add <name> <exec> 添加。");
|
|
110
|
+
}
|
|
111
|
+
// Sync tools on startup
|
|
112
|
+
try {
|
|
113
|
+
await (0, sync_1.syncTools)(config);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(`⚠ 同步 tools 失败: ${err.message}`);
|
|
117
|
+
console.log("继续启动...");
|
|
118
|
+
}
|
|
119
|
+
// Connect to Hub
|
|
120
|
+
const handler = (0, handler_1.createHandler)(config);
|
|
121
|
+
const hub = new hub_1.HubConnection(config, handler);
|
|
122
|
+
// Handle graceful shutdown
|
|
123
|
+
process.on("SIGINT", () => {
|
|
124
|
+
console.log("\n正在停止...");
|
|
125
|
+
hub.stop();
|
|
126
|
+
process.exit(0);
|
|
127
|
+
});
|
|
128
|
+
process.on("SIGTERM", () => {
|
|
129
|
+
hub.stop();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
});
|
|
132
|
+
hub.connect();
|
|
133
|
+
});
|
|
134
|
+
// Default: show help
|
|
135
|
+
if (process.argv.length <= 2) {
|
|
136
|
+
program.help();
|
|
137
|
+
}
|
|
138
|
+
program.parse();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RunnerConfig } from "./types";
|
|
2
|
+
export declare function getConfigPath(custom?: string): string;
|
|
3
|
+
export declare function loadConfig(configPath: string): RunnerConfig;
|
|
4
|
+
export declare function saveConfig(configPath: string, config: RunnerConfig): void;
|
|
5
|
+
export declare function initConfig(configPath: string, hubUrl: string, appToken: string): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getConfigPath = getConfigPath;
|
|
37
|
+
exports.loadConfig = loadConfig;
|
|
38
|
+
exports.saveConfig = saveConfig;
|
|
39
|
+
exports.initConfig = initConfig;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const yaml = __importStar(require("js-yaml"));
|
|
42
|
+
const DEFAULT_PATH = "runner.yaml";
|
|
43
|
+
function getConfigPath(custom) {
|
|
44
|
+
return custom || DEFAULT_PATH;
|
|
45
|
+
}
|
|
46
|
+
function loadConfig(configPath) {
|
|
47
|
+
if (!fs.existsSync(configPath)) {
|
|
48
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
49
|
+
}
|
|
50
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
51
|
+
const config = yaml.load(raw);
|
|
52
|
+
if (!config.hub_url)
|
|
53
|
+
throw new Error("hub_url is required in config");
|
|
54
|
+
if (!config.app_token)
|
|
55
|
+
throw new Error("app_token is required in config");
|
|
56
|
+
if (!config.commands)
|
|
57
|
+
config.commands = {};
|
|
58
|
+
if (!config.max_output)
|
|
59
|
+
config.max_output = 2000;
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
function saveConfig(configPath, config) {
|
|
63
|
+
const raw = yaml.dump(config, { lineWidth: -1, noRefs: true });
|
|
64
|
+
fs.writeFileSync(configPath, raw, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
function initConfig(configPath, hubUrl, appToken) {
|
|
67
|
+
const config = {
|
|
68
|
+
hub_url: hubUrl,
|
|
69
|
+
app_token: appToken,
|
|
70
|
+
max_output: 2000,
|
|
71
|
+
commands: {},
|
|
72
|
+
};
|
|
73
|
+
saveConfig(configPath, config);
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeCommand = executeCommand;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
function executeCommand(cmdConfig, args, maxOutput) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const command = cmdConfig.exec.replace(/\$\{args\}/g, args || "");
|
|
8
|
+
const timeout = (cmdConfig.timeout || 30) * 1000;
|
|
9
|
+
(0, child_process_1.exec)(command, { timeout, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
10
|
+
if (error) {
|
|
11
|
+
if (error.killed) {
|
|
12
|
+
resolve(`命令超时(${cmdConfig.timeout || 30}s)`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const errMsg = stderr?.trim() || error.message;
|
|
16
|
+
resolve(`命令执行失败: ${errMsg}`.slice(0, maxOutput));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let output = stdout.trim();
|
|
20
|
+
if (!output && stderr?.trim()) {
|
|
21
|
+
output = stderr.trim();
|
|
22
|
+
}
|
|
23
|
+
if (!output) {
|
|
24
|
+
output = "(无输出)";
|
|
25
|
+
}
|
|
26
|
+
// Truncate to max_output
|
|
27
|
+
if (output.length > maxOutput) {
|
|
28
|
+
output = output.slice(0, maxOutput - 20) + "\n... (输出已截断)";
|
|
29
|
+
}
|
|
30
|
+
resolve(output);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHandler = createHandler;
|
|
4
|
+
const executor_1 = require("./executor");
|
|
5
|
+
function createHandler(config) {
|
|
6
|
+
return async (event, sendReply) => {
|
|
7
|
+
if (event.event.type !== "command")
|
|
8
|
+
return;
|
|
9
|
+
const cmdName = event.event.data.command;
|
|
10
|
+
const text = event.event.data.text || "";
|
|
11
|
+
const sender = event.event.data.sender;
|
|
12
|
+
console.log(`← /${cmdName} ${text} (from ${sender?.name || "unknown"})`);
|
|
13
|
+
const cmdConfig = config.commands[cmdName];
|
|
14
|
+
if (!cmdConfig) {
|
|
15
|
+
const available = Object.keys(config.commands).map(c => `/${c}`).join(", ");
|
|
16
|
+
sendReply(`未知命令: /${cmdName}\n可用命令: ${available}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const result = await (0, executor_1.executeCommand)(cmdConfig, text, config.max_output || 2000);
|
|
21
|
+
console.log(`→ (${result.length} chars)`);
|
|
22
|
+
sendReply(result);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error(`命令执行出错:`, err);
|
|
26
|
+
sendReply(`执行失败: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RunnerConfig, HubEvent } from "./types";
|
|
2
|
+
export type EventHandler = (event: HubEvent, sendReply: (content: string) => void) => void;
|
|
3
|
+
export declare class HubConnection {
|
|
4
|
+
private ws;
|
|
5
|
+
private config;
|
|
6
|
+
private onEvent;
|
|
7
|
+
private reconnectTimer;
|
|
8
|
+
private pingTimer;
|
|
9
|
+
private stopped;
|
|
10
|
+
constructor(config: RunnerConfig, onEvent: EventHandler);
|
|
11
|
+
connect(): void;
|
|
12
|
+
sendReply(content: string): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
private cleanup;
|
|
15
|
+
}
|
package/dist/src/hub.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HubConnection = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
class HubConnection {
|
|
9
|
+
ws = null;
|
|
10
|
+
config;
|
|
11
|
+
onEvent;
|
|
12
|
+
reconnectTimer = null;
|
|
13
|
+
pingTimer = null;
|
|
14
|
+
stopped = false;
|
|
15
|
+
constructor(config, onEvent) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.onEvent = onEvent;
|
|
18
|
+
}
|
|
19
|
+
connect() {
|
|
20
|
+
if (this.stopped)
|
|
21
|
+
return;
|
|
22
|
+
const wsUrl = `${this.config.hub_url.replace(/^http/, "ws")}/bot/v1/ws?token=${this.config.app_token}`;
|
|
23
|
+
this.ws = new ws_1.default(wsUrl);
|
|
24
|
+
this.ws.on("open", () => {
|
|
25
|
+
console.log("✓ 已连接到 Hub");
|
|
26
|
+
// Start ping interval
|
|
27
|
+
this.pingTimer = setInterval(() => {
|
|
28
|
+
if (this.ws?.readyState === ws_1.default.OPEN) {
|
|
29
|
+
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
30
|
+
}
|
|
31
|
+
}, 30000);
|
|
32
|
+
});
|
|
33
|
+
this.ws.on("message", (data) => {
|
|
34
|
+
try {
|
|
35
|
+
const msg = JSON.parse(data.toString());
|
|
36
|
+
if (msg.type === "init") {
|
|
37
|
+
console.log(` Bot: ${msg.data.bot_id}`);
|
|
38
|
+
console.log(` App: ${msg.data.app_slug}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (msg.type === "event") {
|
|
42
|
+
this.onEvent(msg, (content) => this.sendReply(content));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (msg.type === "pong" || msg.type === "ack")
|
|
46
|
+
return;
|
|
47
|
+
if (msg.type === "error") {
|
|
48
|
+
console.error(`Hub 错误: ${msg.error}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error("消息解析失败:", err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
this.ws.on("close", () => {
|
|
57
|
+
console.log("与 Hub 断开连接");
|
|
58
|
+
this.cleanup();
|
|
59
|
+
if (!this.stopped) {
|
|
60
|
+
console.log("5 秒后重连...");
|
|
61
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 5000);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.ws.on("error", (err) => {
|
|
65
|
+
console.error("连接错误:", err.message);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
sendReply(content) {
|
|
69
|
+
if (this.ws?.readyState === ws_1.default.OPEN) {
|
|
70
|
+
this.ws.send(JSON.stringify({ type: "send", content }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
this.stopped = true;
|
|
75
|
+
this.cleanup();
|
|
76
|
+
this.ws?.close();
|
|
77
|
+
}
|
|
78
|
+
cleanup() {
|
|
79
|
+
if (this.pingTimer) {
|
|
80
|
+
clearInterval(this.pingTimer);
|
|
81
|
+
this.pingTimer = null;
|
|
82
|
+
}
|
|
83
|
+
if (this.reconnectTimer) {
|
|
84
|
+
clearTimeout(this.reconnectTimer);
|
|
85
|
+
this.reconnectTimer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.HubConnection = HubConnection;
|
package/dist/src/sync.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.syncTools = syncTools;
|
|
4
|
+
async function syncTools(config) {
|
|
5
|
+
const tools = Object.entries(config.commands).map(([name, cmd]) => ({
|
|
6
|
+
name,
|
|
7
|
+
description: cmd.description || name,
|
|
8
|
+
command: name,
|
|
9
|
+
parameters: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
args: { type: "string", description: "命令参数" },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
const resp = await fetch(`${config.hub_url}/bot/v1/installation/tools`, {
|
|
17
|
+
method: "PUT",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${config.app_token}`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({ tools }),
|
|
23
|
+
});
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
const text = await resp.text();
|
|
26
|
+
throw new Error(`同步 tools 失败: ${resp.status} ${text}`);
|
|
27
|
+
}
|
|
28
|
+
const result = await resp.json();
|
|
29
|
+
console.log(`✓ 已同步 ${result.tool_count} 个命令到 Hub`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface CommandConfig {
|
|
2
|
+
description?: string;
|
|
3
|
+
exec: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface RunnerConfig {
|
|
7
|
+
hub_url: string;
|
|
8
|
+
app_token: string;
|
|
9
|
+
max_output?: number;
|
|
10
|
+
commands: Record<string, CommandConfig>;
|
|
11
|
+
}
|
|
12
|
+
export interface HubEvent {
|
|
13
|
+
type: string;
|
|
14
|
+
v: number;
|
|
15
|
+
trace_id: string;
|
|
16
|
+
installation_id: string;
|
|
17
|
+
bot: {
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
event: {
|
|
21
|
+
type: string;
|
|
22
|
+
id: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
data: Record<string, any>;
|
|
25
|
+
};
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openilink-app-runner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run local commands as OpeniLink Hub App tools — bridge CLI to WeChat",
|
|
5
|
+
"bin": {
|
|
6
|
+
"openilink-app-runner": "dist/bin/runner.js"
|
|
7
|
+
},
|
|
8
|
+
"files": ["dist/", "runner.example.yaml", "README.md"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["openilink", "wechat", "cli", "bot"],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/openilink/openilink-app-runner"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"ws": "^8.18.0",
|
|
22
|
+
"js-yaml": "^4.1.0",
|
|
23
|
+
"commander": "^13.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"@types/ws": "^8.0.0",
|
|
28
|
+
"@types/js-yaml": "^4.0.0",
|
|
29
|
+
"@types/node": "^22.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# OpeniLink App Runner 配置
|
|
2
|
+
# 文档: https://github.com/openilink/openilink-app-runner
|
|
3
|
+
|
|
4
|
+
hub_url: "https://hub.openilink.com"
|
|
5
|
+
app_token: "app_your_token_here"
|
|
6
|
+
max_output: 2000
|
|
7
|
+
|
|
8
|
+
commands:
|
|
9
|
+
hn:
|
|
10
|
+
description: "HackerNews 热门"
|
|
11
|
+
exec: "opencli hackernews top --format json"
|
|
12
|
+
timeout: 10
|
|
13
|
+
weather:
|
|
14
|
+
description: "查天气"
|
|
15
|
+
exec: "curl -s 'wttr.in/${args}?format=3'"
|
|
16
|
+
timeout: 5
|
|
17
|
+
ip:
|
|
18
|
+
description: "查公网 IP"
|
|
19
|
+
exec: "curl -s ifconfig.me"
|
|
20
|
+
timeout: 5
|