@code2rich/jpage 1.5.0 → 1.5.2

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.
Files changed (44) hide show
  1. package/.github/workflows/ci.yml +3 -1
  2. package/.github/workflows/release.yml +87 -0
  3. package/CLAUDE.md +3 -1
  4. package/README.md +26 -2
  5. package/bin/commands/ls.js +6 -3
  6. package/bin/commands/update.js +74 -0
  7. package/bin/jpage.js +7 -2
  8. package/docs/RELEASING.md +209 -0
  9. package/docs/skill-integration-design.md +384 -0
  10. package/eslint.config.mjs +2 -0
  11. package/lib/csp.js +8 -2
  12. package/lib/render.js +9 -2
  13. package/lib/templates.js +1 -1
  14. package/mcp/tools-files.js +5 -1
  15. package/package.json +4 -4
  16. package/public/css/style.css +128 -1
  17. package/public/index.html +51 -3
  18. package/public/js/app.js +8 -6
  19. package/public/js/pages/content-templates.js +1 -1
  20. package/public/js/pages/home.js +218 -9
  21. package/public/js/pages/landing.js +1 -1
  22. package/public/js/pages/preview.js +1 -1
  23. package/public/js/utils.js +15 -7
  24. package/routes/skills.js +77 -3
  25. package/server.js +10 -3
  26. package/skills/jpage-presentation/INSTALL.md +50 -0
  27. package/skills/jpage-presentation/README.md +71 -0
  28. package/skills/jpage-presentation/SKILL.md +226 -0
  29. package/skills/jpage-presentation/assets/plugin/highlight/monokai.css +71 -0
  30. package/skills/jpage-presentation/assets/plugin/highlight/plugin.js +439 -0
  31. package/skills/jpage-presentation/assets/plugin/notes/notes.js +1 -0
  32. package/skills/jpage-presentation/assets/reveal-base.css +9 -0
  33. package/skills/jpage-presentation/assets/reveal.js +9 -0
  34. package/skills/jpage-presentation/assets/themes/academic.css +68 -0
  35. package/skills/jpage-presentation/assets/themes/business.css +64 -0
  36. package/skills/jpage-presentation/assets/themes/creative.css +81 -0
  37. package/skills/jpage-presentation/assets/themes/minimal.css +117 -0
  38. package/skills-registry.js +0 -6
  39. package/test/dispatch-bench.js +0 -3
  40. package/test/integration/cli.test.js +93 -0
  41. package/test/integration/skills.test.js +27 -5
  42. package/test/perf-harness.js +0 -9
  43. package/test/unit/fts.test.js +0 -1
  44. package/.claude/settings.local.json +0 -68
@@ -28,7 +28,9 @@ jobs:
28
28
  uses: actions/setup-node@v4
29
29
  with:
30
30
  node-version: ${{ matrix.node-version }}
31
- cache: 'npm'
31
+ # 不启用 cache: 'npm':setup-node 恢复缓存时 _cacache/tmp 残留与
32
+ # npm ci 冲突(EEXIST errno -17),Node 20 矩阵稳定复现。
33
+ # npm ci 本身够快(~10s),缓存收益不值得这个 bug。
32
34
 
33
35
  - name: Install dependencies
34
36
  run: npm ci
