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 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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,2 @@
1
+ import { CommandConfig } from "./types";
2
+ export declare function executeCommand(cmdConfig: CommandConfig, args: string, maxOutput: number): Promise<string>;
@@ -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,2 @@
1
+ import { RunnerConfig, HubEvent } from "./types";
2
+ export declare function createHandler(config: RunnerConfig): (event: HubEvent, sendReply: (content: string) => void) => Promise<void>;
@@ -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
+ }
@@ -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;
@@ -0,0 +1,2 @@
1
+ import { RunnerConfig } from "./types";
2
+ export declare function syncTools(config: RunnerConfig): Promise<void>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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