Skip to content

Commit c6a8477

Browse files
thinkthinkingclaude
andcommitted
feat(npm): 改用 optionalDependencies 平台子包分发,装完即用
把 npm 分发从「postinstall 运行时下载二进制」改为业界标准的 optionalDependencies + 按平台拆分子包(esbuild / codex / claude-code 同款)。 旧方案依赖 postinstall 下载,pnpm v9+ / --ignore-scripts / 严格 CI 会跳过脚本 导致二进制缺失,且每次安装需联网拉 GitHub Releases。新方案二进制随 npm registry 分发,npm 按 os/cpu 自动只装匹配当前系统的子包,无 postinstall、无运行时下载。 结构: - 主包 @thinkthinking/cli(壳):删除 postinstall,新增 optionalDependencies 声明 5 个平台子包;wrapper 改用 require.resolve('@thinkthinking/cli-<plat>-<arch>/ thinkthinking') 定位二进制(子包不含 exports,故可解析包内文件路径)。 - 5 个平台子包:darwin-arm64/darwin-x64/linux-x64/linux-arm64/win32-x64, 各含二进制 + os/cpu 限定 + preferUnplugged。 - 新增 npm/scripts/build-packages.mjs:从 GitHub Release 下载各平台二进制、组装 5 个子包、同步主包 version 与 optionalDependencies;支持本地归档缓存 npm/.cache/v<version>/ 便于本机/离线验证。 - release.yml 的 npm-publish job 改为先发 5 个子包再发主包(顺序硬要求),全程 OIDC。 - 删除 npm/package/install.js;更新 README 分发说明;gitignore 忽略 npm/packages/ 与 npm/.cache/。 版本号升至 0.1.2(0.1.0 旧版与 0.1.1 已占用)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 498a1fe commit c6a8477

8 files changed

Lines changed: 317 additions & 189 deletions

File tree

.github/workflows/release.yml

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ jobs:
4343
env:
4444
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4545

46-
# 第二步:发布 npm 分发壳,用 OIDC Trusted Publishing(无需任何 token)。
47-
# - tag push:等 goreleaser 成功后再发(postinstall 要从 Release 下载二进制)。
46+
# 第二步:构建并发布 npm 包,用 OIDC Trusted Publishing(无需任何 token)。
47+
# 结构:主包(壳)+ 5 个平台子包(含二进制)。npm 装主包时按 optionalDependencies
48+
# 自动只装匹配当前系统的子包,装完即用、无 postinstall、无运行时下载。
49+
# - tag push:等 goreleaser 成功后再发(子包要从 Release 下载二进制)。
4850
# - 手动触发:直接发(Release 已存在)。
4951
npm-publish:
5052
needs: goreleaser
@@ -80,14 +82,31 @@ jobs:
8082
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
8183
echo "publishing @thinkthinking/cli@$VERSION"
8284
83-
- name: Sync package version
84-
working-directory: npm/package
85-
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
85+
- name: Build platform subpackages + sync main package
86+
# 从 Release 下载 5 平台二进制,生成 npm/packages/cli-* 子包,并把主包
87+
# version 与 optionalDependencies 同步成本次版本号。
88+
run: node npm/scripts/build-packages.mjs
89+
env:
90+
VERSION: ${{ steps.ver.outputs.version }}
91+
REPO: thinkthinking/cli
92+
# 仅用于下载 Release 资产防限流。
93+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8694

87-
- name: Publish to npm (OIDC)
88-
working-directory: npm/package
95+
- name: Publish subpackages then main (OIDC)
8996
# 关键:本步骤刻意不设置任何 npm auth token 环境变量。
9097
# 哪怕设成空字符串,npm 也会误用空 token 而非走 OIDC;
9198
# 只有完全不设置时,npm 才会自动检测并使用 OIDC 认证。
9299
# provenance 在 OIDC 下自动生成,无需 --provenance。
93-
run: npm publish --access public
100+
#
101+
# 顺序是硬要求:必须先发完 5 个子包,再发主包。
102+
# 主包 optionalDependencies 是精确版本,若子包未发,用户安装主包时
103+
# optional 解析失败会静默跳过 → wrapper 找不到二进制。set -e 保证任一
104+
# 子包失败立即中止,绝不会执行到发主包那行。
105+
run: |
106+
set -e
107+
for d in npm/packages/cli-*; do
108+
echo "==> publishing $d"
109+
( cd "$d" && npm publish --access public )
110+
done
111+
echo "==> publishing main package npm/package"
112+
( cd npm/package && npm publish --access public )

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ go.work.sum
3535
/bin/
3636
/dist/
3737

