Просмотр исходного кода

feat: 技能 npx 一键安装(package.json + 安装器)+ 凭据回退与相对路径修复

- 新增 package.json(npm 包化,files 白名单/engines>=18)与 scripts/install.mjs
  安装器: 复制 SKILL/README/.env.example/scripts/templates 到
  ~/.claude/skills/smartbadge-ops,升级时保留 .env/.token/out(不覆盖已配置凭据)
- api.mjs/check-online.mjs 密码走 .env 回退链(findEnvFile+loadEnv),
  配好 .env 后 node scripts/api.mjs login / check-online.mjs 无参即可
- fetch-report-text.mjs: --url 支持列表接口相对 downloadUrl(带 token 鉴权
  下载,401 自动重登),修复相对路径下 new URL() 抛 TypeError
- README/SKILL.md: 安装章节改 npx 一条命令,提示词去掉 clone 与路径书写

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Huccct 2 недель назад
Родитель
Сommit
13e690b139
7 измененных файлов с 127 добавлено и 31 удалено
  1. 37 23
      README.md
  2. 1 1
      SKILL.md
  3. 27 0
      package.json
  4. 6 2
      scripts/api.mjs
  5. 4 2
      scripts/check-online.mjs
  6. 9 3
      scripts/fetch-report-text.mjs
  7. 43 0
      scripts/install.mjs

+ 37 - 23
README.md

@@ -4,29 +4,30 @@
 
 一个技能覆盖:巡检、故障自诊断、补跑/重试、OSS 同步、列表排查、报告解读、**月报/年报按需聚合分析**、线上部署核验。
 
-## 快速开始(在线安装)
+## 快速开始(npx 安装,无需克隆)
 
 ```bash
-# 1. 克隆技能仓库到本地
-git clone https://git.fmode.cn/lami/skills-smartbadge-ops.git
-cd skills-smartbadge-ops
+# 1. 一条命令安装/更新技能(自动放到 ~/.claude/skills/smartbadge-ops)
+npx --yes --package=git+https://git.fmode.cn/lami/skills-smartbadge-ops.git smartbadge-ops
 
-# 2. 配置凭据(仅登录密码必需;OSS 四项见下方密钥清单)
-cp .env.example .env          # Windows: copy .env.example .env
-# 编辑 .env,填写 SMARTBADGE_PASSWORD
+# 2. 配置凭据(按提示复制;Windows: copy .env.example .env)
+cd ~/.claude/skills/smartbadge-ops
+cp .env.example .env            # 编辑 .env,填写 SMARTBADGE_PASSWORD(及需要的 OSS 四项)
 
-# 3. 登录并一键核查
-node scripts/api.mjs login <密码>
+# 3. 登录并一键核查(密码亦可取自 .env,无需再传)
+node scripts/api.mjs login
 node scripts/check-online.mjs
 ```
 
 之后 Claude 中直接提问即可(如「昨天管道有问题吗」「帮我出 8 月月报」),技能通过「确认门」交互流程逐步完成。
 
-### 安装为 Claude/通用 AI 技能(任选其一)
+### 升级
 
-- **Claude Code(Windows)**: `cmd /c mklink /D "%USERPROFILE%\.claude\skills\smartbadge-ops" "<本仓库绝对路径>"`
-- **Claude Code(macOS/Linux)**: `ln -s "<本仓库绝对路径>" ~/.claude/skills/smartbadge-ops`
-- **其他 AI**: 把仓库目录当作技能目录配置(含 `SKILL.md` 的目录即可)。
+重跑第 1 步即可(已配置的 `.env`/登录 token 自动保留,不被覆盖)。
+
+### 安装到其他 AI
+
+技能安装完成后,`~/.claude/skills/smartbadge-ops` 是含 `SKILL.md` 的完整技能目录,在其他 AI 中直接指向该目录即可。
 
 ## 环境要求
 
@@ -60,7 +61,11 @@ node scripts/check-online.mjs
 
 ## 给 AI 的完整配置提示(安装时粘贴)
 
-**步骤 1**:克隆技能仓库(名称 `smartbadge-ops`)并按上方方式链接到 AI 技能目录。
+**步骤 1**:一条命令安装技能(自动安装到 AI 技能目录,无需克隆、无需记路径):
+
+```bash
+npx --yes --package=git+https://git.fmode.cn/lami/skills-smartbadge-ops.git smartbadge-ops
+```
 
 **步骤 2**:密钥清单。**只需 `SMARTBADGE_PASSWORD` 登录密码**(脚本自动发现同机服务端项目的 `server/.env` 读取 OSS 凭据);无服务端项目的环境才需要 OSS 四项(填入技能仓库 `.env` 或系统环境变量):
 
