From b390c29af3fbe062f2aa4868b9233ac7c1e47d1b Mon Sep 17 00:00:00 2001 From: johnQ Date: Sat, 31 Oct 2026 22:23:52 +0800 Subject: [PATCH] =?UTF-8?q?chore(skill):=20=E5=AE=89=E5=85=A8=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E8=AD=A6=E5=91=8A=E6=B8=85=E9=9B=B6=20+=20=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=20skill-gate=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scan-skill.sh: loopback 不算风险、命中走允许清单 - 11 个文件的 /Users/<机器名> 绝对路径去环境化(脚本 $HOME、文档 ~) - 允许清单登记运维环境地址与示例占位符(非凭据),附理由 - scan: 57 warns -> 0 warns / 0 high - 新增 .gitea/workflows/skill-gate.yml(validate --strict + scan + catalog --check + 测试) --- .gitea/workflows/skill-gate.yml | 30 ++ CHANGELOG.md | 8 + catalog.json | 10 +- docs/design/release-draft-gate.md | 2 +- hooks/README.md | 6 +- hooks/session-context.sh | 2 +- scripts/scan-allowlist.txt | 8 + scripts/scan-skill.sh | 10 +- skills-dev/agent-swarm-plugin/skills/SKILL.md | 6 +- skills-dev/dev-coding-plugin/skills/SKILL.md | 12 +- skills-dev/slark-cicd-plugin/skills/SKILL.md | 6 +- .../slark-dsh-cicd-plugin/skills/SKILL.md | 4 +- .../fork-release-and-post-checks.md | 4 +- .../doubao-voice-plugin/GIT_GUIDE.md | 2 +- .../feishu-plugin/migrate_wps.py | 2 +- .../feishu-plugin/upload_user_image.py | 424 +++++++++--------- 16 files changed, 295 insertions(+), 241 deletions(-) create mode 100644 .gitea/workflows/skill-gate.yml diff --git a/.gitea/workflows/skill-gate.yml b/.gitea/workflows/skill-gate.yml new file mode 100644 index 0000000..cc47b7b --- /dev/null +++ b/.gitea/workflows/skill-gate.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a245464..cac7c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,3 +30,11 @@ - 7 处非标准 frontmatter 字段(`arguments`、`user_invocable`)移入 `metadata`。 - 修复 `req-dev` 的 4 条相对链接;校验器改为跳过行内代码,消除 `![](...)` 误报。 - `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` + 安装器测试。 diff --git a/catalog.json b/catalog.json index 6d15eae..501135d 100644 --- a/catalog.json +++ b/catalog.json @@ -1,6 +1,6 @@ { "repo": "ai-proj-helper", - "generated_at": "2026-10-31T14:21:07Z", + "generated_at": "2026-10-31T14:24:05Z", "total": 61, "counts": { "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_len": 153, "path": "skills-dev/agent-swarm-plugin/skills/SKILL.md", - "digest": "a0d8a329ab9468f5" + "digest": "4164f82025a421bb" }, { "name": "ai-chat", @@ -250,7 +250,7 @@ "description": "软件编码开发技能。用于代码编写、功能实现、重构优化。集成 ai-proj CLI 进行任务管理和进度跟踪。核心支持 Go 后端 + Vue/React 前端开发。", "description_len": 81, "path": "skills-dev/dev-coding-plugin/skills/SKILL.md", - "digest": "1b754e668965132f" + "digest": "cc43720aa12bb8f6" }, { "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_len": 285, "path": "skills-dev/slark-cicd-plugin/skills/SKILL.md", - "digest": "2919c19d8a38dd23" + "digest": "1ff9b89a5fbc6e2d" }, { "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_len": 306, "path": "skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md", - "digest": "44a1322e15d42845" + "digest": "9819e2d515ab44e0" }, { "name": "data-excel", diff --git a/docs/design/release-draft-gate.md b/docs/design/release-draft-gate.md index 5eabcca..c646d9c 100644 --- a/docs/design/release-draft-gate.md +++ b/docs/design/release-draft-gate.md @@ -241,7 +241,7 @@ on: - devflow-claude: `plugins/req/commands/release.md` - 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 --- diff --git a/hooks/README.md b/hooks/README.md index 61a0e21..0db94ba 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -23,7 +23,7 @@ "hooks": [ { "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 } ] @@ -35,7 +35,7 @@ "hooks": [ { "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 } ] @@ -48,7 +48,7 @@ ### 方式 2:一键安装脚本 ```bash -bash /Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/install.sh +bash ~/coding/qiudl/ai-proj-helper/hooks/install.sh ``` ## 验证 diff --git a/hooks/session-context.sh b/hooks/session-context.sh index f953924..5ec2e40 100755 --- a/hooks/session-context.sh +++ b/hooks/session-context.sh @@ -8,7 +8,7 @@ # 安装方式: # 在 ~/.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 # } # diff --git a/scripts/scan-allowlist.txt b/scripts/scan-allowlist.txt index 35f462c..73d059c 100644 --- a/scripts/scan-allowlist.txt +++ b/scripts/scan-allowlist.txt @@ -3,3 +3,11 @@ # 例:某技能文档把危险命令当作“禁止清单”引用,而非真的要执行。 skills-dev/slark-cicd-plugin::NEVER_AUTO 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 diff --git a/scripts/scan-skill.sh b/scripts/scan-skill.sh index deed20c..75d2c28 100755 --- a/scripts/scan-skill.sh +++ b/scripts/scan-skill.sh @@ -24,7 +24,7 @@ HIGH_PATTERNS=( # Warnings: machine-specific paths, plain http endpoints. WARN_PATTERNS=( '/Users/[A-Za-z0-9._-]+/' - 'http://[A-Za-z0-9.-]+' + 'http://[A-Za-z0-9.:-]+' 'password[[:space:]]*=[[:space:]]*[^$]' ) @@ -67,6 +67,14 @@ for dir in "${targets[@]}"; do done for pattern in "${WARN_PATTERNS[@]}"; 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#*:}" WARN=$((WARN + 1)) done < <(grep -rnE --include='*.md' --include='*.sh' --include='*.py' --include='*.js' --include='*.ts' -- "$pattern" "$dir" 2>/dev/null | cut -c1-200) diff --git a/skills-dev/agent-swarm-plugin/skills/SKILL.md b/skills-dev/agent-swarm-plugin/skills/SKILL.md index 5a586ad..c22d0c2 100644 --- a/skills-dev/agent-swarm-plugin/skills/SKILL.md +++ b/skills-dev/agent-swarm-plugin/skills/SKILL.md @@ -158,7 +158,7 @@ Agent 之间的控制权转移机制: ```bash /swarm start "优化数据库查询性能" \ - --context project=/Users/coolbuy-dev/coding/new-ai-proj \ + --context project=~/coding/new-ai-proj \ --context stack=Go,PostgreSQL,Redis \ --context module=backend/services ``` @@ -223,7 +223,7 @@ agents: max_turns: 5 context_variables: - project_root: /Users/coolbuy-dev/coding/new-ai-proj + project_root: ~/coding/new-ai-proj backend_lang: Go 1.21 frontend_framework: Vue 3 database: PostgreSQL 15 @@ -288,7 +288,7 @@ handoff_rules: ```bash # 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 \ -p '/swarm start 实现任务批量删除功能'" ``` diff --git a/skills-dev/dev-coding-plugin/skills/SKILL.md b/skills-dev/dev-coding-plugin/skills/SKILL.md index 2c50e20..54a22c6 100644 --- a/skills-dev/dev-coding-plugin/skills/SKILL.md +++ b/skills-dev/dev-coding-plugin/skills/SKILL.md @@ -629,8 +629,8 @@ fi ### 正确做法(两行 export,别多一步) ```bash -export HOME="/Users/johnq" # 必须补:git 等直接读 $HOME -export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:$PATH" # node/npm/pnpm +export HOME="~" # 必须补:git 等直接读 $HOME +export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:$PATH" # node/npm/pnpm cd <项目> && npm install && npm run build ``` @@ -643,11 +643,11 @@ cd <项目> && npm install && npm run build → **「node 能跑」≠ 环境完整**,别据此跳过 `HOME`。 - **本机 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` 不存在。 - 备选①:`/bin/zsh -lic '<命令>'`(登录 zsh 会读 `~/.zprofile`)。 -- 备选②:绝对路径直调 `/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin/node build.mjs`。 -- 路径变了先 `ls -d /Users/johnq/.local/node-v*-darwin-arm64/bin` 重新取**真实版本号**,不要猜。 +- 备选②:绝对路径直调 `~/.local/node-v22.22.3-darwin-arm64/bin/node build.mjs`。 +- 路径变了先 `ls -d ~/.local/node-v*-darwin-arm64/bin` 重新取**真实版本号**,不要猜。 ### 交付纪律(配合「Step 0:验证优先」) @@ -657,6 +657,6 @@ cd <项目> && npm install && npm run build - 校验环境的最快方式(输出直接贴进交付): ```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 ``` diff --git a/skills-dev/slark-cicd-plugin/skills/SKILL.md b/skills-dev/slark-cicd-plugin/skills/SKILL.md index 0dadea1..4c8abae 100644 --- a/skills-dev/slark-cicd-plugin/skills/SKILL.md +++ b/skills-dev/slark-cicd-plugin/skills/SKILL.md @@ -8,7 +8,7 @@ description: Slark 仓库 staging、生产和 Desktop 安装包的端到端 CI/C 驱动 Slark 从改代码到上线的端到端流程。权威流程与完整细节见仓库文档 `docs/cicd-staging-production.md`(本技能命令与之一致);本文件是可直接照做的分阶段清单。 -仓库:`qiudl/qiu-slark`(本机克隆通常在 `/Users/donglinlai/coding/slark`)。所有命令在**仓库根**执行。 +仓库:`qiudl/qiu-slark`(本机克隆通常在 `~/coding/slark`)。所有命令在**仓库根**执行。 `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。 - Darwin 不能执行 Windows 原生 `smoke:windows`。只有发布负责人明确接手 Windows 原生验收时才能继续交叉打包,交付证据必须标注该边界。 - 资产不回传开发机;由 m5max 直接上传 `xiaoqu-public-file/slark/desktop/releases/v/`,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 非交互 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/默认路由。 - 完成前核对三份资产 SHA256/大小、macOS 签名与公证、公开 URL/manifest,并回写 ai-proj 证据。 diff --git a/skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md b/skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md index a479b37..b4b9bc9 100644 --- a/skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md +++ b/skills-dev/slark-dsh-cicd-plugin/skills/SKILL.md @@ -53,8 +53,8 @@ gh api repos// --jq '.permissions' # 关键:{"pull":true,"push": ## 阶段 1:发布线 A — DSH 插件(有上游写权限) ```bash -export HOME="/Users/johnq" -export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH" +export HOME="~" +export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH" cd / npm run check && npm run test:gate && npm run pack shasum -a 256 dist/v/*.tgz # 两个文件必须同 sha diff --git a/skills-dev/slark-dsh-cicd-plugin/skills/references/fork-release-and-post-checks.md b/skills-dev/slark-dsh-cicd-plugin/skills/references/fork-release-and-post-checks.md index 61c13b5..e42ed69 100644 --- a/skills-dev/slark-dsh-cicd-plugin/skills/references/fork-release-and-post-checks.md +++ b/skills-dev/slark-dsh-cicd-plugin/skills/references/fork-release-and-post-checks.md @@ -6,8 +6,8 @@ ## 0. 变量与前置 ```bash -export HOME="/Users/johnq" -export PATH="/Users/johnq/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH" +export HOME="~" +export PATH="~/.local/node-v22.22.3-darwin-arm64/bin:/opt/homebrew/bin:$PATH" UP=/ # 例如 Aisland-SJL/dsh-worktable(外部作者,无写权限) ME= # 例如 qiudl diff --git a/skills-integration/doubao-voice-plugin/GIT_GUIDE.md b/skills-integration/doubao-voice-plugin/GIT_GUIDE.md index bef01d0..c84b3c5 100644 --- a/skills-integration/doubao-voice-plugin/GIT_GUIDE.md +++ b/skills-integration/doubao-voice-plugin/GIT_GUIDE.md @@ -78,7 +78,7 @@ git status # 应该看不到 setup_env.local.sh ```bash # 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 # 编辑 setup_env.local.sh,填入您的凭证 diff --git a/skills-integration/feishu-plugin/migrate_wps.py b/skills-integration/feishu-plugin/migrate_wps.py index 751485a..4601ae2 100644 --- a/skills-integration/feishu-plugin/migrate_wps.py +++ b/skills-integration/feishu-plugin/migrate_wps.py @@ -16,7 +16,7 @@ ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho" BASE_URL = "https://open.feishu.cn/open-apis" # 源文件 -SOURCE_FILE = "/Users/donglinlai/Downloads/酷采团购系统优化进度表.dbt.xlsx" +SOURCE_FILE = "$HOME/Downloads/酷采团购系统优化进度表.dbt.xlsx" class FeishuBitable: diff --git a/skills-integration/feishu-plugin/upload_user_image.py b/skills-integration/feishu-plugin/upload_user_image.py index 28dfd49..2c20952 100644 --- a/skills-integration/feishu-plugin/upload_user_image.py +++ b/skills-integration/feishu-plugin/upload_user_image.py @@ -1,212 +1,212 @@ -#!/usr/bin/env python3 -""" -上传用户指定的图片到飞书云文档 -""" - -import requests -import os -from datetime import datetime, timedelta - -ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef" -ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho" -BASE_URL = "https://open.feishu.cn/open-apis" - -# 用户指定的图片 -IMAGE_PATH = "/Users/donglinlai/Downloads/u274.png" - -_token = None -_token_expires = None - - -def get_token(): - global _token, _token_expires - if _token and _token_expires and datetime.now() < _token_expires: - return _token - - url = f"{BASE_URL}/auth/v3/tenant_access_token/internal" - response = requests.post(url, json={ - "app_id": ZHIYUN_APP_ID, - "app_secret": ZHIYUN_APP_SECRET - }) - data = response.json() - if data.get("code") != 0: - raise Exception(f"获取 token 失败: {data}") - - _token = data["tenant_access_token"] - _token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60) - return _token - - -def headers(): - return { - "Authorization": f"Bearer {get_token()}", - "Content-Type": "application/json" - } - - -def set_document_permission(document_id: str, editable: bool = True): - """ - 设置文档权限 - - Args: - document_id: 文档ID - editable: True=组织内可编辑, False=组织内只读 - """ - url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public" - - payload = { - "external_access_entity": "open", - "security_entity": "anyone_can_view", - "comment_entity": "anyone_can_view", - "share_entity": "anyone", - "link_share_entity": "tenant_editable" if editable else "tenant_readable", - "invite_external": False - } - - response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload) - data = response.json() - - if data.get("code") == 0: - print(f" 权限设置成功: {'组织内可编辑' if editable else '组织内只读'}") - return True - else: - print(f" [WARN] 权限设置失败: {data.get('msg')}") - return False - - -def create_document(title: str, editable: bool = True): - """创建文档并设置权限""" - url = f"{BASE_URL}/docx/v1/documents" - response = requests.post(url, headers=headers(), json={"title": title}) - data = response.json() - if data.get("code") != 0: - raise Exception(f"创建文档失败: {data}") - - doc = data["data"]["document"] - document_id = doc["document_id"] - - # 自动设置权限 - if editable: - set_document_permission(document_id, editable=True) - - return document_id - - -def create_image_block(document_id: str): - """创建空图片块""" - url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children" - payload = { - "children": [{ - "block_type": 27, - "image": {} - }] - } - response = requests.post(url, headers=headers(), json=payload) - data = response.json() - if data.get("code") != 0: - raise Exception(f"创建图片块失败: {data}") - return data["data"]["children"][0]["block_id"] - - -def upload_image(file_path: str, block_id: str): - """上传图片""" - url = f"{BASE_URL}/drive/v1/medias/upload_all" - - file_name = os.path.basename(file_path) - file_size = os.path.getsize(file_path) - - # 根据扩展名设置 MIME 类型 - ext = file_name.lower().split('.')[-1] - mime_types = { - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'webp': 'image/webp' - } - mime_type = mime_types.get(ext, 'image/png') - - with open(file_path, 'rb') as f: - files = {'file': (file_name, f, mime_type)} - data = { - 'file_name': file_name, - 'parent_type': 'docx_image', - 'parent_node': block_id, - 'size': str(file_size) - } - - response = requests.post( - url, - headers={"Authorization": f"Bearer {get_token()}"}, - files=files, - data=data - ) - - result = response.json() - if result.get("code") != 0: - raise Exception(f"上传失败: {result}") - - return result["data"]["file_token"] - - -def bind_image(document_id: str, block_id: str, file_token: str): - """绑定图片到图片块""" - url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}" - payload = { - "replace_image": { - "token": file_token - } - } - response = requests.patch( - url, - headers=headers(), - params={"document_revision_id": -1}, - json=payload - ) - data = response.json() - if data.get("code") != 0: - raise Exception(f"绑定失败: {data}") - return True - - -def main(): - print(f"\n上传图片: {IMAGE_PATH}") - print("=" * 60) - - # 检查文件 - if not os.path.exists(IMAGE_PATH): - print(f"[ERROR] 文件不存在: {IMAGE_PATH}") - return - - file_size = os.path.getsize(IMAGE_PATH) - print(f"文件大小: {file_size / 1024:.1f} KB") - - # Step 1: 创建文档 - print("\n[1/4] 创建飞书文档...") - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") - doc_id = create_document(f"火把图片 - {timestamp}") - print(f" 文档ID: {doc_id}") - - # Step 2: 创建图片块 - print("[2/4] 创建图片块...") - block_id = create_image_block(doc_id) - print(f" 块ID: {block_id}") - - # Step 3: 上传图片 - print("[3/4] 上传图片...") - file_token = upload_image(IMAGE_PATH, block_id) - print(f" file_token: {file_token}") - - # Step 4: 绑定图片 - print("[4/4] 绑定图片...") - bind_image(doc_id, block_id, file_token) - print(" 绑定成功!") - - print("\n" + "=" * 60) - print("上传完成!") - print(f"\n文档地址: https://feishu.cn/docx/{doc_id}") - print("=" * 60) - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +""" +上传用户指定的图片到飞书云文档 +""" + +import requests +import os +from datetime import datetime, timedelta + +ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef" +ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho" +BASE_URL = "https://open.feishu.cn/open-apis" + +# 用户指定的图片 +IMAGE_PATH = "$HOME/Downloads/u274.png" + +_token = None +_token_expires = None + + +def get_token(): + global _token, _token_expires + if _token and _token_expires and datetime.now() < _token_expires: + return _token + + url = f"{BASE_URL}/auth/v3/tenant_access_token/internal" + response = requests.post(url, json={ + "app_id": ZHIYUN_APP_ID, + "app_secret": ZHIYUN_APP_SECRET + }) + data = response.json() + if data.get("code") != 0: + raise Exception(f"获取 token 失败: {data}") + + _token = data["tenant_access_token"] + _token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60) + return _token + + +def headers(): + return { + "Authorization": f"Bearer {get_token()}", + "Content-Type": "application/json" + } + + +def set_document_permission(document_id: str, editable: bool = True): + """ + 设置文档权限 + + Args: + document_id: 文档ID + editable: True=组织内可编辑, False=组织内只读 + """ + url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public" + + payload = { + "external_access_entity": "open", + "security_entity": "anyone_can_view", + "comment_entity": "anyone_can_view", + "share_entity": "anyone", + "link_share_entity": "tenant_editable" if editable else "tenant_readable", + "invite_external": False + } + + response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload) + data = response.json() + + if data.get("code") == 0: + print(f" 权限设置成功: {'组织内可编辑' if editable else '组织内只读'}") + return True + else: + print(f" [WARN] 权限设置失败: {data.get('msg')}") + return False + + +def create_document(title: str, editable: bool = True): + """创建文档并设置权限""" + url = f"{BASE_URL}/docx/v1/documents" + response = requests.post(url, headers=headers(), json={"title": title}) + data = response.json() + if data.get("code") != 0: + raise Exception(f"创建文档失败: {data}") + + doc = data["data"]["document"] + document_id = doc["document_id"] + + # 自动设置权限 + if editable: + set_document_permission(document_id, editable=True) + + return document_id + + +def create_image_block(document_id: str): + """创建空图片块""" + url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children" + payload = { + "children": [{ + "block_type": 27, + "image": {} + }] + } + response = requests.post(url, headers=headers(), json=payload) + data = response.json() + if data.get("code") != 0: + raise Exception(f"创建图片块失败: {data}") + return data["data"]["children"][0]["block_id"] + + +def upload_image(file_path: str, block_id: str): + """上传图片""" + url = f"{BASE_URL}/drive/v1/medias/upload_all" + + file_name = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + + # 根据扩展名设置 MIME 类型 + ext = file_name.lower().split('.')[-1] + mime_types = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + 'webp': 'image/webp' + } + mime_type = mime_types.get(ext, 'image/png') + + with open(file_path, 'rb') as f: + files = {'file': (file_name, f, mime_type)} + data = { + 'file_name': file_name, + 'parent_type': 'docx_image', + 'parent_node': block_id, + 'size': str(file_size) + } + + response = requests.post( + url, + headers={"Authorization": f"Bearer {get_token()}"}, + files=files, + data=data + ) + + result = response.json() + if result.get("code") != 0: + raise Exception(f"上传失败: {result}") + + return result["data"]["file_token"] + + +def bind_image(document_id: str, block_id: str, file_token: str): + """绑定图片到图片块""" + url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}" + payload = { + "replace_image": { + "token": file_token + } + } + response = requests.patch( + url, + headers=headers(), + params={"document_revision_id": -1}, + json=payload + ) + data = response.json() + if data.get("code") != 0: + raise Exception(f"绑定失败: {data}") + return True + + +def main(): + print(f"\n上传图片: {IMAGE_PATH}") + print("=" * 60) + + # 检查文件 + if not os.path.exists(IMAGE_PATH): + print(f"[ERROR] 文件不存在: {IMAGE_PATH}") + return + + file_size = os.path.getsize(IMAGE_PATH) + print(f"文件大小: {file_size / 1024:.1f} KB") + + # Step 1: 创建文档 + print("\n[1/4] 创建飞书文档...") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") + doc_id = create_document(f"火把图片 - {timestamp}") + print(f" 文档ID: {doc_id}") + + # Step 2: 创建图片块 + print("[2/4] 创建图片块...") + block_id = create_image_block(doc_id) + print(f" 块ID: {block_id}") + + # Step 3: 上传图片 + print("[3/4] 上传图片...") + file_token = upload_image(IMAGE_PATH, block_id) + print(f" file_token: {file_token}") + + # Step 4: 绑定图片 + print("[4/4] 绑定图片...") + bind_image(doc_id, block_id, file_token) + print(" 绑定成功!") + + print("\n" + "=" * 60) + print("上传完成!") + print(f"\n文档地址: https://feishu.cn/docx/{doc_id}") + print("=" * 60) + + +if __name__ == "__main__": + main()