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

feat: Docker/Linux 容器适配(零内置凭据+上海时区固定+OSS 环境变量兜底+playwright 缺失提示)

- 移除全部脚本/模板内置默认用户名与密码,凭据仅经环境变量/.env 注入,缺省友好报错
- 日期口径固定 Asia/Shanghai(UTC 容器默认窗不再错位一天)
- 修复 oss.mjs: 文件探测失败时回退进程环境 OSS_*(Docker -e 注入场景)
- verify-online.py playwright 缺失时友好提示跳 check-online.mjs
- README 新增 Docker/Linux 部署一节 + 版本 v1.2.0
- 容器实测(Docker node:20 + UTC): 零凭据报错/登录/12项巡检/聚合窗口/OSS/API 7/7 PASS
Huccct 1 неделя назад
Родитель
Сommit
6d2d8a1486
9 измененных файлов с 78 добавлено и 35 удалено
  1. 3 3
      .env.example
  2. 31 7
      README.md
  3. 4 2
      SKILL.md
  4. 1 1
      package.json
  5. 2 2
      scripts/api.mjs
  6. 5 2
      scripts/lib.mjs
  7. 7 5
      scripts/oss.mjs
  8. 13 9
      scripts/report-aggregate.mjs
  9. 12 4
      scripts/verify-online.py

+ 3 - 3
.env.example

@@ -5,9 +5,9 @@
 # 接口地址(线上默认; 本地开发用 http://localhost:3002)
 SMARTBADGE_API_BASE=https://recording.sh-lami.com
 
-# 登录账号
-SMARTBADGE_USERNAME=lami123456
-SMARTBADGE_PASSWORD=<填登录密码>
+# 登录账号(无内置默认值——本仓库不含任何密钥;Docker 环境可用 --env-file / -e 注入同名环境变量)
+SMARTBADGE_USERNAME=<登录用户名>
+SMARTBADGE_PASSWORD=<填写 API 登录密码>
 
 # OSS 直连凭据(可选; 通常由自动发现的服务端项目 server/.env 提供; 无服务端项目的环境在此填写)
 # OSS_ACCESS_KEY_ID=xxx

+ 31 - 7
README.md

@@ -33,10 +33,33 @@ node scripts/check-online.mjs
 
 | 项 | 要求 |
 |---|---|
-| Node | >= 18(内建 fetch,零 npm 依赖) |
-| Python | 仅 `verify-online.py` 需要,含 playwright |
+| Node | >= 18(内建 fetch,零 npm 依赖;建议 node:20 镜像) |
+| Python | 仅 `verify-online.py` 需要,含 playwright + chromium(浏览器走查;**纯 Docker/无浏览器环境跳过**,用 `check-online.mjs`) |
 | 网络 | 可达 `recording.sh-lami.com`(或本地后端) |
-| 凭据 | 登录密码;OSS 凭据自动发现自项目 `server/.env` |
+| 时区 | 脚本日期口径固定 Asia/Shanghai(容器 UTC 时区无需配置) |
+| 凭据 | 登录用户名/密码(无内置默认值,见下);OSS 凭据可从服务端项目 `server/.env` 自动发现 |
+
+## Docker / Linux 部署(与服务器一致的容器环境)
+
+```bash
+# 1. 拉取安装(或 git clone 后用 node scripts/install.mjs --dest)
+npm install -g 或用 npx --yes --package=git+https://git.fmode.cn/lami/skills-smartbadge-ops.git smartbadge-ops
+
+# 2. 凭据不写入镜像——用环境变量注入(Docker 容器内 ~/.claude/skills 建议挂 volume 持久化 .token)
+docker run --rm -it \
+  -e SMARTBADGE_USERNAME=xxx \
+  -e SMARTBADGE_PASSWORD=xxx \
+  -e OSS_ACCESS_KEY_ID=xxx -e OSS_ACCESS_KEY_SECRET=xxx \
+  -e OSS_BUCKET=xxx -e OSS_REGION=xxx \
+  -v smartbadge-skill:/root/.claude/skills smartbadge-ops:latest \
+  node scripts/check-online.mjs
+```
+
+要点:
+- **零密钥入库**:全部脚本无内置用户名/密码/密钥,凭据仅经 `.env`(容器内)或进程环境变量注入;仓库级 `.gitignore` 排除 `.env`/`.token`/`out/`。
+- **无需 Python**:容器内只跑 Node 脚本即可完成巡检/聚合/OSS 对账;浏览器走查 `verify-online.py` 在宿主机或带 chromium 的环境执行。
+- **时区无关**:日期计算(`report-aggregate` 默认窗口等)显式按 Asia/Shanghai,容器默认 UTC 不会错位。
+- **Windows Git Bash 兼容**:以 `/` 开头的 API 路径参数需 `export MSYS_NO_PATHCONV=1`(仅 Windows;Linux/Docker 无此问题)。
 
 ## 脚本一览
 
