@ybbapp/push-cli 0.0.1-alpha.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 +77 -0
- package/bin/push-cli.js +18 -0
- package/package.json +24 -0
- package/scripts/install.js +107 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# push-cli
|
|
2
|
+
|
|
3
|
+
`push-cli` 是供 Agent 低成本接入 Lark 群聊机器人的单向消息推送参考实现。它只通过自定义机器人 webhook 发送消息,不接收事件、不提供回调、消息读取或其他 Lark API 能力。
|
|
4
|
+
|
|
5
|
+
仓库根目录是 Go CLI 参考实现;`skills/lark-push/` 是可单独安装的 Agent Skill,包含操作说明和示例。CLI 仅调用群聊自定义机器人 webhook 推送消息,不提供其他 Lark API。
|
|
6
|
+
|
|
7
|
+
可用 GitHub CLI 安装:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
gh skill install ybbapp/push-cli lark-push
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
也可以把 `skills/lark-push/` 复制到 Agent 支持的 skills 目录。Skill 提供操作说明和示例;`push-cli` 可执行文件及本地 webhook 配置需要另行准备。构建命令:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
go build -o push-cli ./cmd
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 分发和安装
|
|
20
|
+
|
|
21
|
+
发布 tag 会构建并分发 npm 包、GitHub Release 二进制和 GHCR Docker 镜像。npm 包是推荐入口;它第一次运行时会按当前 OS/CPU 下载对应 Release 二进制并校验 SHA-256。
|
|
22
|
+
|
|
23
|
+
alpha 版本:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx --yes @ybbapp/push-cli@alpha --version
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
直接下载 Release 二进制:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
gh release download alpha-0.0.1 \
|
|
33
|
+
--repo ybbapp/push-cli \
|
|
34
|
+
--pattern 'push-cli-alpha-0.0.1-*-*.tar.gz'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Docker 镜像支持 Linux 多架构:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
docker run --rm --env LARK_WEBHOOK_URL \
|
|
41
|
+
ghcr.io/ybbapp/push-cli:alpha \
|
|
42
|
+
lark send --title '每日市场报告' --markdown '报告正文'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
首次引导:npm 要求包先存在,才能配置 Trusted Publisher。仓库公开后,先推送 `alpha-0.0.1` 生成公开 Release,再从该版本源码目录手动运行一次 `npm publish --access public --tag alpha`(使用 npm 交互式登录;该版本是 `0.0.1-alpha.0`)。包存在后,在 npm 包的 **Settings → Trusted publishing** 绑定 GitHub Actions:owner `ybbapp`、repository `push-cli`、workflow filename `release.yml`,并允许 `npm publish`。之后每次推送 `alpha-*` 或 `v*` tag,workflow 使用 OIDC 发布,不需要保存 npm token。包尚未创建时,workflow 会跳过 npm job,但仍会发布 GitHub Release 和 GHCR 镜像。
|
|
46
|
+
|
|
47
|
+
首次向 GHCR 发布后,需要在 GitHub Packages 的 `push-cli` 镜像设置中将可见性改为 Public,公开后 Docker 用户才能匿名拉取。
|
|
48
|
+
|
|
49
|
+
## 配置和使用
|
|
50
|
+
|
|
51
|
+
推荐通过环境变量注入 webhook。也可以在当前工作目录放置 `.env`,或使用 `--env` 指定文件。`.env` 不应提交到仓库。
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
export LARK_WEBHOOK_URL='https://open.larksuite.com/open-apis/bot/v2/hook/REPLACE_WITH_YOUR_TOKEN'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
发送 Markdown:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
push-cli lark send --title '每日市场报告' --file report.md
|
|
61
|
+
cat report.md | push-cli lark send --title '每日市场报告'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
附带图表发送示例(在仓库根目录运行):
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
push-cli lark send \
|
|
68
|
+
--title 'NVDA 行情' \
|
|
69
|
+
--markdown 'NVDA 收盘价走势' \
|
|
70
|
+
--chart-file skills/lark-push/examples/nvda-line-chart.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 版本和平台
|
|
74
|
+
|
|
75
|
+
首个 Git tag `alpha-0.0.1` 对应 npm SemVer `0.0.1-alpha.0`,发布到 npm 的 `alpha` dist-tag;GitHub Release 与 Docker tag 保留 `alpha-0.0.1`,并更新 Docker `alpha` tag。
|
|
76
|
+
|
|
77
|
+
原生二进制支持 macOS amd64/arm64、Linux amd64/arm64/riscv64、Windows amd64/arm64。Docker 镜像支持 Linux amd64/arm64/riscv64。
|
package/bin/push-cli.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const { ensureBinary } = require("../scripts/install");
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
try {
|
|
8
|
+
const executable = await ensureBinary();
|
|
9
|
+
const result = spawnSync(executable, process.argv.slice(2), { stdio: "inherit" });
|
|
10
|
+
if (result.error) throw result.error;
|
|
11
|
+
process.exitCode = result.status === null ? 1 : result.status;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(`push-cli: ${error.message}`);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ybbapp/push-cli",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "Small Lark group-bot push CLI for agents",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ybbapp/push-cli.git"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"push-cli": "bin/push-cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"scripts/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const OWNER = "ybbapp";
|
|
8
|
+
const REPOSITORY = "push-cli";
|
|
9
|
+
const PACKAGE_VERSION = require("../package.json").version;
|
|
10
|
+
const VENDOR_DIR = path.join(__dirname, "..", "vendor");
|
|
11
|
+
const BINARY_NAME = process.platform === "win32" ? "push-cli.exe" : "push-cli";
|
|
12
|
+
const BINARY_PATH = path.join(VENDOR_DIR, BINARY_NAME);
|
|
13
|
+
const REDIRECT_HOSTS = new Set([
|
|
14
|
+
"github.com",
|
|
15
|
+
"objects.githubusercontent.com",
|
|
16
|
+
"release-assets.githubusercontent.com",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function target() {
|
|
20
|
+
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform];
|
|
21
|
+
const arch = { x64: "amd64", arm64: "arm64", riscv64: "riscv64" }[process.arch];
|
|
22
|
+
if (!platform || !arch) {
|
|
23
|
+
throw new Error(`unsupported platform: ${process.platform}-${process.arch}`);
|
|
24
|
+
}
|
|
25
|
+
return { platform, arch };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function releaseTag() {
|
|
29
|
+
const alpha = PACKAGE_VERSION.match(/^(\d+\.\d+\.\d+)-alpha\.\d+$/);
|
|
30
|
+
if (alpha) return `alpha-${alpha[1]}`;
|
|
31
|
+
if (/^\d+\.\d+\.\d+$/.test(PACKAGE_VERSION)) return `v${PACKAGE_VERSION}`;
|
|
32
|
+
throw new Error(`cannot map npm version ${PACKAGE_VERSION} to a GitHub release`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assetName(tag, platform, arch) {
|
|
36
|
+
const extension = platform === "windows" ? "zip" : "tar.gz";
|
|
37
|
+
return `push-cli-${tag}-${platform}-${arch}.${extension}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function get(url, redirects = 0) {
|
|
41
|
+
const parsed = new URL(url);
|
|
42
|
+
if (!REDIRECT_HOSTS.has(parsed.hostname)) throw new Error(`unexpected download host: ${parsed.hostname}`);
|
|
43
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
44
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
45
|
+
if (redirects >= 3) throw new Error("too many redirects while downloading the release");
|
|
46
|
+
const location = response.headers.get("location");
|
|
47
|
+
if (!location) throw new Error("release download redirect has no location");
|
|
48
|
+
return get(new URL(location, url).toString(), redirects + 1);
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok) throw new Error(`download failed (${response.status}): ${url}`);
|
|
51
|
+
const finalHost = new URL(response.url).hostname;
|
|
52
|
+
if (!REDIRECT_HOSTS.has(finalHost)) throw new Error(`unexpected download host: ${finalHost}`);
|
|
53
|
+
return Buffer.from(await response.arrayBuffer());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extract(archive, directory, platform) {
|
|
57
|
+
if (platform === "windows") {
|
|
58
|
+
const command = "$ErrorActionPreference='Stop'; Expand-Archive -LiteralPath $env:PUSH_CLI_ARCHIVE -DestinationPath $env:PUSH_CLI_DEST -Force";
|
|
59
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], {
|
|
60
|
+
stdio: "inherit",
|
|
61
|
+
env: { ...process.env, PUSH_CLI_ARCHIVE: archive, PUSH_CLI_DEST: directory },
|
|
62
|
+
});
|
|
63
|
+
if (result.error) throw result.error;
|
|
64
|
+
if (result.status !== 0) throw new Error("could not extract the Windows release archive");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const result = spawnSync("tar", ["-xzf", archive, "-C", directory], { stdio: "inherit" });
|
|
68
|
+
if (result.error) throw result.error;
|
|
69
|
+
if (result.status !== 0) throw new Error("could not extract the release archive; install tar and retry");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function ensureBinary() {
|
|
73
|
+
if (fs.existsSync(BINARY_PATH)) return BINARY_PATH;
|
|
74
|
+
|
|
75
|
+
const { platform, arch } = target();
|
|
76
|
+
const tag = releaseTag();
|
|
77
|
+
const asset = assetName(tag, platform, arch);
|
|
78
|
+
const base = `https://github.com/${OWNER}/${REPOSITORY}/releases/download/${encodeURIComponent(tag)}`;
|
|
79
|
+
const [archiveData, checksums] = await Promise.all([
|
|
80
|
+
get(`${base}/${asset}`),
|
|
81
|
+
get(`${base}/checksums.txt`),
|
|
82
|
+
]);
|
|
83
|
+
const checksumLine = checksums.toString("utf8").split(/\r?\n/).find((line) => line.trim().endsWith(` ${asset}`));
|
|
84
|
+
if (!checksumLine) throw new Error(`release checksums do not include ${asset}`);
|
|
85
|
+
const expected = checksumLine.trim().split(/\s+/)[0].toLowerCase();
|
|
86
|
+
const actual = crypto.createHash("sha256").update(archiveData).digest("hex");
|
|
87
|
+
if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) {
|
|
88
|
+
throw new Error(`SHA-256 verification failed for ${asset}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "push-cli-"));
|
|
92
|
+
try {
|
|
93
|
+
const archivePath = path.join(temporary, asset);
|
|
94
|
+
fs.writeFileSync(archivePath, archiveData, { mode: 0o600 });
|
|
95
|
+
extract(archivePath, temporary, platform);
|
|
96
|
+
const extracted = path.join(temporary, BINARY_NAME);
|
|
97
|
+
if (!fs.existsSync(extracted)) throw new Error(`release archive does not contain ${BINARY_NAME}`);
|
|
98
|
+
fs.mkdirSync(VENDOR_DIR, { recursive: true });
|
|
99
|
+
fs.copyFileSync(extracted, BINARY_PATH);
|
|
100
|
+
fs.chmodSync(BINARY_PATH, 0o755);
|
|
101
|
+
return BINARY_PATH;
|
|
102
|
+
} finally {
|
|
103
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { ensureBinary, releaseTag, target, assetName };
|