@@ -0,0 +1,87 @@
1
+ # npm 自动发版:打 v* tag 触发,跑测试 + 校验版本号一致 + npm publish。
2
+ #
3
+ # 用法(本地):
4
+ # 1. 改 package.json 的 version(如 1.5.1)
5
+ # 2. commit + push 到 main
6
+ # 3. git tag v1.5.1 && git push origin v1.5.1
7
+ # 4. 本 workflow 自动跑:test → 校验 tag==version → npm publish
8
+ #
9
+ # 前置(仓库设置,一次性):
10
+ # GitHub 仓库 Settings → Secrets and variables → Actions → New repository secret
11
+ # Name: NPM_TOKEN
12
+ # Value: 在 npm 生成的 granular access token(@code2rich scope, Read and write)
13
+ # 生成地址:https://www.npmjs.com/settings/code2richnpm/tokens/granular-access-tokens/new
14
+ #
15
+ # 版本一致性:tag 名(去 v 前缀)必须等于 package.json 的 version,否则失败。
16
+ # 这样能防止「tag 标 v1.6.0 但 package.json 还写着 1.5.0」这类手滑。
17
+
18
+ name: Release
19
+
20
+ on:
21
+ push:
22
+ tags: ['v*']
23
+
24
+ # 同一 tag 不要并发跑(理论上 git push tag 是原子的,但保险起见)
25
+ concurrency:
26
+ group: release-${{ github.ref }}
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ release:
31
+ name: Publish to npm
32
+ runs-on: ubuntu-latest
33
+ # 同时跑测试矩阵确认绿(轻量:只 Node 20,CI workflow 已覆盖 20+22)
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+
37
+ - name: Setup Node.js
38
+ uses: actions/setup-node@v4
39
+ with:
40
+ node-version: '20'
41
+ # 不启用 cache: 'npm':见 ci.yml 同款注释(EEXIST bug)
42
+ registry-url: 'https://registry.npmjs.org'
43
+
44
+ - name: Install dependencies
45
+ run: npm ci
46
+
47
+ - name: Lint
48
+ run: npm run lint
49
+
50
+ - name: Run tests (unit + integration)
51
+ run: npm test
52
+
53
+ - name: Build frontend bundle
54
+ run: npm run build
55
+
56
+ - name: Verify tag matches package version
57
+ run: |
58
+ TAG="${GITHUB_REF_NAME#v}"
59
+ PKG=$(node -p "require('./package.json').version")
60
+ echo "tag (去 v 前缀): $TAG"
61
+ echo "package.json version: $PKG"
62
+ if [ "$TAG" != "$PKG" ]; then
63
+ echo "::error::tag ($TAG) 与 package.json version ($PKG) 不一致"
64
+ exit 1
65
+ fi
66
+
67
+ - name: Publish to npm
68
+ run: npm publish
69
+ env:
70
+ # setup-node 配置的 registry 会读这个环境变量做鉴权
71
+ # publishConfig.access=public 已设,scoped 包公开发布无需 --access
72
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
73
+
74
+ - name: Summary
75
+ if: always()
76
+ run: |
77
+ echo "## Release ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
78
+ echo "" >> $GITHUB_STEP_SUMMARY
79
+ if [ "${{ job.status }}" = "success" ]; then
80
+ echo "✅ 已发布 \`$(node -p "require('./package.json').name")@$(node -p "require('./package.json').version")\`" >> $GITHUB_STEP_SUMMARY
81
+ echo "" >> $GITHUB_STEP_SUMMARY
82
+ echo "安装:\`\`\`bash" >> $GITHUB_STEP_SUMMARY
83
+ echo "npm install -g $(node -p "require('./package.json').name")" >> $GITHUB_STEP_SUMMARY
84
+ echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
85
+ else
86
+ echo "❌ 发布失败,查看上面日志" >> $GITHUB_STEP_SUMMARY
87
+ fi
package/CLAUDE.md CHANGED
@@ -133,7 +133,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
133
133
  - `GET /api/skills` — 列出已安装的 skill 包(需登录)
134
134
  - `GET /api/skills/:name` — skill 详情(含 SKILL.md 内容、文件列表、INSTALL.md 渲染)
135
135
  - `GET /api/skills/:name/download` — ZIP 下载整个 skill 目录
136
- - `GET /api/mcp/config` — 返回 MCP 连接配置(URL、Token 列表、JSON 配置片段)
136
+ - `GET /api/mcp/config` — 返回 MCP 连接配置(URL、Token 列表、多客户端 mcpServers JSON 片段;仅 MCP 客户端)
137
+ - `GET /api/cli/guide` — 返回 `jpage` CLI 用法指南(baseUrl、渲染后的 guideHtml、纯文本 guideText);CLI 与 MCP 是并列的两个客户端入口,各自独立端点
137
138
 
138
139
  **Skills registry** — `skills-registry.js` 自动发现 `skills/*/SKILL.md`,解析 YAML frontmatter(`name`, `description`, `version`, `author`)。Web UI 首页展示 Skills 区块,管理员可查看详情(弹窗)和下载 ZIP。ZIP 包与磁盘目录结构一致,可直接解压到 `~/.claude/skills/`。
139
140
 
@@ -168,6 +169,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
168
169
  - **大 body 端点专用解析** — 全局 `express.json` 限 1MB;`upload-json` / `upload-zip-base64` / `overwrite-json` 用 `largeJson`(50MB)。新增大 body 端点时挂 `largeJson`。