@@ -77,18 +82,27 @@ node scripts/check-online.mjs
 **步骤 3**:给 AI 的完整提示词模板(复制粘贴,替换 `<...>` 占位):
 
 ```markdown
-# 技能已安装: smartbadge-ops(智能工牌录音管道全功能运维)
-## 环境配置(SMARTBADGE_* 变量)
+# 技能安装: smartbadge-ops(智能工牌录音管道全功能运维)
+## 第一步 安装技能(npx 一条命令,自动装到 AI 技能目录)
+npx --yes --package=git+https://git.fmode.cn/lami/skills-smartbadge-ops.git smartbadge-ops
+cd ~/.claude/skills/smartbadge-ops && cp .env.example .env   # Windows: copy .env.example .env
+## 第二步 配置 env(复制本块到技能仓库 .env,或配系统环境变量)
 SMARTBADGE_API_BASE=https://recording.sh-lami.com
 SMARTBADGE_USERNAME=lami123456
 SMARTBADGE_PASSWORD=<填 API 登录密码>
-# 有服务端项目同机: OSS 四项免配,技能自动发现项目 server/.env
-# 无服务端项目: 在技能仓库 .env 填 OSS_ACCESS_KEY_ID/OSS_ACCESS_KEY_SECRET/OSS_REGION/OSS_BUCKET
-## 使用规范
-按 SKILL.md §1 确认门协议执行: 先复述我的问题与方案,回问「是这个意思吗?」;
+OSS_ACCESS_KEY_ID=xxx
+OSS_ACCESS_KEY_SECRET=xxx
+OSS_BUCKET=lm-recording-files
+OSS_REGION=cn-shanghai
+OSS_PUBLIC_DOMAIN=lm-recording-files.oss-cn-shanghai.aliyuncs.com
+## 第三步 登录核查
+node scripts/api.mjs login
+node scripts/check-online.mjs
+## 使用规范(遵照技能 SKILL.md §1 确认门协议)
+先复述我的问题与方案,回问「是这个意思吗?」再执行;
 只读操作(巡检/列表/对账/聚合)可一句备案继续;写操作(补跑/重试/补传/改配置)必须先罗列将修改的数据面并等我确认。
-## 上线自检
-先运行 node scripts/check-online.mjs 确认 12 项 PASS,再进入我的提问。
+## 上线自检
+先运行 node scripts/check-online.mjs 确认 12 项 PASS,再响应我的提问。
 ```
 
 **步骤 4**:验证——让 AI 说「做一次每日巡检」,应输出三行简报(PASS 项/警告项/留痕)。
@@ -99,4 +113,4 @@ SMARTBADGE_PASSWORD=<填 API 登录密码>
 
 ## 版本
 
-v1.0.1 — 2026-09-04,新增「给 AI 的完整配置提示」章节
+v1.1.0 — 2026-09-04,npx 一条命令安装(package.json + 安装器,自动落位到 AI 技能目录,升级保留 .env/.token);check-online 密码支持 .env 回退;fetch-report-text 支持列表相对 downloadUrl

+ 1 - 1
SKILL.md

@@ -44,7 +44,7 @@ export MSYS_NO_PATHCONV=1     # Windows Git Bash: 以 / 开头的路径参数先
 ```
 - **接口地址**:默认线上 `https://recording.sh-lami.com`;本地开发:环境变量 `SMARTBADGE_API_BASE=http://localhost:3002`(或各脚本 `--base`)。
 - **登录**:`node scripts/api.mjs login <密码>`,token 缓存到 `scripts/.token`(各脚本自动共享)。
-- **凭据回退链**(OSS 等):`--env 参数` > `SMARTBADGE_ENV_FILE` > 技能仓库根 `.env`(clone 后复制 `.env.example` 填值) > 自动发现(向上扫本仓库同级/兄弟目录含 `server/.env` 的项目,如服务端项目与技能放在同一父目录) > 已存在的 `OSS_*` 进程环境变量。
+- **凭据回退链**(OSS 等):`--env 参数` > `SMARTBADGE_ENV_FILE` > 技能仓库根 `.env`(npx 安装后复制 `.env.example` 填值) > 自动发现(向上扫本仓库同级/兄弟目录含 `server/.env` 的项目,如服务端项目与技能放在同一父目录) > 已存在的 `OSS_*` 进程环境变量。
 - 敏感凭据只存在于服务端项目 `server/.env`(OSS/讯飞/DeepSeek/masterKey),本技能仓库**不含任何密钥**;`node scripts/oss.mjs --find-env` 可查看实际采用的 `server/.env` 路径。
 
 ## §3 能力矩阵与编排表