@@ -72,8 +95,8 @@ npx --yes --package=git+https://git.fmode.cn/lami/skills-smartbadge-ops.git smar
 | 环境变量 | 必填 | 取值来源 | 说明 |
 |---|---|---|---|
 | `SMARTBADGE_API_BASE` | 否 | 线上 `https://recording.sh-lami.com` | 或本机后端 `http://localhost:3002` |
-| `SMARTBADGE_USERNAME` | 否 | 登录用户名 | 脚本默认 `lami123456` |
-| `SMARTBADGE_PASSWORD` | **是** | API 登录密码(与系统一致) | 唯一必需项 |
+| `SMARTBADGE_USERNAME` | **是** | 登录用户名(**无内置默认值**) | 缺省即报错提示 |
+| `SMARTBADGE_PASSWORD` | **是** | API 登录密码(与系统一致) | 无内置默认值 |
 | `OSS_ACCESS_KEY_ID` | 条件 | 自动发现服务端项目 `server/.env`;无则填入技能仓库 `.env` | 需 OSS 对账时才需要 |
 | `OSS_ACCESS_KEY_SECRET` | 条件 | 同上 | 需 OSS 对账时才需要 |
 | `OSS_REGION` | 条件 | 同上(如 `oss-cn-shanghai`) | 同上 |
@@ -88,8 +111,8 @@ npx --yes --package=git+https://git.fmode.cn/lami/skills-smartbadge-ops.git smar
 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 登录密码>
+SMARTBADGE_USERNAME=<登录用户名>
+SMARTBADGE_PASSWORD=<填 API 登录密码>
 OSS_ACCESS_KEY_ID=xxx
 OSS_ACCESS_KEY_SECRET=xxx
 OSS_BUCKET=lm-recording-files
@@ -113,4 +136,5 @@ node scripts/check-online.mjs
 
 ## 版本
 
+v1.2.0 — 2026-09-06,Docker/Linux 容器适配:① 零密钥原则——移除全部脚本/模板内置默认用户名与密码(登录凭据不再有任何代码内置值),凭据仅经环境变量/.env 注入,缺省脚本报错提示;② 时区固定 Asia/Shanghai(report-aggregate 默认窗在 UTC 容器不再错位一天);③ verify-online.py playwright 缺失时友好提示;④ 新增「Docker/Linux 部署」一节
 v1.1.0 — 2026-09-04,npx 一条命令安装(package.json + 安装器,自动落位到 AI 技能目录,升级保留 .env/.token);check-online 密码支持 .env 回退;fetch-report-text 支持列表相对 downloadUrl

+ 4 - 2
SKILL.md

@@ -40,12 +40,14 @@ description: 智能工牌录音管道全功能运维技能。当用户提到录
 ## §2 连接与凭据
 
 ```bash
-export MSYS_NO_PATHCONV=1     # Windows Git Bash: 以 / 开头的路径参数先防止被转成 Windows 路径
+export MSYS_NO_PATHCONV=1     # 仅 Windows Git Bash 需要(防 / 开头参数被转成 Windows 路径);Linux/Docker 无此问题
 ```
 - **接口地址**:默认线上 `https://recording.sh-lami.com`;本地开发:环境变量 `SMARTBADGE_API_BASE=http://localhost:3002`(或各脚本 `--base`)。
 - **登录**:`node scripts/api.mjs login <密码>`,token 缓存到 `scripts/.token`(各脚本自动共享)。