169
170
  - **静态资源长缓存** — `express.static` 统一带 `STATIC_OPTS`(30d + immutable)。前端 CSS/JS 引用带 `?v=x.y.z`,改资源后务必 bump 版本号以失效缓存。
170
171
  - **MCP 进程内分发** — MCP tool 不再走 `fetch('http://127.0.0.1:port/...')` 自调用,改用 `lib/dispatch.js` 的 `createDispatcher(app, {token})` 直接调 `app.handle()`。新增 MCP tool 时用传入的 `api` 对象(`api.get/post/put/del`),接口与 fetch 版一致;鉴权靠 `Authorization: Bearer <token>` 头走 `requireAuth`,行为与 HTTP 完全相同(权限、限流、审计都生效)。
172
+ - **CLI 与 MCP 双客户端入口必须同步** — `bin/commands/*.js`(CLI 命令)与 `mcp/tools-*.js`(MCP tool)是两个对等的客户端入口,都构建在同一套 REST API(`routes/`)之上。**新增或修改任一端的功能时,另一端必须同步实现对应能力**:CLI 加命令就同时给 MCP 加 tool,反之亦然,避免两边能力漂移。功能域与文件对应:文件管理 → `commands/{upload,ls,cat,url,mv,rm,star,tags}.js` ↔ `mcp/tools-files.js`;标签 → `commands/tags.js` ↔ `mcp/tools-tags.js`;版本 → `mcp/tools-versions.js`(CLI 待补);分类 → `mcp/tools-categories.js`(CLI 待补)。
171
173
  - **模板预编译** — `loadTemplates()` 把每个模板经 `compileTemplate()` 编为函数存入 `templateCache`(静态 vendor URL 占位符在加载时一次替换,运行时仅 title/content/hljs_theme 三个 `split/join`)。`templateCache[name]` 是**函数**而非字符串,`applyTemplate(tplFn, ...)` 直接调用。
172
174
  - **marked.parse 同步** — 所有 `marked.parse(...)` 显式带 `async: false`(v12 默认同步,显式声明防止未来升级开启 async 返回 Promise 被当字符串拼接)。
173
175
  - **搜索 UNION 合并** — `/api/files/search` 用 `UNION` 合并 FTS 全文命中(带 `snippet`)与文件名 LIKE 命中(snippet 为 NULL),一次往返替代旧的两查询+内存去重,分页准确。注意 FTS5 的 `MATCH` 不能与普通列在 `LEFT JOIN + OR` 中混用(SQLite 报 "unable to use function MATCH")。
package/README.md CHANGED
@@ -6,8 +6,6 @@
6
6
 
7
7
  [English](README_EN.md) | 中文
8
8
 
