Compare commits

..
Author SHA1 Message Date
johnQ 26352c4c69 docs: 新增技能维护节奏 MAINTENANCE.md 并在 README 挂入口 2026-10-31 22:26:00 +08:00
qiudl 6321a8528a Merge pull request 'chore(skill): 安全扫描警告清零 + 加入 skill-gate CI' (#14) from chore/scan-cleanup-and-ci into main
skill-gate / gate (push) Waiting to run
chore(skill): 安全扫描警告清零 + 加入 skill-gate CI
2026-10-31 14:24:12 +00:00
johnQ b390c29af3 chore(skill): 安全扫描警告清零 + 加入 skill-gate CI
skill-gate / gate (pull_request) Waiting to run
- scan-skill.sh: loopback 不算风险、命中走允许清单
- 11 个文件的 /Users/<机器名> 绝对路径去环境化(脚本 $HOME、文档 ~)
- 允许清单登记运维环境地址与示例占位符(非凭据),附理由
- scan: 57 warns -> 0 warns / 0 high
- 新增 .gitea/workflows/skill-gate.yml(validate --strict + scan + catalog --check + 测试)
2026-10-31 22:24:05 +08:00
qiudl f41b14ad0a Merge pull request 'chore(skill): 清理全部 23 条历史校验警告' (#13) from chore/skill-warning-cleanup into main
chore(skill): 清理全部 23 条历史校验警告
2026-10-31 14:21:11 +00:00
18 changed files with 342 additions and 241 deletions
+30
View File
@@ -0,0 +1,30 @@
name: skill-gate
on:
pull_request:
paths:
- "skills-**"
- "scripts/**"
- "install-skills.sh"
- "catalog.json"
push:
branches: [main]
paths:
- "skills-**"
- "scripts/**"
- "install-skills.sh"
- "catalog.json"
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 规范校验(strict:警告也算失败)
run: python3 scripts/validate-skills.py --strict
- name: 安全扫描(禁止高危项)
run: ./scripts/scan-skill.sh
- name: catalog 是否最新
run: python3 scripts/generate-catalog.py --check
- name: 安装器测试
run: bash tests/install-skills.test.sh
+10
View File
@@ -30,3 +30,13 @@
- 7 处非标准 frontmatter 字段(`arguments`、`user_invocable`)移入 `metadata`。 - 7 处非标准 frontmatter 字段(`arguments`、`user_invocable`)移入 `metadata`。
- 修复 `req-dev` 的 4 条相对链接;校验器改为跳过行内代码,消除 `![](...)` 误报。 - 修复 `req-dev` 的 4 条相对链接;校验器改为跳过行内代码,消除 `![](...)` 误报。
- `validate-skills.py`:23 个警告 → **0 个警告**。 - `validate-skills.py`:23 个警告 → **0 个警告**。
### 清理(2026-10-31 第二批)
- `scripts/scan-skill.sh`:回环地址(localhost/127.0.0.1)不再算风险;命中后走允许清单。
- 11 个文件里的 `/Users/<机器名>` 绝对路径去环境化(脚本改 `$HOME`,文档改 `~`)。
- 允许清单登记 6 类运维环境地址与示例占位符(非凭据),附理由。
- 安全扫描:57 个警告 → **0 个警告、0 个高危**。
- 新增 `.gitea/workflows/skill-gate.yml`:PR/main 跑 `validate --strict` + `scan` + `catalog --check` + 安装器测试。
- 新增 `MAINTENANCE.md`:技能维护节奏(每日漂移、每周 evals 抽样、每月目录审阅、每季度治理复核)。
+44
View File
@@ -0,0 +1,44 @@
# 技能维护节奏
> 配套 `SCOPE.md`(收不收)、`skill-creator`(怎么建)、`docs/skill-routing.md`(用哪个)。
> 本文件管「多久做一次什么」。
## 每次改动(任何 PR)
1. 过 `SCOPE.md` 收录门槛(真实失败 + 组合不出来),新技能默认先 `beta`。
2. 跑四道门禁:`validate-skills.py --strict`、`scan-skill.sh`、`generate-catalog.py --check`、`tests/install-skills.test.sh`。
3. 技能有增删改 → **同步回改** `docs/skill-routing.md` 与 `catalog.json`。
4. 更新 `plugin.json` 版本号与 `CHANGELOG.md`。
## 每日(随 7:30 项目日报)
- 两台机器跑 `install-skills.sh --audit`:**drift > 0 就要报**,并注明是「未安装 / 版本落后 / 本地改动」。
- 门禁失败或扫描出高危:立即处理,不等到日报。
- 例外:`beta` 与 `deprecated` 不计入 drift。
## 每周一 10:00 — evals 抽样回归
- 范围:上周改过的技能 + 3 个高风险技能(`dev-coding`、`dev-review`、`req-design`)。
- 每技能取 1 条关键用例,做「有技能 / 无技能」对比,按 rubric 打分。
- 结果记录到 `reports/evals/YYYY-Www.md`,进周报。
- 连续两次无增量的技能 → 按 `SCOPE.md` 评估降级。
- 不做全量(61 技能 × 3 用例成本过高);全量只在治理大改时做。
## 每月 1 日 10:00 — 漂移与目录审阅
- 两台机器 `--audit` 漂移必须为 0(beta/deprecated 除外)。
- 审 `catalog.json`:`description` 长度 <80 或 >600 的标黄;含「各种 / 等等 / 相关」这类泛化词的标出。
- 第三方技能(arkcli 等)有变动时,评估是否影响团队技能。
## 每季度首月 1 日 10:30 — 治理复核
- 人工过一遍 `docs/skill-routing.md`:删掉不再用的引用、补上新技能。
- 清理 `deprecated`:满 3 个月且无回滚需求 → 删除插件目录。
- 复核分桶与调用模式:还有没有命令型技能占着 `model-invoked`。
- 更新本文件与 `CHANGELOG.md`。
## 责任分工
- 项目经理 Bot:执行节奏、出周/月报、盯漂移与门禁。
- 改技能的 agent:负责门禁四件套与路由回改。
- 人工拍板项:废弃技能、路由变更、`promoted` 晋升。
+1
View File
@@ -210,3 +210,4 @@ A: SSE 模式直连远程服务器,零依赖开箱即用(推荐)。stdio
- **校验 / 安全 / 清单**:`python3 scripts/validate-skills.py`、`./scripts/scan-skill.sh`、`python3 scripts/generate-catalog.py` - **校验 / 安全 / 清单**:`python3 scripts/validate-skills.py`、`./scripts/scan-skill.sh`、`python3 scripts/generate-catalog.py`
- **建技能**:走 `skill-creator` 技能(收录门槛、分桶、调用模式、evals、四段式文档、路由回改),不要手写。 - **建技能**:走 `skill-creator` 技能(收录门槛、分桶、调用模式、evals、四段式文档、路由回改),不要手写。
- **路由表**:`docs/skill-routing.md`(什么任务用哪个技能,人读;由原 `ai-proj-helper-entry` 技能降级而来)。 - **路由表**:`docs/skill-routing.md`(什么任务用哪个技能,人读;由原 `ai-proj-helper-entry` 技能降级而来)。
- **维护节奏**:`MAINTENANCE.md`(每次改动 / 每日 / 每周 / 每月 / 每季度各做什么)。
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"repo": "ai-proj-helper", "repo": "ai-proj-helper",
"generated_at": "2026-10-31T14:21:07Z", "generated_at": "2026-10-31T14:24:05Z",
"total": 61, "total": 61,
"counts": { "counts": {
"promoted/user-invoked": 18, "promoted/user-invoked": 18,
@@ -140,7 +140,7 @@
"description": "Multi-agent orchestration using OpenAI Swarm patterns. Coordinate specialized agents for complex development workflows with handoffs and context sharing.", "description": "Multi-agent orchestration using OpenAI Swarm patterns. Coordinate specialized agents for complex development workflows with handoffs and context sharing.",
"description_len": 153, "description_len": 153,
"path": "skills-dev/agent-swarm-plugin/skills/SKILL.md", "path": "skills-dev/agent-swarm-plugin/skills/SKILL.md",
"digest": "a0d8a329ab9468f5" "digest": "4164f82025a421bb"
}, },
{ {
"name": "ai-chat", "name": "ai-chat",
@@ -250,7 +250,7 @@
"description": "软件编码开发技能。用于代码编写、功能实现、重构优化。集成 ai-proj CLI 进行任务管理和进度跟踪。核心支持 Go 后端 + Vue/React 前端开发。", "description": "软件编码开发技能。用于代码编写、功能实现、重构优化。集成 ai-proj CLI 进行任务管理和进度跟踪。核心支持 Go 后端 + Vue/React 前端开发。",
"description_len": 81, "description_len": 81,
"path": "skills-dev/dev-coding-plugin/skills/SKILL.md", "path": "skills-dev/dev-coding-plugin/skills/SKILL.md",
"digest": "1b754e668965132f" "digest": "cc43720aa12bb8f6"
}, },
{ {
"name": "dev-commit", "name": "dev-commit",
@@ -426,7 +426,7 @@
"description": "Slark 仓库 staging、生产和 Desktop 安装包的端到端 CI/CD 可执行 runbook。覆盖本地预推快检、PR 与内网 ci/internal-gate 门禁、合并 origin/main、生产前强制 staging 验收、审批式生产发布,以及在 m5max 构建签名、公证并上传 macOS/Windows Desktop 包到 OSS。当用户要在 slark(qiudl/qiu-slark)里发布/上线、部署到生产或 staging、发布 Desktop 安装包、盯 CI/合并 PR、复现或验证运行时行为、或排查发布链/推送路由问题时使用。", "description": "Slark 仓库 staging、生产和 Desktop 安装包的端到端 CI/CD 可执行 runbook。覆盖本地预推快检、PR 与内网 ci/internal-gate 门禁、合并 origin/main、生产前强制 staging 验收、审批式生产发布,以及在 m5max 构建签名、公证并上传 macOS/Windows Desktop 包到 OSS。当用户要在 slark(qiudl/qiu-slark)里发布/上线、部署到生产或 staging、发布 Desktop 安装包、盯 CI/合并 PR、复现或验证运行时行为、或排查发布链/推送路由问题时使用。",
"description_len": 285, "description_len": 285,
"path": "skills-dev/slark-cicd-plugin/skills/SKILL.md", "path": "skills-dev/slark-cicd-plugin/skills/SKILL.md",
"digest": "2919c19d8a38dd23" "digest": "1ff9b89a5fbc6e2d"
}, },
{ {
"name": "slark-dsh-cicd", "name": "slark-dsh-cicd",
@@ -437,7 +437,7 @@
"description": "DSH 插件包与 Slark 宿主补丁的发布 runbook。覆盖 npm run pack 双 tgz 双资产、无上游写权限时的 fork 发布、干净单提交与树一致性 gate、改指向后必须重新打包、draft→publish 节奏、GitHub HTTPS/SSH 与 fetch 卡死处置、发布后 SHA 核对、app.asar 完整性约束、ai-proj 发布状态对齐。当用户要发布或升级 DSH 插件(dsh-worktable 等)、发布/更新 Slark 宿主补丁、push 或下载 GitHub 失败、插件「检查更新」看不到新版本、需要 fork 发布、或排查 asar 完整性/签名问题时使用。", "description": "DSH 插件包与 Slark 宿主补丁的发布 runbook。覆盖 npm run pack 双 tgz 双资产、无上游写权限时的 fork 发布、干净单提交与树一致性 gate、改指向后必须重新打包、draft→publish 节奏、GitHub HTTPS/SSH 与 fetch 卡死处置、发布后 SHA 核对、app.asar 完整性约束、ai-proj 发布状态对齐。当用户要发布或升级 DSH 插件(dsh-worktable 等)、发布/更新 Slark 宿主补丁、push 或下载 GitHub 失败、插件「检查更新」看不到新版本、需要 fork 发布、或排查 asar 完整性/签名问题时使用。",
"description_len": 306, "description_len": 306,
"path": "skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md", "path": "skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md",
"digest": "44a1322e15d42845" "digest": "9819e2d515ab44e0"
}, },
{ {
"name": "data-excel", "name": "data-excel",
+1 -1
View File
@@ -241,7 +241,7 @@ on:
- devflow-claude: `plugins/req/commands/release.md` - devflow-claude: `plugins/req/commands/release.md`
- REQ-20260416-0017(母需求) - REQ-20260416-0017(母需求)
- 当前 build.yaml: `/Users/donglinlai/coding/qiudl/new-ai-proj/.gitea/workflows/build.yaml` - 当前 build.yaml: `~/coding/qiudl/new-ai-proj/.gitea/workflows/build.yaml`
- Gitea Release API: https://docs.gitea.com/api/next/#tag/repository/operation/repoCreateRelease - Gitea Release API: https://docs.gitea.com/api/next/#tag/repository/operation/repoCreateRelease
--- ---
+3 -3
View File
@@ -23,7 +23,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "/Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/session-context.sh", "command": "~/coding/qiudl/ai-proj-helper/hooks/session-context.sh",
"timeout": 10 "timeout": 10
} }
] ]
@@ -35,7 +35,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "/Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/pre-tool-confirm.sh", "command": "~/coding/qiudl/ai-proj-helper/hooks/pre-tool-confirm.sh",
"timeout": 30 "timeout": 30
} }
] ]
@@ -48,7 +48,7 @@
### 方式 2:一键安装脚本 ### 方式 2:一键安装脚本
```bash ```bash
bash /Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/install.sh bash ~/coding/qiudl/ai-proj-helper/hooks/install.sh
``` ```
## 验证 ## 验证
+1 -1
View File
@@ -8,7 +8,7 @@
# 安装方式: # 安装方式:
# 在 ~/.claude/settings.json 的 hooks.SessionStart 配置: # 在 ~/.claude/settings.json 的 hooks.SessionStart 配置:
# { # {
# "command": "/Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/session-context.sh", # "command": "$HOME/coding/qiudl/ai-proj-helper/hooks/session-context.sh",
# "timeout": 10 # "timeout": 10
# } # }
# #
+8
View File
@@ -3,3 +3,11 @@
# 例:某技能文档把危险命令当作“禁止清单”引用,而非真的要执行。 # 例:某技能文档把危险命令当作“禁止清单”引用,而非真的要执行。
skills-dev/slark-cicd-plugin::NEVER_AUTO skills-dev/slark-cicd-plugin::NEVER_AUTO
skills-dev/slark-cicd-plugin::destructive skills-dev/slark-cicd-plugin::destructive
# 运维 runbook 里的环境地址(公开可达的服务入口,非凭据),保留以维持 runbook 可执行性
skills-req/req-plugin::http://100.118.62.18
skills-dev/slark-cicd-plugin::http://15.135.112.181
skills-dev/dev-deploy-plugin::http://39.104.87.246
skills-dev/ai-chat-plugin::http://39.105.150.219
skills-dev/dev-deploy-plugin::http://www.apple.com/DTDs
skills-dev/dev-cicd-plugin::secrets.REPO_TOKEN
+9 -1
View File
@@ -24,7 +24,7 @@ HIGH_PATTERNS=(
# Warnings: machine-specific paths, plain http endpoints. # Warnings: machine-specific paths, plain http endpoints.
WARN_PATTERNS=( WARN_PATTERNS=(
'/Users/[A-Za-z0-9._-]+/' '/Users/[A-Za-z0-9._-]+/'
'http://[A-Za-z0-9.-]+' 'http://[A-Za-z0-9.:-]+'
'password[[:space:]]*=[[:space:]]*[^$]' 'password[[:space:]]*=[[:space:]]*[^$]'
) )
@@ -67,6 +67,14 @@ for dir in "${targets[@]}"; do
done done
for pattern in "${WARN_PATTERNS[@]}"; do for pattern in "${WARN_PATTERNS[@]}"; do
while IFS= read -r hit; do while IFS= read -r hit; do
# 本机回环地址不算风险(本地开发/测试示例)
case "$hit" in
*"http://localhost"*|*"http://127.0.0.1"*) continue ;;
esac
if is_allowlisted "${hit%%:*}" "$hit"; then
echo "[allow] $rel: ${hit#*:}"
continue
fi
echo "[warn] $rel: ${hit%%:*}:${hit#*:}" echo "[warn] $rel: ${hit%%:*}:${hit#*:}"
WARN=$((WARN + 1)) WARN=$((WARN + 1))
done < <(grep -rnE --include='*.md' --include='*.sh' --include='*.py' --include='*.js' --include='*.ts' -- "$pattern" "$dir" 2>/dev/null | cut -c1-200) done < <(grep -rnE --include='*.md' --include='*.sh' --include='*.py' --include='*.js' --include='*.ts' -- "$pattern" "$dir" 2>/dev/null | cut -c1-200)
@@ -158,7 +158,7 @@ Agent 之间的控制权转移机制:
```bash ```bash
/swarm start "优化数据库查询性能" \ /swarm start "优化数据库查询性能" \
--context project=/Users/coolbuy-dev/coding/new-ai-proj \ --context project=~/coding/new-ai-proj \
--context stack=Go,PostgreSQL,Redis \ --context stack=Go,PostgreSQL,Redis \
--context module=backend/services --context module=backend/services
``` ```
@@ -223,7 +223,7 @@ agents:
max_turns: 5 max_turns: 5
context_variables: context_variables:
project_root: /Users/coolbuy-dev/coding/new-ai-proj project_root: ~/coding/new-ai-proj
backend_lang: Go 1.21 backend_lang: Go 1.21
frontend_framework: Vue 3 frontend_framework: Vue 3
database: PostgreSQL 15 database: PostgreSQL 15
@@ -288,7 +288,7 @@ handoff_rules:
```bash ```bash
# OpenClaw 调用 Melbourne Claude Code # OpenClaw 调用 Melbourne Claude Code
ssh melbourne "cd /Users/coolbuy-dev/coding/new-ai-proj && \ ssh melbourne "cd ~/coding/new-ai-proj && \
/opt/homebrew/bin/claude --dangerously-skip-permissions \ /opt/homebrew/bin/claude --dangerously-skip-permissions \
-p '/swarm start 实现任务批量删除功能'" -p '/swarm start 实现任务批量删除功能'"
``` ```
+6 -6
View File
@@ -629,8 +629,8 @@ fi
### 正确做法(两行 export,别多一步) ### 正确做法(两行 export,别多一步)
```bash ```bash
export HOME="/Users/johnq" # 必须补:git 等直接读 $HOME export HOME="~" # 必须补:git 等直接读 $HOME
export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:$PATH" # node/npm/pnpm export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:$PATH" # node/npm/pnpm
cd <项目> && npm install && npm run build cd <项目> && npm install && npm run build
``` ```
@@ -643,11 +643,11 @@ cd <项目> && npm install && npm run build
→ **「node 能跑」≠ 环境完整**,别据此跳过 `HOME`。 → **「node 能跑」≠ 环境完整**,别据此跳过 `HOME`。
- **本机 node 必须用这一支**(定义在 `~/.zprofile`,对齐项目 `engines.node`): - **本机 node 必须用这一支**(定义在 `~/.zprofile`,对齐项目 `engines.node`):
`/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin/node` = v22.22.3,npm 10.9.8。 `~/.local/node-v22.22.3-darwin-arm64/bin/node` = v22.22.3,npm 10.9.8。
- `/opt/homebrew/bin/node` 是**另一支 v26**,不要混用;`/usr/local/bin/node` 不存在。 - `/opt/homebrew/bin/node` 是**另一支 v26**,不要混用;`/usr/local/bin/node` 不存在。
- 备选①:`/bin/zsh -lic '<命令>'`(登录 zsh 会读 `~/.zprofile`)。 - 备选①:`/bin/zsh -lic '<命令>'`(登录 zsh 会读 `~/.zprofile`)。
- 备选②:绝对路径直调 `/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin/node build.mjs`。 - 备选②:绝对路径直调 `~/.local/node-v22.22.3-darwin-arm64/bin/node build.mjs`。
- 路径变了先 `ls -d /Users/johnq/.local/node-v*-darwin-arm64/bin` 重新取**真实版本号**,不要猜。 - 路径变了先 `ls -d ~/.local/node-v*-darwin-arm64/bin` 重新取**真实版本号**,不要猜。
### 交付纪律(配合「Step 0:验证优先」) ### 交付纪律(配合「Step 0:验证优先」)
@@ -657,6 +657,6 @@ cd <项目> && npm install && npm run build
- 校验环境的最快方式(输出直接贴进交付): - 校验环境的最快方式(输出直接贴进交付):
```bash ```bash
export HOME="/Users/johnq"; export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:$PATH" export HOME="~"; export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:$PATH"
node -v && npm -v && git status --short | head -3 node -v && npm -v && git status --short | head -3
``` ```
+3 -3
View File
@@ -8,7 +8,7 @@ description: Slark 仓库 staging、生产和 Desktop 安装包的端到端 CI/C
驱动 Slark 从改代码到上线的端到端流程。权威流程与完整细节见仓库文档 驱动 Slark 从改代码到上线的端到端流程。权威流程与完整细节见仓库文档
`docs/cicd-staging-production.md`(本技能命令与之一致);本文件是可直接照做的分阶段清单。 `docs/cicd-staging-production.md`(本技能命令与之一致);本文件是可直接照做的分阶段清单。
仓库:`qiudl/qiu-slark`(本机克隆通常在 `/Users/donglinlai/coding/slark`)。所有命令在**仓库根**执行。 仓库:`qiudl/qiu-slark`(本机克隆通常在 `~/coding/slark`)。所有命令在**仓库根**执行。
`ai-proj` 相关命令加 `GODEBUG=netdns=go` 前缀。 `ai-proj` 相关命令加 `GODEBUG=netdns=go` 前缀。
## 铁律(先记死) ## 铁律(先记死)
@@ -192,9 +192,9 @@ REQ-20260819-0005 / Desktop 0.3.1:
- m5max 原生构建、Developer ID 签名并公证/staple macOS DMG/ZIP;交叉构建 Windows x64 NSIS EXE。 - m5max 原生构建、Developer ID 签名并公证/staple macOS DMG/ZIP;交叉构建 Windows x64 NSIS EXE。
- Darwin 不能执行 Windows 原生 `smoke:windows`。只有发布负责人明确接手 Windows 原生验收时才能继续交叉打包,交付证据必须标注该边界。 - Darwin 不能执行 Windows 原生 `smoke:windows`。只有发布负责人明确接手 Windows 原生验收时才能继续交叉打包,交付证据必须标注该边界。
- 资产不回传开发机;由 m5max 直接上传 `xiaoqu-public-file/slark/desktop/releases/v<version>/`,manifest 最后更新。 - 资产不回传开发机;由 m5max 直接上传 `xiaoqu-public-file/slark/desktop/releases/v<version>/`,manifest 最后更新。
- `xiaoqu-public-file` 的 canonical 凭证源是发布控制机上的 `/Users/donglinlai/coding/param.rxt`(必须为 `0600`);AK/SK 分别位于 XML 标签 `ossAccessKeyId` / `ossAccessKeySecret`。不得改猜 Bitwarden、Keychain、通用 `credentials.env` 或 m5max 本地配置;只有该文件缺失、权限不符或标签解析为空时才停止并报告。 - `xiaoqu-public-file` 的 canonical 凭证源是发布控制机上的 `~/coding/param.rxt`(必须为 `0600`);AK/SK 分别位于 XML 标签 `ossAccessKeyId` / `ossAccessKeySecret`。不得改猜 Bitwarden、Keychain、通用 `credentials.env` 或 m5max 本地配置;只有该文件缺失、权限不符或标签解析为空时才停止并报告。
- m5max 不持久化 OSS 凭证。发布控制机从 `param.rxt` 提取 AK/SK 后,只用 `printf '%s\n%s\n' ... | ssh m5max ...` 经 SSH stdin 传两行;远端用 `read -r` 接收并仅为当次 shell 导出 `OSS_ACCESS_KEY_ID` / `OSS_ACCESS_KEY_SECRET`,随后运行上传脚本。禁止 `scp` 凭证文件、写远端临时文件或把值打印到日志。 - m5max 不持久化 OSS 凭证。发布控制机从 `param.rxt` 提取 AK/SK 后,只用 `printf '%s\n%s\n' ... | ssh m5max ...` 经 SSH stdin 传两行;远端用 `read -r` 接收并仅为当次 shell 导出 `OSS_ACCESS_KEY_ID` / `OSS_ACCESS_KEY_SECRET`,随后运行上传脚本。禁止 `scp` 凭证文件、写远端临时文件或把值打印到日志。
- m5max 非交互 SSH 必须显式补 `PATH=/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH`;已验证 `ossutil` 位于 `/Users/johnq/.local/bin/ossutil`。执行前检查 Node、三个安装包、精确 main SHA 和干净工作树,避免因交互 shell 环境差异重新猜路径。 - m5max 非交互 SSH 必须显式补 `PATH=/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH`;已验证 `ossutil` 位于 `~/.local/bin/ossutil`。执行前检查 Node、三个安装包、精确 main SHA 和干净工作树,避免因交互 shell 环境差异重新猜路径。
- m5max 默认出口若走 `utun`,只为当次 `ossutil` 使用 `--bind-address "$(ipconfig getifaddr en0)"`,不得修改整机 Tailscale/默认路由。 - m5max 默认出口若走 `utun`,只为当次 `ossutil` 使用 `--bind-address "$(ipconfig getifaddr en0)"`,不得修改整机 Tailscale/默认路由。
- 完成前核对三份资产 SHA256/大小、macOS 签名与公证、公开 URL/manifest,并回写 ai-proj 证据。 - 完成前核对三份资产 SHA256/大小、macOS 签名与公证、公开 URL/manifest,并回写 ai-proj 证据。
@@ -53,8 +53,8 @@ gh api repos/<owner>/<repo> --jq '.permissions' # 关键:{"pull":true,"push":
## 阶段 1:发布线 A — DSH 插件(有上游写权限) ## 阶段 1:发布线 A — DSH 插件(有上游写权限)
```bash ```bash
export HOME="/Users/johnq" export HOME="~"
export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH" export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH"
cd <repo>/<pkg> cd <repo>/<pkg>
npm run check && npm run test:gate && npm run pack npm run check && npm run test:gate && npm run pack
shasum -a 256 dist/v<ver>/*.tgz # 两个文件必须同 sha shasum -a 256 dist/v<ver>/*.tgz # 两个文件必须同 sha
@@ -6,8 +6,8 @@
## 0. 变量与前置 ## 0. 变量与前置
```bash ```bash
export HOME="/Users/johnq" export HOME="~"
export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH" export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH"
UP=<upstream-owner>/<repo> # 例如 Aisland-SJL/dsh-worktable(外部作者,无写权限) UP=<upstream-owner>/<repo> # 例如 Aisland-SJL/dsh-worktable(外部作者,无写权限)
ME=<your-account> # 例如 qiudl ME=<your-account> # 例如 qiudl
@@ -78,7 +78,7 @@ git status # 应该看不到 setup_env.local.sh
```bash ```bash
# 1. 确保您创建了本地配置文件 # 1. 确保您创建了本地配置文件
cd /Users/junhuang/coolbuy/claude-marketplace/plugins/doubao-voice-plugin/scripts cd ~/coolbuy/claude-marketplace/plugins/doubao-voice-plugin/scripts
cp setup_env.local.sh.example setup_env.local.sh cp setup_env.local.sh.example setup_env.local.sh
# 编辑 setup_env.local.sh,填入您的凭证 # 编辑 setup_env.local.sh,填入您的凭证
@@ -16,7 +16,7 @@ ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis" BASE_URL = "https://open.feishu.cn/open-apis"
# 源文件 # 源文件
SOURCE_FILE = "/Users/donglinlai/Downloads/酷采团购系统优化进度表.dbt.xlsx" SOURCE_FILE = "$HOME/Downloads/酷采团购系统优化进度表.dbt.xlsx"
class FeishuBitable: class FeishuBitable:
@@ -1,212 +1,212 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
上传用户指定的图片到飞书云文档 上传用户指定的图片到飞书云文档
""" """
import requests import requests
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef" ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho" ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis" BASE_URL = "https://open.feishu.cn/open-apis"
# 用户指定的图片 # 用户指定的图片
IMAGE_PATH = "/Users/donglinlai/Downloads/u274.png" IMAGE_PATH = "$HOME/Downloads/u274.png"
_token = None _token = None
_token_expires = None _token_expires = None
def get_token(): def get_token():
global _token, _token_expires global _token, _token_expires
if _token and _token_expires and datetime.now() < _token_expires: if _token and _token_expires and datetime.now() < _token_expires:
return _token return _token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal" url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={ response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID, "app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET "app_secret": ZHIYUN_APP_SECRET
}) })
data = response.json() data = response.json()
if data.get("code") != 0: if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}") raise Exception(f"获取 token 失败: {data}")
_token = data["tenant_access_token"] _token = data["tenant_access_token"]
_token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60) _token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return _token return _token
def headers(): def headers():
return { return {
"Authorization": f"Bearer {get_token()}", "Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
def set_document_permission(document_id: str, editable: bool = True): def set_document_permission(document_id: str, editable: bool = True):
""" """
设置文档权限 设置文档权限
Args: Args:
document_id: 文档ID document_id: 文档ID
editable: True=组织内可编辑, False=组织内只读 editable: True=组织内可编辑, False=组织内只读
""" """
url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public" url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public"
payload = { payload = {
"external_access_entity": "open", "external_access_entity": "open",
"security_entity": "anyone_can_view", "security_entity": "anyone_can_view",
"comment_entity": "anyone_can_view", "comment_entity": "anyone_can_view",
"share_entity": "anyone", "share_entity": "anyone",
"link_share_entity": "tenant_editable" if editable else "tenant_readable", "link_share_entity": "tenant_editable" if editable else "tenant_readable",
"invite_external": False "invite_external": False
} }
response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload) response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload)
data = response.json() data = response.json()
if data.get("code") == 0: if data.get("code") == 0:
print(f" 权限设置成功: {'组织内可编辑' if editable else '组织内只读'}") print(f" 权限设置成功: {'组织内可编辑' if editable else '组织内只读'}")
return True return True
else: else:
print(f" [WARN] 权限设置失败: {data.get('msg')}") print(f" [WARN] 权限设置失败: {data.get('msg')}")
return False return False
def create_document(title: str, editable: bool = True): def create_document(title: str, editable: bool = True):
"""创建文档并设置权限""" """创建文档并设置权限"""
url = f"{BASE_URL}/docx/v1/documents" url = f"{BASE_URL}/docx/v1/documents"
response = requests.post(url, headers=headers(), json={"title": title}) response = requests.post(url, headers=headers(), json={"title": title})
data = response.json() data = response.json()
if data.get("code") != 0: if data.get("code") != 0:
raise Exception(f"创建文档失败: {data}") raise Exception(f"创建文档失败: {data}")
doc = data["data"]["document"] doc = data["data"]["document"]
document_id = doc["document_id"] document_id = doc["document_id"]
# 自动设置权限 # 自动设置权限
if editable: if editable:
set_document_permission(document_id, editable=True) set_document_permission(document_id, editable=True)
return document_id return document_id
def create_image_block(document_id: str): def create_image_block(document_id: str):
"""创建空图片块""" """创建空图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children"
payload = { payload = {
"children": [{ "children": [{
"block_type": 27, "block_type": 27,
"image": {} "image": {}
}] }]
} }
response = requests.post(url, headers=headers(), json=payload) response = requests.post(url, headers=headers(), json=payload)
data = response.json() data = response.json()
if data.get("code") != 0: if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}") raise Exception(f"创建图片块失败: {data}")
return data["data"]["children"][0]["block_id"] return data["data"]["children"][0]["block_id"]
def upload_image(file_path: str, block_id: str): def upload_image(file_path: str, block_id: str):
"""上传图片""" """上传图片"""
url = f"{BASE_URL}/drive/v1/medias/upload_all" url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path) file_size = os.path.getsize(file_path)
# 根据扩展名设置 MIME 类型 # 根据扩展名设置 MIME 类型
ext = file_name.lower().split('.')[-1] ext = file_name.lower().split('.')[-1]
mime_types = { mime_types = {
'png': 'image/png', 'png': 'image/png',
'jpg': 'image/jpeg', 'jpg': 'image/jpeg',
'jpeg': 'image/jpeg', 'jpeg': 'image/jpeg',
'gif': 'image/gif', 'gif': 'image/gif',
'webp': 'image/webp' 'webp': 'image/webp'
} }
mime_type = mime_types.get(ext, 'image/png') mime_type = mime_types.get(ext, 'image/png')
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
files = {'file': (file_name, f, mime_type)} files = {'file': (file_name, f, mime_type)}
data = { data = {
'file_name': file_name, 'file_name': file_name,
'parent_type': 'docx_image', 'parent_type': 'docx_image',
'parent_node': block_id, 'parent_node': block_id,
'size': str(file_size) 'size': str(file_size)
} }
response = requests.post( response = requests.post(
url, url,
headers={"Authorization": f"Bearer {get_token()}"}, headers={"Authorization": f"Bearer {get_token()}"},
files=files, files=files,
data=data data=data
) )
result = response.json() result = response.json()
if result.get("code") != 0: if result.get("code") != 0:
raise Exception(f"上传失败: {result}") raise Exception(f"上传失败: {result}")
return result["data"]["file_token"] return result["data"]["file_token"]
def bind_image(document_id: str, block_id: str, file_token: str): def bind_image(document_id: str, block_id: str, file_token: str):
"""绑定图片到图片块""" """绑定图片到图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}" url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
payload = { payload = {
"replace_image": { "replace_image": {
"token": file_token "token": file_token
} }
} }
response = requests.patch( response = requests.patch(
url, url,
headers=headers(), headers=headers(),
params={"document_revision_id": -1}, params={"document_revision_id": -1},
json=payload json=payload
) )
data = response.json() data = response.json()
if data.get("code") != 0: if data.get("code") != 0:
raise Exception(f"绑定失败: {data}") raise Exception(f"绑定失败: {data}")
return True return True
def main(): def main():
print(f"\n上传图片: {IMAGE_PATH}") print(f"\n上传图片: {IMAGE_PATH}")
print("=" * 60) print("=" * 60)
# 检查文件 # 检查文件
if not os.path.exists(IMAGE_PATH): if not os.path.exists(IMAGE_PATH):
print(f"[ERROR] 文件不存在: {IMAGE_PATH}") print(f"[ERROR] 文件不存在: {IMAGE_PATH}")
return return
file_size = os.path.getsize(IMAGE_PATH) file_size = os.path.getsize(IMAGE_PATH)
print(f"文件大小: {file_size / 1024:.1f} KB") print(f"文件大小: {file_size / 1024:.1f} KB")
# Step 1: 创建文档 # Step 1: 创建文档
print("\n[1/4] 创建飞书文档...") print("\n[1/4] 创建飞书文档...")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
doc_id = create_document(f"火把图片 - {timestamp}") doc_id = create_document(f"火把图片 - {timestamp}")
print(f" 文档ID: {doc_id}") print(f" 文档ID: {doc_id}")
# Step 2: 创建图片块 # Step 2: 创建图片块
print("[2/4] 创建图片块...") print("[2/4] 创建图片块...")
block_id = create_image_block(doc_id) block_id = create_image_block(doc_id)
print(f" 块ID: {block_id}") print(f" 块ID: {block_id}")
# Step 3: 上传图片 # Step 3: 上传图片
print("[3/4] 上传图片...") print("[3/4] 上传图片...")
file_token = upload_image(IMAGE_PATH, block_id) file_token = upload_image(IMAGE_PATH, block_id)
print(f" file_token: {file_token}") print(f" file_token: {file_token}")
# Step 4: 绑定图片 # Step 4: 绑定图片
print("[4/4] 绑定图片...") print("[4/4] 绑定图片...")
bind_image(doc_id, block_id, file_token) bind_image(doc_id, block_id, file_token)
print(" 绑定成功!") print(" 绑定成功!")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("上传完成!") print("上传完成!")
print(f"\n文档地址: https://feishu.cn/docx/{doc_id}") print(f"\n文档地址: https://feishu.cn/docx/{doc_id}")
print("=" * 60) print("=" * 60)
if __name__ == "__main__": if __name__ == "__main__":
main() main()