38-
# npm wrapper 下载的 native 二进制
38+
# 构建脚本生成的平台子包(CI 临时产物,不入库)
39+
/npm/packages/
40+
# 本地缓存的 Release 归档(本机验证用,不入库)
41+
/npm/.cache/
42+
# 旧版 postinstall 下载的 native 二进制残留
3943
/npm/package/bin/native/
4044
/npm/package/node_modules/
4145

README.md

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
npm install -g @thinkthinking/cli
1717
```
1818

19-
npm 包只是 Go 二进制的分发壳:`postinstall` 会根据你的平台/架构从 GitHub Releases 下载对应二进制,运行时**不依赖 Node**
19+
npm 包是 Go 二进制的分发壳:通过 `optionalDependencies` 把各平台二进制拆成独立子包,npm 安装时**自动只下载匹配你系统的那一个**,装完即用——无 postinstall、无运行时下载,运行时**不依赖 Node**
2020

2121
### 一键脚本
2222

@@ -84,6 +84,30 @@ thinkthinking wechat convert --input article.md --theme midnight
8484
- `--no-footnotes` 关闭外链转脚注
8585
- `--no-containers` 关闭 `:::callout` / `:::timeline` / `:::dialogue` / `:::quote` / `:::highlight` / `:::summary` 容器块
8686

87+
### 如何把内容正确放进公众号编辑器
88+
89+
> ⚠️ **不要把 `convert` 打印的 HTML 文本直接复制粘贴进公众号**——会显示成 HTML 源码,而非排版。
90+
91+
原因:微信编辑器只有在系统剪贴板携带 **`text/html` 富文本类型**时才会渲染粘贴内容。终端里 `convert | pbcopy` 或从 `.html` 文件复制,剪贴板只有纯文本(`text/plain`),微信便把标签当字面量插入。转换出的 HTML 本身是正确的、微信兼容的——问题只在「怎么投递」。三种正确方式:
92+
93+
| 方式 | 命令 | 适用场景 |
94+
|------|------|----------|
95+
| **剪贴板**(仅 macOS) | `convert --copy` | 终端最快:写入富文本剪贴板,到公众号 `Cmd+V` 直接渲染 |
96+
| **浏览器预览页**(全平台) | `convert --preview` | 最稳:打开预览页肉眼校对排版,点「复制到公众号」按钮再粘贴 |
97+
| **草稿 API**(推荐给 Agent) | `wechat draft create` | 全自动:直接把正文写进公众号草稿箱,无需剪贴板,见下节 |
98+
99+
```bash
100+
# macOS:转换并写入剪贴板,然后去公众号 Cmd+V
101+
thinkthinking wechat convert --input article.md --copy
102+
103+
# 任意平台:生成预览页并打开浏览器,页面内一键复制
104+
thinkthinking wechat convert --input article.md --preview
105+
```
106+
107+
- `--copy` 成功后 JSON 含 `"copied": true`;非 macOS 返回 `PLATFORM_NOT_SUPPORTED`,请改用 `--preview``--output`
108+
- `--preview` 成功后 JSON 含 `preview_path`(预览文件路径)与 `opened`(是否成功唤起浏览器;为 `false` 时按提示手动打开)。
109+
- `--copy` / `--preview` / `--output` 可叠加使用。
110+
87111
### wechat draft create
88112

89113
```bash
@@ -125,7 +149,7 @@ thinkthinking wechat draft create --markdown-file article.md --title "标题" \
125149
}
126150
```
127151

128-
错误码:`INVALID_INPUT` `CONFIG_ERROR` `FILE_NOT_FOUND` `MARKDOWN_CONVERT_ERROR` `WECHAT_AUTH_ERROR` `WECHAT_API_ERROR` `NETWORK_ERROR` `INTERNAL_ERROR`
152+
错误码:`INVALID_INPUT` `CONFIG_ERROR` `FILE_NOT_FOUND` `MARKDOWN_CONVERT_ERROR` `WECHAT_AUTH_ERROR` `WECHAT_API_ERROR` `NETWORK_ERROR` `PLATFORM_NOT_SUPPORTED` `INTERNAL_ERROR`
129153

130154
全局 flags:`--config` `--pretty` `--quiet` `--no-color` `--trace-id` `--verbose`
131155

@@ -175,17 +199,13 @@ output:
175199

176200
## npm 分发原理
177201

178-
`@thinkthinking/cli` 不用 Node 实现 CLI,只作为 Go 二进制的分发壳:
179-
180-
1. `npm install -g @thinkthinking/cli` 触发 `postinstall` → `install.js`
181-
2. `install.js` 按 `process.platform` / `process.arch` 拼出 GitHub Releases 归档名并下载
182-
3. 解压到 `bin/native/`,给 macOS/Linux 二进制加可执行权限
183-
4. `bin/thinkthinking.js` 作为 wrapper,用 `spawnSync` 把参数原样转发给 native 二进制,保留 stdout/stderr 与 exit code
202+
`@thinkthinking/cli` 不用 Node 实现 CLI,只作为 Go 二进制的分发壳。采用业界标准的 **`optionalDependencies` + 按平台拆分子包** 模式(esbuild / @openai/codex / @anthropic-ai/claude-code 同款):
184203

185-
环境变量:
204+
- 主包 `@thinkthinking/cli`(壳)的 `optionalDependencies` 声明 5 个平台子包:`@thinkthinking/cli-{darwin-arm64,darwin-x64,linux-x64,linux-arm64,win32-x64}`。
205+
- 每个子包用 npm 的 `os` / `cpu` 字段限定平台,二进制直接打在子包里随 npm registry 分发。`npm install` 时 npm **自动只安装匹配当前系统的那一个子包**,无需 postinstall、无运行时下载。
206+
- `bin/thinkthinking.js` 作为 wrapper,用 `require.resolve('@thinkthinking/cli-<platform>-<arch>/thinkthinking')` 定位子包二进制,再用 `spawnSync` 把参数原样转发,保留 stdout/stderr 与 exit code。
186207

187-
- `THINKTHINKING_SKIP_DOWNLOAD=1` 跳过下载(离线 / 自行构建)
188-
- `THINKTHINKING_VERSION=x.y.z` 指定下载版本
208+
发布流程:`npm/scripts/build-packages.mjs` 从 GitHub Release 下载各平台二进制、组装出 5 个子包并同步主包版本号;CI(`.github/workflows/release.yml`)先发 5 个子包、再发主包(OIDC Trusted Publishing,无需 token)。
189209

190210
---
191211

npm/package/README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
npm install -g @thinkthinking/cli
99
```
1010