9
- **[>>> 查看即页产品介绍 <<<](https://jpage.cn/)**
10
-
11
9
  **即页**是一个零配置的 HTML / Markdown 即时预览与分享工具。把写好的文档拖进来,立刻获得一个干净的在线页面——无需部署流程,无需服务器知识。特别适合 AI 生成内容的一键分享。
12
10
 
13
11
  ---
@@ -105,6 +103,32 @@ node test/mcp-harness.js 8858 # MCP 端点
105
103
  node test/perf-bench.js 8858 # 渲染/列表/缓存延迟基准
106
104
  ```
107
105
 
106
+ ### CLI 工具(npm 包已发布)
107
+
108
+ 即页随包提供 `jpage` 命令行工具,可通过 REST API 上传 / 列出 / 管理文件,对大文件和 ZIP 走 multipart 二进制流式上传(比 MCP 的 base64 进 token 流更快更省):
109
+
110
+ ```bash
111
+ npm install -g @code2rich/jpage
112
+ jpage upload ./report.html --public --token <你的 token>
113
+ jpage ls --kw 季度
114
+ jpage cat 8
115
+ jpage --help
116
+ ```
117
+
118
+ `jpage` 与 MCP 是对称的两个客户端入口,都架在同一套 REST API 之上。详见 `jpage --help`。
119
+
120
+ 更新到最新版(不需 token):
121
+
122
+ ```bash
123
+ jpage update # 自更新到最新版
124
+ jpage update --check # 只查有没有新版本
125
+ jpage update --registry https://registry.npmmirror.com # 国内源
126
+ ```
127
+
128
+ ### 发版
129
+
130
+ 维护者发版指南(含 GitHub Actions 自动发版配置、token 轮换、故障排查)见 [`docs/RELEASING.md`](docs/RELEASING.md)。
131
+
108
132
  ## 鉴权与安全
109
133
 
110
134
  即页支持多用户体系。admin 可管理全部用户和文件,普通用户只能操作自己的文件和公开文件。
@@ -1,9 +1,9 @@
1
1
  // ls 命令:列出文件。
2
2
  // 后端:GET /api/files(支持 page/limit/sort/order/keyword/category/tag)。
3
3
 
4
- const { formatSize, formatTime, out } = require('./_shared');
4
+ const { formatSize, formatTime, shareUrl, out } = require('./_shared');
5
5
 
6
- async function run(client, args) {
6
+ async function run(client, args, { base }) {
7
7
  const o = args.opts;
8
8
  const params = new URLSearchParams();
9
9
  if (o.page) params.set('page', o.page);
@@ -24,14 +24,17 @@ async function run(client, args) {
24
24
  return;
25
25
  }
26
26
 
27
- // 对齐表格:id / 类型 / 公开 / 大小 / 更新时间 / 文件名 / 标签
27
+ // 对齐表格:id / 类型 / 公开 / 大小 / 更新时间 / 文件名 / 短链 / 标签
28
28
  for (const f of files) {
29
29
  const pub = f.is_public ? 'pub' : 'pri';
30
30
  const tags = (f.tags || []).map((t) => t.name).join(',');
31
31
  const bundle = f.is_bundle ? ' 📦' : '';
32
+ const url = shareUrl(base, f);
33
+ const short = url ? ` ${url}` : '';
32
34
  out(
33
35
  `#${f.id} [${f.file_type || '?'} ${pub}] ${formatSize(f.size).padEnd(7)} ` +
34
36
  `${formatTime(f.updated_at)} ${f.original_name}${bundle}` +
37
+ short +
35
38
  (tags ? ` {${tags}}` : '') +
36
39
  '\n'
37
40
  );
@@ -0,0 +1,74 @@
1
+ // update 命令:把 jpage 自更新到最新版(npm 全局包)。
2
+ //
3
+ // 纯客户端操作:不调后端 API,不需 token(自更新与 jpage 服务端无关)。
4
+ // 流程:npm view 查最新版本 → 与本地对比 → 有新版则 npm install -g 重装。
5
+ //
6
+ // 可注入 npmExec(形如 (args) => string):测试时注入假执行器,避免真的跑 npm。
7
+ // 默认走 child_process.execFileSync,与 build.js 既有风格一致。
8
+
9
+ const { execFileSync } = require('child_process');
10
+ const { out, err } = require('./_shared');
11
+
12
+ const PKG_NAME = '@code2rich/jpage';
13
+ const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
14
+
15
+ // 默认 npm 执行器:同步拿 stdout(trim 尾部换行)。
16
+ function defaultNpmExec(args) {
17
+ return execFileSync(npmBin, args, {
18
+ encoding: 'utf-8',
19
+ stdio: ['ignore', 'pipe', 'pipe'],
20
+ }).trim();
21
+ }
22
+
23
+ async function run(_client, args, ctx) {
24
+ const o = args.opts;
25
+ const exit = ctx.exit || ((c) => { process.exitCode = c; });
26
+ const npmExec = ctx.npmExec || defaultNpmExec;
27
+
28
+ // --registry 后面没给值时,args.js 会把它解析成 true。
29
+ if (o.registry === true) {
30
+ const e = new Error('用法:jpage update [--registry <url>] [--check]');
31
+ e.name = 'UsageError';
32
+ throw e;
33
+ }
34
+ const registryArgs = o.registry ? ['--registry', o.registry] : [];
35
+
36
+ const current = require('../../package.json').version;
37
+
38
+ // 查最新版本。
39
+ let latest;
40
+ try {
41
+ latest = (npmExec(['view', PKG_NAME, 'version', ...registryArgs]) || '').trim();
42
+ } catch (e) {
43
+ err(`✗ 查询最新版本失败:${e.message || e}\n`);
44
+ err(' 检查网络连接,或用 --registry 指定可达的 npm 源。\n');
45
+ exit(1);
46
+ return;
47
+ }
48
+
49
+ if (latest === current) {
50
+ out(`已是最新版 ${current}\n`);
51
+ return;
52
+ }
53
+
54
+ out(`发现新版本 ${latest}(当前 ${current}),正在更新…\n`);
55
+
56
+ if (o.check) {
57
+ return; // 只查不更新
58
+ }
59
+
60
+ try {
61
+ npmExec(['install', '-g', `${PKG_NAME}@latest`, ...registryArgs]);
62
+ } catch (e) {
63
+ const detail = (e.stderr || e.message || e).toString().split('\n').slice(0, 3).join('\n');
64
+ err(`✗ 更新失败:\n${detail}\n`);
65
+ err(' 常见原因:权限不足(试 sudo)、网络中断、registry 不可达。\n');
66
+ err(' 也可手动执行:npm install -g ' + PKG_NAME + '@latest\n');
67
+ exit(1);
68
+ return;
69
+ }
70
+
71
+ out(`✓ 已更新到 ${latest},重新运行 jpage 生效\n`);
72
+ }
73
+
74
+ module.exports = { run };
package/bin/jpage.js CHANGED
@@ -31,8 +31,12 @@ const COMMANDS = {
31
31
  tags: () => require('./commands/tags'),
32
32
  skills: () => require('./commands/skills'),
33
33
  whoami: () => require('./commands/whoami'),
34
+ update: () => require('./commands/update'),
34
35
  };
