chore(skill): 安全扫描警告清零 + 加入 skill-gate CI #14
@@ -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
|
||||||
@@ -30,3 +30,11 @@
|
|||||||
- 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` + 安装器测试。
|
||||||
|
|||||||
+5
-5
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|||||||
@@ -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
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 实现任务批量删除功能'"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user