+ 27 - 0
package.json

@@ -0,0 +1,27 @@
+{
+  "name": "skills-smartbadge-ops",
+  "version": "1.1.0",
+  "description": "智能工牌录音管道运维技能(Claude skill): 巡检/自诊断/补跑重试/OSS 直连/区间聚合(月报年报)/报告解读/部署核验",
+  "type": "module",
+  "bin": {
+    "smartbadge-ops": "./scripts/install.mjs"
+  },
+  "files": [
+    "SKILL.md",
+    "README.md",
+    ".env.example",
+    ".gitignore",
+    "scripts/",
+    "templates/"
+  ],
+  "engines": {
+    "node": ">=18"
+  },
+  "keywords": [
+    "claude-skill",
+    "smartbadge",
+    "ops",
+    "pipeline"
+  ],
+  "license": "MIT"
+}

+ 6 - 2
scripts/api.mjs

@@ -1,7 +1,7 @@
 #!/usr/bin/env node
 // 智能工牌录音管道 API 工具(零依赖, Node >=18)
 // 用法:
-//   node api.mjs login <密码>                       # 登录并缓存 token 到 scripts/.token
+//   node api.mjs login [<密码>]                      # 登录并缓存 token 到 scripts/.token(密码可取自 .env/环境变量)
 //   node api.mjs get /api/archive/missing
 //   node api.mjs post /api/archive/run '{"dates":["2026-09-03"]}'
 //   node api.mjs env                                # 查看连接诊断(BASE / 是否已登录)
@@ -9,7 +9,11 @@
 //   node api.mjs get /api/reports --base http://localhost:3002   # 指定接口地址
 // 环境变量: SMARTBADGE_API_BASE(默认线上 https://recording.sh-lami.com)、
 //          SMARTBADGE_USERNAME(默认 lami123456)、SMARTBADGE_PASSWORD(或参数传入)
-import { baseUrl, fail, login, httpJson, loadToken, TOKEN_FILE } from './lib.mjs';
+import { baseUrl, fail, login, httpJson, loadToken, TOKEN_FILE, findEnvFile, loadEnv } from './lib.mjs';
+
+// .env 密码回退(与 check-online 同链): 已有环境变量优先,不覆盖
+const env = findEnvFile();
+if (env.hit) loadEnv(env.hit);
 
 const args = process.argv.slice(2);
 const positional = [];

+ 4 - 2
scripts/check-online.mjs

@@ -3,9 +3,11 @@
 // 用法:
 //   node check-online.mjs                        # 核查线上 https://recording.sh-lami.com
 //   SMARTBADGE_API_BASE=http://localhost:3002 node check-online.mjs   # 核查本地
-// 密码: SMARTBADGE_PASSWORD 环境变量或第一个参数
-import { baseUrl, login, httpJson, loadToken, fail } from './lib.mjs';
+// 密码: SMARTBADGE_PASSWORD 环境变量或第一个参数(亦可来自 .env,经 lib 回退链自动加载)
+import { baseUrl, login, httpJson, loadToken, fail, findEnvFile, loadEnv } from './lib.mjs';
 
+const env = findEnvFile();
+if (env.hit) loadEnv(env.hit);
 const PASSWORD = process.env.SMARTBADGE_PASSWORD || process.argv[2];
 const base = baseUrl();
 if (!PASSWORD) fail('用法: node check-online.mjs [密码](或先 SMARTBADGE_PASSWORD=<密码>)');

+ 9 - 3
scripts/fetch-report-text.mjs

@@ -2,7 +2,7 @@
 // 报告正文拉取: --id 按 reportId 定位(经 /api/reports/:id)或 --url 直链,下载 HTML → 剥标签存 out/ 并打印
 // 用法:
 //   node fetch-report-text.mjs --id <reportId>            # 按报告 id(当前窗口内最佳)
-//   node fetch-report-text.mjs --url <ossUrl|downloadUrl> # 直链(自动跟随 302)
+//   node fetch-report-text.mjs --url <ossUrl|downloadUrl> # 直链(自动跟随 302;接口相对路径 /api/... 亦支持)
 //   node fetch-report-text.mjs --id xxxx --preview        # 只输出标题+前 28 行摘要
 //   node fetch-report-text.mjs --id xxxx --json           # 输出 {id,title,chars,file}
 //   node fetch-report-text.mjs --id xxxx --out 报告.txt   # 指定输出文件