35
36
 
37
+ // 这些命令纯本地执行(不调后端 API),不强制要求 token。
38
+ const NO_TOKEN = new Set(['update']);
39
+
36
40
  const HELP = `jpage —— 即页命令行
37
41
 
38
42
  用法:
@@ -51,6 +55,7 @@ const HELP = `jpage —— 即页命令行
51
55
  skills ls | get <名> | download <名> [--out 文件]
52
56
  列出 / 查看 / 下载 Skill
53
57
  whoami 校验 token 是否有效
58
+ update [--registry <url>] [--check] 自更新到最新版(不需 token)
54
59
 
55
60
  通用选项:
56
61
  --token <TOKEN> 鉴权 token(jp_ 用户 token 或 MCP_TOKEN)
@@ -108,7 +113,7 @@ async function run(argv, inject = {}) {
108
113
  }
109
114
 
110
115
  const { token, base } = resolveConfig(opts, inject.env, inject.cwd);
111
- if (!token) {
116
+ if (!token && !NO_TOKEN.has(cmd)) {
112
117
  stderr.write(
113
118
  '未提供 token。用 --token <TOKEN>、JPAGE_TOKEN 环境变量、或 .env 的 MCP_TOKEN 设置。\n'
114
119
  );
@@ -117,7 +122,7 @@ async function run(argv, inject = {}) {
117
122
  }
118
123
 
119
124
  const client = createClient({ base, token, fetchImpl: inject.fetchImpl });
120
- const ctx = { base, token, exit };
125
+ const ctx = { base, token, exit, npmExec: inject.npmExec };
121
126
  const mod = COMMANDS[cmd]();
122
127
 
123
128
  try {
@@ -0,0 +1,209 @@
1
+ # 发版指南
2
+
3
+ > 维护者文档。记录如何把 `@code2rich/jpage` 发布到 npm,以及 GitHub Actions 自动发版的配置与一次性设置。
4
+
5
+ ## TL;DR
6
+
7
+ ```bash
8
+ npm version patch # 1.5.0 → 1.5.1(自动改 package.json + commit + 打 tag)
9
+ git push origin main # 推 commit
10
+ git push origin v1.5.1 # 推 tag → 触发 GitHub Actions 自动发布
11
+ ```
12
+
13
+ 打完 tag 等 Actions 跑完(2-3 分钟),`npm view @code2rich/jpage version` 确认。
14
+
15
+ ---
16
+
17
+ ## 前置(一次性配置)
18
+
19
+ ### 1. 生成 npm granular access token
20
+
21
+ 专为 CI 用的 token,不要复用你本地的或之前的。
22
+
23
+ 1. 访问 `https://www.npmjs.com/settings/code2richnpm/tokens/granular-access-tokens/new`
24
+ 2. 按以下填:
25
+ - **Name**: `jpage-ci-publish`(或任意易识别的名字)
26
+ - **Expiration**: 建议 90 天(到期前轮换)
27
+ - **Packages and scopes**: `Only select packages and scopes` → 勾 `@code2rich`
28
+ - 包已存在时可选精确到 `@code2rich/jpage`,权限更小更安全
29
+ - **Permissions**: `Read and write`
30
+ 3. 生成后复制 `npm_xxxx...`(**只显示一次**)
31
+
32
+ > granular token 在生成时已勾选 bypass 2FA,CI 发布无需 OTP。
33
+
34
+ ### 2. 把 token 存进 GitHub Secrets
35
+
36
+ 1. 访问 `https://github.com/code2rich/jpage/settings/secrets/actions`
37
+ 2. **New repository secret**
38
+ - Name: `NPM_TOKEN`
39
+ - Value: 上一步复制的 token
40
+ 3. Add secret
41
+
42
+ token 从此只存在 GitHub Secrets,不出现在本地终端、代码、`.npmrc`。
43
+
44
+ ### 3.(强烈建议)给 npm 账号开 2FA
45
+
46
+ 访问 `https://www.npmjs.com/settings/code2richnpm/account/security` → 选 `auth-and-writes`。
47
+
48
+ - 2FA 保护**账号登录态**(防账号被盗后乱改设置)
49
+ - granular token 的 bypass 2FA 只针对**发布动作**,两者互补不冲突
50
+
51
+ ---
52
+
53
+ ## 发版流程
54
+
55
+ ### 常规发版(推荐:自动 CI)
56
+
57
+ ```bash
58
+ # 1. 确认在 main 分支且工作区干净
59
+ git checkout main
60
+ git status
61
+
62
+ # 2. 升版本号(npm version 会自动改 package.json + commit + 打 tag)
63
+ npm version patch # 1.5.0 → 1.5.1 修 bug
64
+ # npm version minor # 1.5.0 → 1.6.0 新功能
65
+ # npm version major # 1.5.0 → 2.0.0 破坏性变更
66
+
67
+ # 3. 推送 commit 和 tag
68
+ git push origin main
69
+ git push origin v1.5.1
70
+
71
+ # 4. 等 GitHub Actions 跑完
72
+ # 进度:https://github.com/code2rich/jpage/actions
73
+ # 成功后验证:
74
+ npm view @code2rich/jpage version
75
+ ```
76
+
77
+ **CI 会做什么**(见 `.github/workflows/release.yml`):
78
+
79
+ 1. checkout 代码
80
+ 2. `npm ci`(用 lockfile 锁定的依赖)
81
+ 3. lint + test(全绿才继续)
82
+ 4. build 前端产物
83
+ 5. **校验 tag 名 == package.json version**(防手滑:tag 标 v1.6.0 但 package.json 还写 1.5.0 会失败)
84
+ 6. `npm publish`(用 `NPM_TOKEN` 鉴权)
85
+ 7. 在 Actions Summary 写发布结果
86
+
87
+ ### 手动发版(应急,CI 挂了时用)
88
+
89
+ ```bash
90
+ # 1. 确认版本号已改、commit 已推
91
+ npm version patch
92
+ git push origin main
93
+ git push origin v1.5.1
94
+
95
+ # 2. 本地登录(首次需要)
96
+ npm login
97
+
98
+ # 3. 发布(账号开了 2FA 时需要 --otp)
99
+ npm publish --otp=<authenticator 当前的 6 位>
100
+
101
+ # 4. 验证
102
+ npm view @code2rich/jpage version
103
+ ```
104
+
105
+ > 手动发版后建议补打 tag(如果 `npm version` 已打就不用),保持「每个 npm 版本对应一个 git tag」。
106
+
107
+ ---
108
+
109
+ ## 版本号约定(语义化版本)
110
+
111
+ | 改动类型 | 命令 | 例子 | 含义 |
112
+ |---|---|---|---|
113
+ | 修 bug、小优化 | `npm version patch` | 1.5.0 → 1.5.1 | 向后兼容的修复 |
114
+ | 新功能 | `npm version minor` | 1.5.0 → 1.6.0 | 向后兼容的新能力 |
115
+ | 破坏性变更 | `npm version major` | 1.5.0 → 2.0.0 | 不兼容旧版的改动 |
116
+
117
+ > 重大变更应同步更新 `CHANGELOG`(如果有)或在 GitHub Release 写说明。
118
+
119
+ ---
120
+
121
+ ## 预发版(可选,beta/rc)
122
+
123
+ npm 支持预发布版本号,如 `1.6.0-beta.1`:
124
+
125
+ ```bash
126
+ npm version prerelease --preid=beta # 1.5.0 → 1.6.0-beta.0
127
+ git push origin main
128
+ git push origin v1.6.0-beta.0
129
+
130
+ # 用户安装 beta:npm install -g @code2rich/jpage@beta
131
+ # 正式版:npm install -g @code2rich/jpage@latest(默认)
132
+ ```
133
+
134
+ 预发版默认不进 `latest` tag,用户不主动指定 `@beta` 不会装到。
135
+
136
+ ---
137
+
138
+ ## 回滚 / 撤回
139
+
140
+ **npm 版本一旦发布,版本号永久占用,无法删除**(只能 deprecate)。
141
+
142
+ ### 标记弃用(deprecate)
143
+
144
+ ```bash
145
+ # 弃用某个版本(用户安装时会看到警告)
146
+ npm deprecate @code2rich/jpage@1.5.1 "有严重 bug,请用 1.5.2"
147
+ ```
148
+
149
+ ### 发布修复版本
150
+
151
+ 发一个新版本(如 `1.5.2`)修正问题,然后 deprecate 坏版本。**不要试图覆盖已发布的版本号**——npm 不允许重复发布同一版本。
152
+
153
+ ### 完全撤下包(72 小时内)
154
+
155
+ ```bash
156
+ # 包发布 72 小时内可 unpublish 整个包(慎用,会破坏所有依赖者)
157
+ # npm unpublish @code2rich/jpage --force
158
+ ```
159
+
160
+ > 超过 72 小时无法 unpublish。一般用 deprecate + 发新版本代替。
161
+
162
+ ---
163
+
164
+ ## 故障排查
165
+
166
+ ### CI 发版失败
167
+
168
+ | 错误 | 原因 | 处理 |
169
+ |---|---|---|
170
+ | `403 Forbidden - Two-factor authentication required` | `NPM_TOKEN` 是 session token 不是 granular token | 重新生成 granular token(bypass 2FA),更新 GitHub Secret |
171
+ | `403 Forbidden - You do not have permission` | token 权限不含 `@code2rich` scope | 重新生成 token,Packages 勾 `@code2rich` |
172
+ | `EPUBLISHCONFLICT - You cannot publish over` | 该版本号已发布过 | 升版本号再发,npm 不允许覆盖 |
173
+ | `tag (x) 与 package.json version (y) 不一致` | tag 名和 package.json 对不上 | 确保 `npm version` 自动打 tag,或手动对齐 |
174
+ | lint/test/build 失败 | 代码有问题 | 看 Actions 日志修,修完不用重打 tag(直接 push commit 到 main,tag 已存在不会重跑——需删 tag 重打) |
175
+
176
+ **删 tag 重跑**(修完代码后):
177
+
178
+ ```bash
179
+ git tag -d v1.5.1 # 删本地
180
+ git push origin :refs/tags/v1.5.1 # 删远程
181
+ # 重新打 tag(指向最新 commit)
182
+ git tag v1.5.1
183
+ git push origin v1.5.1
184
+ ```
185
+
186
+ ### `NPM_TOKEN` 过期 / 轮换
187
+
188
+ 1. npm 网站生成新 granular token
189
+ 2. 更新 GitHub Secret:`https://github.com/code2rich/jpage/settings/secrets/actions` → `NPM_TOKEN` → Update
190
+ 3. 下次发版自动用新 token
191
+
192
+ ---
193
+
194
+ ## 安全清单(每次发版前后自查)
195
+
196
+ - [ ] 没有把 npm token / `.npmrc` 含明文 token 的文件 commit 到仓库
197
+ - [ ] `.gitignore` 含 `.npmrc`(防误提交)
198
+ - [ ] CI 的 `NPM_TOKEN` 是 granular token(最小权限),不是账号 session token
199
+ - [ ] npm 账号开了 2FA(`auth-and-writes`)
200
+ - [ ] 过期的 token 已在 npm 网站撤销
201
+
202
+ ---
203
+
204
+ ## 相关文件
205
+
206
+ - `.github/workflows/release.yml` — 自动发版 workflow(tag 触发)
207
+ - `.github/workflows/ci.yml` — CI 测试 workflow(push/PR 触发)
208
+ - `package.json` 的 `publishConfig.access: "public"` — scoped 包公开发布(必须)
209
+ - `package.json` 的 `bin`、`engines`、`version` — 发布元信息