11-
本 npm 包只是 Go 二进制的分发壳:`postinstall` 会根据你的平台/架构从 [GitHub Releases](https://github.com/thinkthinking/cli/releases) 下载对应二进制,运行时**不依赖 Node**
11+
本 npm 包是 Go 二进制的分发壳:通过 `optionalDependencies` 把各平台二进制拆成独立子包(`@thinkthinking/cli-darwin-arm64` 等),npm 安装时**自动只下载匹配你系统的那一个**,装完即用——无 postinstall、无运行时下载,运行时**不依赖 Node**
1212

1313
## 快速开始
1414

@@ -20,10 +20,7 @@ thinkthinking wechat convert --input article.md
2020
thinkthinking wechat draft create --markdown-file article.md --title "标题"
2121
```
2222

23-
## 环境变量
24-
25-
- `THINKTHINKING_SKIP_DOWNLOAD=1` 跳过二进制下载(离线 / 自行构建)
26-
- `THINKTHINKING_VERSION=x.y.z` 指定下载版本
23+
> 若在极少数环境下安装时带了 `--no-optional` / `--omit=optional`,平台子包会被跳过、命令会报「找不到平台二进制」。此时去掉该参数重装即可,或用 [GitHub Releases](https://github.com/thinkthinking/cli/releases) 的一键脚本安装。
2724
2825
完整文档见 [GitHub 仓库](https://github.com/thinkthinking/cli)
2926

npm/package/bin/thinkthinking.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,47 @@
11
#!/usr/bin/env node
2-
// Node wrapper:把所有参数原样转发给已下载的 native 二进制,
3-
// 保留 stdout/stderr 与 exit code。Node 仅作转发壳,不参与业务逻辑。
2+
// Node wrapper:定位随 optionalDependencies 安装的平台子包二进制,
3+
// 把所有参数原样转发给它,保留 stdout/stderr 与 exit code。
4+
// Node 仅作转发壳,不参与业务逻辑,运行时不依赖 Node 之外的下载。
45

5-
const path = require("path");
6-
const fs = require("fs");
76
const { spawnSync } = require("child_process");
87

9-
const BINARY_NAME = "thinkthinking";
10-
const isWindows = process.platform === "win32";
11-
const binaryFile = isWindows ? `${BINARY_NAME}.exe` : BINARY_NAME;
12-
const binaryPath = path.join(__dirname, "native", binaryFile);
8+
// 平台/架构 → [子包名, 二进制文件名]。
9+
// 键用 Node 的 `${process.platform}-${process.arch}`(win32 / x64)。
10+
const BINARIES = {
11+
"darwin-arm64": ["@thinkthinking/cli-darwin-arm64", "thinkthinking"],
12+
"darwin-x64": ["@thinkthinking/cli-darwin-x64", "thinkthinking"],
13+
"linux-x64": ["@thinkthinking/cli-linux-x64", "thinkthinking"],
14+
"linux-arm64": ["@thinkthinking/cli-linux-arm64", "thinkthinking"],
15+
"win32-x64": ["@thinkthinking/cli-win32-x64", "thinkthinking.exe"],
16+
};
1317

14-
if (!fs.existsSync(binaryPath)) {
15-
console.error(
16-
`[thinkthinking] native binary not found at ${binaryPath}\n` +
17-
`请重新安装(npm install -g @thinkthinking/cli),或检查 postinstall 下载是否失败。`
18-
);
19-
process.exit(1);
18+
function resolveBinary() {
19+
const key = `${process.platform}-${process.arch}`;
20+
const entry = BINARIES[key];
21+
if (!entry) {
22+
console.error(
23+
`[thinkthinking] 不支持的平台:${key}。` +
24+
`支持的平台:${Object.keys(BINARIES).join(", ")}。`
25+
);
26+
process.exit(1);
27+
}
28+
const [pkg, file] = entry;
29+
try {
30+
// 子包 package.json 不含 exports 字段,可直接解析包内二进制文件路径。
31+
return require.resolve(`${pkg}/${file}`);
32+
} catch (err) {
33+
console.error(
34+
`[thinkthinking] 找不到平台二进制包 ${pkg}(平台 ${key})。\n` +
35+
`可能原因:安装时跳过了 optionalDependencies(如 --no-optional / --omit=optional / --ignore-scripts 无关),\n` +
36+
`或该平台子包未发布。请重新安装:npm install -g @thinkthinking/cli\n` +
37+
`原始错误:${err.message}`
38+
);
39+
process.exit(1);
40+
}
2041
}
2142

43+
const binaryPath = resolveBinary();
44+
2245
// 透传 stdio,让 JSON 走 stdout、日志走 stderr 的约定在 wrapper 层也成立。
2346
const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
2447

npm/package/install.js

Lines changed: 0 additions & 145 deletions
This file was deleted.

npm/package/package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
{
22
"name": "@thinkthinking/cli",
3-
"version": "0.1.0",
3+
"version": "0.1.2",
44
"description": "面向 Agent 的个人 CLI 工具箱:Markdown 转微信公众号 HTML,并上传草稿。Go binary 通过 npm 分发。",
55
"bin": {
66
"thinkthinking": "bin/thinkthinking.js"
77
},
8-
"scripts": {
9-
"postinstall": "node install.js"
10-
},
118
"files": [
129
"bin/thinkthinking.js",
13-
"install.js",
1410
"package.json",
1511
"README.md"
1612
],
1713
"engines": {
1814
"node": ">=16"
1915
},
16+
"optionalDependencies": {
17+
"@thinkthinking/cli-darwin-x64": "0.1.2",
18+
"@thinkthinking/cli-darwin-arm64": "0.1.2",
19+
"@thinkthinking/cli-linux-x64": "0.1.2",
20+
"@thinkthinking/cli-linux-arm64": "0.1.2",
21+
"@thinkthinking/cli-win32-x64": "0.1.2"
22+
},
2023
"keywords": [
2124
"wechat",
2225
"markdown",

0 commit comments

Comments
 (0)