@@ -25,7 +25,7 @@ const jsonOut = args.includes('--json');
 const base = baseUrl();
 
 if (!id && !url) fail('用法: node fetch-report-text.mjs --id <reportId> | --url <http(s)>  [--preview] [--json] [--out 路径]', 3);
-if (url && !/^https?:\/\//i.test(url)) fail(`--url 仅接受 http(s) 协议(拒绝 file:// 等本地协议),收到: ${url}`, 4);
+if (url && !/^https?:\/\//i.test(url) && !url.startsWith('/')) fail(`--url 仅接受 http(s) 绝对地址、或以 / 开头的接口相对路径(拒绝 file:// 等本地协议),收到: ${url}`, 4);
 
 async function getRedirectTarget() {
   const r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token: loadToken(), raw: true, relogin: true });
@@ -43,6 +43,12 @@ async function fetchHtml(target) {
   const controller = new AbortController();
   const timer = setTimeout(() => controller.abort(), 30000);
   try {
+    if (target.startsWith('/')) {
+      // 列表返回的接口相对 downloadUrl(302 至 OSS/本地回源): 走鉴权接口(带 token + 401 自动重登)
+      const r = await httpJson('GET', target, undefined, { base, token: loadToken(), json: false, relogin: true });
+      if (!r.ok) fail(`下载失败 HTTP ${r.status}: ${target}`, 6);
+      return r.text;
+    }
     const res = await fetch(target, { signal: controller.signal });
     if (!res.ok) fail(`下载失败 HTTP ${res.status}: ${target}`, 6);
     return await res.text();
@@ -55,7 +61,7 @@ async function fetchHtml(target) {
 
 const meta = id
   ? await getRedirectTarget()
-  : { id: `url-${Date.now()}`, target: url, title: path.basename(new URL(url).pathname) || 'report', type: '', period: url };
+  : { id: `url-${Date.now()}`, target: url, title: path.basename(url.split('?')[0]) || 'report', type: '', period: url };
 
 const html = await fetchHtml(meta.target);
 const text = htmlToText(html);

+ 43 - 0
scripts/install.mjs

@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+// 技能安装器(npx 入口): 将本包内容安装/更新到 ~/.claude/skills/smartbadge-ops
+// 保护文件: .env/.token/out/__pycache__ 不覆盖(已配置凭据与登录态在升级时保留)
+// 用法:
+//   npx --yes --package=<git|npm 包地址> smartbadge-ops            # 安装/更新
+//   node scripts/install.mjs --dest <路径>                          # 自定义安装目录(测试用)
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const PKG_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
+const args = process.argv.slice(2);
+const argVal = (name) => {
+  const i = args.indexOf(name);
+  return i >= 0 ? args[i + 1] : undefined;
+};
+const TARGET = argVal('--dest') || path.join(os.homedir(), '.claude', 'skills', 'smartbadge-ops');
+const PRODUCTS = ['SKILL.md', 'README.md', '.env.example', '.gitignore', 'scripts', 'templates'];
+const PROTECTED = new Set(['.env', '.env.local', '.token', 'out', '__pycache__', '.git']);
+
+function copyEntry(src, dest) {
+  if (PROTECTED.has(path.basename(src))) return;
+  const stat = fs.statSync(src);
+  if (stat.isDirectory()) {
+    fs.mkdirSync(dest, { recursive: true });
+    for (const name of fs.readdirSync(src)) copyEntry(path.join(src, name), path.join(dest, name));
+  } else {
+    fs.mkdirSync(path.dirname(dest), { recursive: true });
+    fs.copyFileSync(src, dest);
+  }
+}
+
+const existed = fs.existsSync(TARGET);
+for (const entry of PRODUCTS) {
+  const src = path.join(PKG_ROOT, entry);
+  if (fs.existsSync(src)) copyEntry(src, path.join(TARGET, entry));
+}
+
+const hasEnv = fs.existsSync(path.join(TARGET, '.env'));
+console.log(existed ? `已更新: ${TARGET}` : `已安装: ${TARGET}`);
+console.log(hasEnv ? '凭据/登录态已保留(.env/.token 未被覆盖)' : '下一步: 复制 .env.example 为 .env 并填入真实值(或配同名环境变量)');
+if (existed) console.log('提示: 升级后重跑 node scripts/check-online.mjs 验证; 若技能在运行中, 重启 Claude 会话以加载新 SKILL.md');