+- **用户名/密码无内置默认值**(1.2.0 起零密钥原则):必须经环境变量或 `.env` 提供,缺省脚本报错提示;Docker 容器用 `-e SMARTBADGE_USERNAME=/PASSWORD=` 或 `--env-file` 注入。
 - **凭据回退链**(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` 路径。
+- **平台**:技能本体零依赖 Node ≥18 跨平台(Windows/Linux/Docker 一致);`verify-online.py` 浏览器走查需要 playwright+chromium,容器/无浏览器环境跳过(用 `check-online.mjs` 替代);日期口径固定 Asia/Shanghai,容器 UTC 时区无需配置。
 
 ## §3 能力矩阵与编排表
 
@@ -203,7 +205,7 @@ pm2 logs smartbadge-api --lines 50   # 服务器错误日志
 2. 写操作(归档/分析/重试/补传/配置)一律先确认门,并提示互斥 429 可能性。
 3. 凭据只存 `server/.env`(讯飞/DeepSeek/OSS/masterKey),本仓库零密钥;git 提交需用户明确同意。
 4. **禁止直接在 `server/recordings/` 下手动跑 `process.py`**:对输入文件是**移动/删除**语义;排查过滤请用工作区副本或 `repair-oss`。
-5. Windows 本机 `PYTHON_BIN` 需指向真实 python.exe(生产默认 `python3`)——Python 相关仅服务器需要
+5. Python 仅服务器端管道需要;技能本体不调 Python(**Docker/Linux 容器零 Python 依赖可跑全部 Node 脚本**)。唯一例外 `verify-online.py`(浏览器走查)需在宿主机执行 `pip install playwright && playwright install chromium`
 6. `.token`/`out/`/`__pycache__` 已 gitignore,不提交。
 
 ## §15 结束协议(每任务收尾)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "skills-smartbadge-ops",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "description": "智能工牌录音管道运维技能(Claude skill): 巡检/自诊断/补跑重试/OSS 直连/区间聚合(月报年报)/报告解读/部署核验",
   "type": "module",
   "bin": {

+ 2 - 2
scripts/api.mjs

@@ -8,7 +8,7 @@
 //   node api.mjs get /api/reports --raw             # 输出完整响应(含 code/message)
 //   node api.mjs get /api/reports --base http://localhost:3002   # 指定接口地址
 // 环境变量: SMARTBADGE_API_BASE(默认线上 https://recording.sh-lami.com)、
-//          SMARTBADGE_USERNAME(默认 lami123456)、SMARTBADGE_PASSWORD(或参数传入)
+//          SMARTBADGE_USERNAME、SMARTBADGE_PASSWORD(或参数传入;均无内置默认值,见 .env.example)
 import { baseUrl, fail, login, httpJson, loadToken, TOKEN_FILE, findEnvFile, loadEnv } from './lib.mjs';
 
 // .env 密码回退(与 check-online 同链): 已有环境变量优先,不覆盖
@@ -41,7 +41,7 @@ async function main() {
   if (cmd === 'env') {
     const token = loadToken();
     console.log(`BASE    = ${baseUrl(baseOverride)}`);
-    console.log(`USERNAME= ${process.env.SMARTBADGE_USERNAME || 'lami123456'}`);
+    console.log(`USERNAME= ${process.env.SMARTBADGE_USERNAME || '<未配置: 设置环境变量或 .env>'}`);
     console.log(`TOKEN   = ${token ? `已登录 (${token.slice(0, 8)}... @ ${TOKEN_FILE})` : '未登录(先: node api.mjs login <密码>)'}`);
     return;
   }

+ 5 - 2
scripts/lib.mjs

@@ -69,12 +69,15 @@ export function saveToken(token) {
 }
 
 // ---------- 登录 ----------
+// 凭据只从参数/环境变量取值(无内置默认值——本仓库不含任何密钥,含 Docker/裸 Linux 环境)
 export async function login({ base, username, password }) {
-  if (!password) fail('用法: node api.mjs login <密码>(或环境变量 SMARTBADGE_PASSWORD)', 2);
+  const user = username || process.env.SMARTBADGE_USERNAME;
+  if (!user) fail('缺少用户名: 设置环境变量 SMARTBADGE_USERNAME(或 .env 中填写;查看 .env.example 模板)', 2);
+  if (!password) fail('缺少密码: 设置环境变量 SMARTBADGE_PASSWORD(或 .env 中填写;或作为参数: node api.mjs login <密码>)', 2);
   const res = await fetch(`${base}/api/auth/login`, {
     method: 'POST',
     headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
-    body: JSON.stringify({ username: username || process.env.SMARTBADGE_USERNAME || 'lami123456', password }),
+    body: JSON.stringify({ username: user, password }),
   });
   const json = await res.json().catch(() => ({ raw: '非 JSON 响应' }));
   if (!json.success || !json.data?.sessionToken) {

+ 7 - 5
scripts/oss.mjs

@@ -39,7 +39,9 @@ async function main() {
     for (const c of env.candidates) console.error(`  ${c}`);
     process.exit(2);
   }
-  if (!env.hit) {
+  // 凭据来源: .env 文件 > 进程环境已有 OSS_*(Docker -e 注入场景),文件缺失但环境变量齐备时允许继续
+  const hasOssEnv = ['OSS_ACCESS_KEY_ID', 'OSS_ACCESS_KEY_SECRET', 'OSS_BUCKET'].every((k) => process.env[k]);
+  if (!env.hit && !hasOssEnv) {
     console.error('未找到 OSS 凭据文件(探测序列如下),三选一:');
     for (const c of env.candidates) console.error(`  ${c}`);
     console.error('  → 1. 复制 .env.example 为 <技能仓库>/.env 并填入 OSS_* 四项');
@@ -47,15 +49,15 @@ async function main() {
     console.error('  → 3. 直接导出 OSS_* 环境变量(与 server/.env 同键名)');
     process.exit(2);
   }
-  loadEnv(env.hit);
+  if (env.hit) loadEnv(env.hit);
 
-  const serverDir = path.dirname(env.hit);
+  const serverDir = env.hit ? path.dirname(env.hit) : null;
   const missing = ['OSS_ACCESS_KEY_ID', 'OSS_ACCESS_KEY_SECRET', 'OSS_BUCKET'].filter((k) => !process.env[k]);
   if (missing.length) fail(`OSS 凭据不全,缺: ${missing.join(', ')}。请检查 --env 指向的 .env 或环境变量`, 4);
 
   // 客户端两态: 服务端项目模块优先(行为一致),否则内置零依赖实现
-  const ossClientPath = path.join(serverDir, 'services', 'oss-client.js');
-  const { listObjects, publicDomain } = fs.existsSync(ossClientPath)
+  const ossClientPath = serverDir ? path.join(serverDir, 'services', 'oss-client.js') : null;
+  const { listObjects, publicDomain } = ossClientPath && fs.existsSync(ossClientPath)
     ? await import(pathToFileURL(ossClientPath))
     : await import(pathToFileURL(path.join(SCRIPTS_DIR, 'oss-client-mini.mjs')));
   const objects = await listObjects(prefix);

+ 13 - 9
scripts/report-aggregate.mjs

@@ -19,18 +19,22 @@ const argVal = (name) => {
   return i >= 0 ? args[i + 1] : undefined;
 };
 
-function ymd(d) {
-  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
+// 日期口径固定 Asia/Shanghai(UTC+8,无夏令时): 容器默认 UTC 时区,若用本地年月日
+// 「今天/本月1日」默认窗在每天 00:00~08:00(UTC)会错位一天(2026-09-06 Docker 适配)
+function ymdShanghai(d) {
+  // 任意绝对时刻 → 北京时间 YYYY-MM-DD(内部 +8h 后读 UTC getters;输入勿再预偏移)
+  const t = new Date(d.getTime() + 8 * 3600 * 1000);
+  const p = (n) => String(n).padStart(2, '0');
+  return `${t.getUTCFullYear()}-${p(t.getUTCMonth() + 1)}-${p(t.getUTCDate())}`;
 }
 function parseDate(s, label) {
   if (!/^\d{4}-\d{2}-\d{2}$/.test(s ?? '')) fail(`${label} 需为 YYYY-MM-DD,收到: ${JSON.stringify(s)}`, 4);
-  const d = new Date(`${s}T00:00:00`);
+  const d = new Date(`${s}T00:00:00+08:00`); // 显式 +08:00,避免容器本地时区漂移(UTC 容器差 8h)
   if (Number.isNaN(d.getTime())) fail(`${label} 非合法日期: ${s}`, 4);
   return d;
 }
 
-const now = new Date();
-const todayYmd = ymd(now);
+const todayYmd = ymdShanghai(new Date());
 const startArg = argVal('--start');
 const endArg = argVal('--end');
 const type = argVal('--type') ?? 'all';
@@ -58,12 +62,12 @@ const auth = { base, token };
 const reports = await fetchAllReports({
   base, token,
   type: type === 'all' ? undefined : type,
-  start: ymd(start), end: ymd(end), deviceNo,
+  start: ymdShanghai(start), end: ymdShanghai(end), deviceNo,
 });
 
 // 2. 逐日 reconcile(并发,有损:失败跳过但披露)
 const dayList = [];
-for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) dayList.push(ymd(d));
+for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) dayList.push(ymdShanghai(d));
 const reconcile = new Map();
 if (!noReconcile) {
   for (let i = 0; i < dayList.length; i += concurrency) {
@@ -110,7 +114,7 @@ const actions = noReportDays.map((d) => ({
 }));
 
 const data = {
-  window: { start: ymd(start), end: ymd(end), days: dayList.length, generatedAt: new Date().toISOString() },
+  window: { start: ymdShanghai(start), end: ymdShanghai(end), days: dayList.length, generatedAt: new Date().toISOString() },
   type,
   deviceNo: deviceNo ?? '全部',
   totalReports: reports.length,
@@ -134,7 +138,7 @@ if (jsonOut) {
 }
 
 const lines = [];
-lines.push(`# 录音管道区间聚合(${ymd(start)} ~ ${ymd(end)})`);
+lines.push(`# 录音管道区间聚合(${ymdShanghai(start)} ~ ${ymdShanghai(end)})`);
 lines.push(`> 生成: ${data.window.generatedAt} | 口径: type=${type}, 设备=${data.deviceNo}, 窗口 ${dayList.length} 天`);
 lines.push('');
 lines.push('## 总体统计');

+ 12 - 4
scripts/verify-online.py

@@ -1,13 +1,21 @@
 #!/usr/bin/env python3
 # 浏览器走查: 登录 → 5 页(dashboard/recordings/logs/reports/pipeline) → 0 pageerror/console.error 断言
-# 环境变量: PLAYWRIGHT_BASE_URL(默认 https://recording.sh-lami.com)、SMARTBADGE_USERNAME/PASSWORD(默认 lami123456)
+# 环境: 需要 playwright + chromium(宿主机/带 GUI 环境);纯 Docker/无浏览器环境请改用 check-online.mjs(零依赖 12 项断言)
+# 环境变量: PLAYWRIGHT_BASE_URL(默认 https://recording.sh-lami.com)、SMARTBADGE_USERNAME、SMARTBADGE_PASSWORD(均必填)
 import asyncio
 import os
-from playwright.async_api import async_playwright
+import sys
+
+try:
+    from playwright.async_api import async_playwright
+except ImportError:
+    print('playwright 未安装。浏览器走查需在宿主机执行: pip install playwright && playwright install chromium')
+    print('Docker/无浏览器环境请跳过本脚本,使用: node scripts/check-online.mjs')
+    sys.exit(8)
 
 BASE = os.environ.get('PLAYWRIGHT_BASE_URL', 'https://recording.sh-lami.com/')
-USER = os.environ.get('SMARTBADGE_USERNAME', 'lami123456')
-PASS = os.environ.get('SMARTBADGE_PASSWORD', 'lami123456')
+USER = os.environ.get('SMARTBADGE_USERNAME') or sys.exit('缺少 SMARTBADGE_USERNAME(环境变量),不再有内置默认值')
+PASS = os.environ.get('SMARTBADGE_PASSWORD') or sys.exit('缺少 SMARTBADGE_PASSWORD(环境变量),不再有内置默认值')
 errors = []
 
 async def main():