chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:38 +08:00
commit 2417d9556f
117 changed files with 13432 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in the repository](https://github.com/changesets/changesets).
## Usage
When you make a change that needs to be released:
```bash
pnpm changeset
```
This will prompt you to:
1. Select which packages have changed
2. Choose a semver bump type (major/minor/patch)
3. Provide a summary of the changes
A changeset markdown file will be created in this directory.
When it's time to release:
```bash
pnpm changeset version # Apply version bumps and update changelogs
pnpm changeset publish # Publish to npm
```
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
"@wecom/cli",
"@wecom/cli-darwin-arm64",
"@wecom/cli-darwin-x64",
"@wecom/cli-linux-x64",
"@wecom/cli-linux-arm64",
"@wecom/cli-win32-x64"
]
],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+22
View File
@@ -0,0 +1,22 @@
# EditorConfig - https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[Cargo.toml]
indent_size = 4
[Cargo.lock]
indent_size = 4
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
npx --no -- commitlint --edit "$1"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# Run clippy and fmt check if any Rust files are staged
if git diff --cached --name-only --diff-filter=d | grep -q '\.rs$'; then
cargo fmt --check || exit 1
cargo clippy --all-targets -- -D warnings || exit 1
fi
# Run eslint on staged JS/TS files
JS_FILES=$(git diff --cached --name-only --diff-filter=d | grep -E '\.(js|ts)$')
if [ -n "$JS_FILES" ]; then
npx eslint $JS_FILES || exit 1
fi
+29
View File
@@ -0,0 +1,29 @@
# Rust
/target
# Node
node_modules
# Env
.env*
# IDE
.codebuddy
.idea/
.vscode/
# OS
.DS_Store
Thumbs.db
# Logs & temp
*.log
*.swp
*.swo
*~
# Platform binary directories (populated during build)
packages/darwin-arm64/bin/
packages/darwin-x64/bin/
packages/linux-x64/bin/
packages/win32-x64/bin/
+42
View File
@@ -0,0 +1,42 @@
# AI Agent 指引
> 本文件帮助 AI Agent 快速定位项目关键文件,避免阅读无关代码。
## 项目概述
`wecom-cli` 是一个企业微信 CLI 工具(Rust),通过 JSON-RPC 调用远程 MCP 服务。CLI 结构为:
```
wecom <category> <method> # 远程工具调用
wecom <category> +<helper_name> # 本地 helper+ 前缀)
```
## 任务路由
根据任务类型,阅读对应指引文件即可,**不需要阅读其他文件**:
| 任务 | 指引文件 | 你需要修改的文件 |
|------|----------|-----------------|
| 新建/维护现有 helper | [`src/helpers/AGENTS.md`](src/helpers/AGENTS.md) | 对应 helper 的 `.rs` 文件 |
| 理解人类的需求格式 | [`src/helpers/HUMANS.md`](src/helpers/HUMANS.md) | — |
## 项目结构速查
```
src/
├── main.rs # CLI 入口,构建 clap Command
├── json_rpc.rs # 远程调用:call_tool(category, method, args)
├── fs_util/ # 文件系统工具(atomic_write, sanitize_filename
├── service/
│ └── handler.rs # 调度逻辑:helper 优先 → fallback 远程调用
└── helpers/ # ⭐ Helper 子系统(详见 helpers/AGENTS.md
├── mod.rs # 模块声明
├── registry.rs # Helper trait + HelperRegistry
├── AGENTS.md # AI 实现指南(核心文档)
└── <category>/ # 按 category 分目录存放 helper
```
## 注意事项
- **不要修改 `main.rs``service/` 下的文件**helper 系统通过 registry 自动注册,无需改动调度层
- 用户的需求描述格式参考 [`src/helpers/HUMANS.md`](src/helpers/HUMANS.md),除非用户直接声明,否则你无需阅读这篇文档
+19
View File
@@ -0,0 +1,19 @@
# @wecom/cli
## 0.1.9
### Patch Changes
- 3700b1b: update cmds
## 0.1.8
### Patch Changes
- 7774ba3: update init process
## 0.1.7
### Patch Changes
- 83a7495: add smartsheet auto file upload helpers
Generated
+3085
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "wecom-cli"
version = "0.1.9"
edition = "2024"
description = "The official CLI for WeCom — 企业微信命令行工具,让人类和 AI Agent 都能在终端中操作企业微信"
license = "MIT"
repository = "https://github.com/WecomTeam/wecom-cli"
homepage = "https://github.com/WecomTeam/wecom-cli"
readme = "README.md"
keywords = ["wecom", "cli", "mcp", "wechat-work"]
categories = ["command-line-utilities"]
include = ["/src/**/*", "/build.rs", "/Cargo.toml", "/Cargo.lock", "/LICENSE", "/README.md"]
[features]
custom-endpoint = []
[[bin]]
name = "wecom-cli"
path = "src/main.rs"
[dependencies]
tokio = { version = "1.50.0", features = ["full"] }
anyhow = "1"
clap = { version = "4", features = ["derive", "string"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_repr = "0.1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender = "0.2"
reqwest = { version = "0.13.2", default-features = false, features = ["json", "multipart", "stream", "rustls"] }
dotenvy = "0.15.7"
dirs = "6.0.0"
base64 = "0.22"
mime = "0.3"
mime_guess = "2"
infer = "0.19"
rand = "0.9"
hex = "0.4"
sha2 = "0.10"
aes-gcm = "0.10"
keyring = { version = "3", features = ["apple-native", "linux-native"] }
sanitize-filename = "0.6.0"
tempfile = "3.27.0"
cliclack = "0.5"
qr2term = "0.3"
qrcode = "0.14"
open = "5"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 WeCom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+66
View File
@@ -0,0 +1,66 @@
# wecom-cli
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust](https://img.shields.io/badge/rust-%3E%3D1.75-orange.svg)](https://www.rust-lang.org/)
> 💬 扫码加入企业微信交流群:
>
> <img src="https://wwcdn.weixin.qq.com/node/wework/images/202603241759.3fb01c32cc.png" alt="扫码入群交流" width="200" />
企业微信命令行工具 — 让人类和 AI Agent 都能在终端中操作企业微信。
## 功能范围
覆盖企业微信核心业务品类:
| 品类 | 能力 |
| ----------- | ----------------------------------------------------------------------------- |
| 📄 文档 | 文档创建、读取、编辑等;智能文档创建、读取 |
| 📊 智能表格 | 智能表格创建、子表与字段管理、记录增删改查等 |
| 💬 消息 | 会话列表查询、消息记录拉取(文本/图片/文件/语音/视频)、多媒体下载、发送文本等 |
| 👤 通讯录 | 获取可见范围成员列表、按姓名/别名搜索等 |
| ✅ 待办 | 创建/读取/更新/删除待办,变更用户处理状态等 |
| 🎥 会议 | 创建预约会议、取消会议、更新受邀成员、查询列表与详情等 |
| 📅 日程 | 日程增删改查、参与人管理、多成员闲忙查询等 |
**企业场景**
对于 10 人以上规模的企业,企业微信为 API 模式智能机器人提供了文档和待办CLI 能力,机器人可以创建及读取文档、创建并跟进待办,提高企业场景下的办公效率。
**个人/小团队场景**
对于10人及以下的个人/小团队,企业微信为API模式智能机器人提供了消息、文档、日程、会议、待办等CLI能力,以满足个人或小团队提效场景。
## 快速开始
### 前置条件
- 支持平台:macOS (x64/arm64)、Linux (x64/arm64) 及 Windows (x64)
- Node.js `>= 18`
- 企业微信账号
- (可选)智能机器人 Bot ID 和 Secret,获取方式参考 [说明](https://open.work.weixin.qq.com/help2/pc/cat?doc_id=21677)
### 安装 & 使用
```bash
# 安装 CLI
npm install -g @wecom/cli
# 安装 CLI Skill(必需)
npx skills add WeComTeam/wecom-cli -y -g
# 配置凭证(交互式,仅需一次)
wecom-cli init
# 获取通讯录可见范围内的成员列表
wecom-cli contact get_userlist '{}'
```
📖 更多使用方法,请参阅 [CLI 命令参考](docs/cli-reference.md)。
## Agent Skills
🤖 支持的 Skills 使用说明,请参阅 [Skills 文档](docs/skills.md)。
## 许可证
本项目基于 [MIT 许可证](./LICENSE) 开源。
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`WecomTeam/wecom-cli`
- 原始仓库:https://github.com/WecomTeam/wecom-cli
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import os from 'node:os';
import { join } from 'node:path';
const require = createRequire(import.meta.url);
/**
* Returns the platform-specific package name based on current OS and CPU architecture.
*/
function getPlatformPackage() {
const platform = os.platform();
const arch = os.arch();
const platformMap = {
'darwin-arm64': '@wecom/cli-darwin-arm64',
'darwin-x64': '@wecom/cli-darwin-x64',
'linux-arm64': '@wecom/cli-linux-arm64',
'linux-x64': '@wecom/cli-linux-x64',
'win32-x64': '@wecom/cli-win32-x64',
};
const key = `${platform}-${arch}`;
const pkg = platformMap[key];
if (!pkg) {
console.error(
`Error: unsupported platform ${platform}-${arch}.\n` +
`Supported platforms: ${Object.keys(platformMap).join(', ')}`,
);
process.exit(1);
}
return pkg;
}
/**
* Resolves the path to the platform-specific binary.
*/
function getBinaryPath() {
const pkg = getPlatformPackage();
const binaryName = os.platform() === 'win32' ? 'wecom-cli.exe' : 'wecom-cli';
try {
const pkgDir = require.resolve(`${pkg}/package.json`);
return join(pkgDir, '..', 'bin', binaryName);
} catch {
console.error(
`Error: cannot find @wecom/cli binary.\n` +
`Please try reinstalling: npm install @wecom/cli\n\n` +
`If the problem persists, check:\n` +
` 1. Your npm config does not disable optional dependencies (--no-optional)\n` +
` 2. Your platform (${os.platform()}-${os.arch()}) is supported`,
);
process.exit(1);
}
}
// Execute the binary, passing through all arguments
const binaryPath = getBinaryPath();
try {
execFileSync(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
env: process.env,
});
} catch (error) {
if (error.status != null) {
process.exit(error.status);
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::path::Path;
use std::process::Command;
fn main() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let git_dir = Path::new(&manifest_dir).join(".git");
// Set up git hooks
if git_dir.exists() {
let status = Command::new("git")
.args(["config", "core.hooksPath", ".githooks"])
.current_dir(&manifest_dir)
.status();
match status {
Ok(s) if !s.success() => {
println!(
"cargo:warning=⚠️ Failed to set core.hooksPath. Run `git config core.hooksPath .githooks` manually."
);
}
Err(e) => {
println!(
"cargo:warning=⚠️ Failed to run git: {e}. Run `git config core.hooksPath .githooks` manually."
);
}
_ => {}
}
}
println!("cargo:rerun-if-changed=.githooks");
}
+1
View File
@@ -0,0 +1 @@
msrv = "1.85.0"
+32
View File
@@ -0,0 +1,32 @@
export default {
extends: ['@commitlint/config-conventional'],
rules: {
// type 必须是以下之一
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复 bug
'docs', // 文档变更
'style', // 代码格式(不影响功能)
'refactor', // 重构(不是新功能也不是修复)
'perf', // 性能优化
'test', // 测试
'build', // 构建系统或外部依赖
'ci', // CI 配置
'chore', // 其他杂项
'revert', // 回滚
'skills', // skills 更新
],
],
// type 不能为空
'type-empty': [2, 'never'],
// subject 不能为空
'subject-empty': [2, 'never'],
// subject 最大长度
'subject-max-length': [2, 'always', 100],
// header 最大长度
'header-max-length': [2, 'always', 120],
},
};
+14
View File
@@ -0,0 +1,14 @@
# 文档中心
本目录是 `wecom-cli` 的长期维护文档集,用来承载安装、使用约定、Skills 导航和开发说明。根 `README.md` 保持为项目首页,不再承担所有细节参考。
## 从这里开始
- 查看 CLI 使用方法、运行时路径和环境变量:[`docs/cli-reference.md`](cli-reference.md)
- 查看内置 Skills 的分工和入口:[`docs/skills.md`](skills.md)
- 本地开发、调试和仓库结构:[`docs/development.md`](development.md)
## 维护约定
- 优先把持续维护的说明写进 `docs/`
- 同一主题只保留一个主入口,其他页面通过链接复用。
+85
View File
@@ -0,0 +1,85 @@
## 使用说明
### 配置凭证 `init`
交互式配置企业微信机器人凭证,加密存储到本地。仅需执行一次。
- 若选择手动配置 Bot ID 和 Secret,获取方式[参考](https://open.work.weixin.qq.com/help2/pc/cat?doc_id=21677)
- 若选择扫码接入,需使用企业微信扫码创建绑定
```bash
wecom-cli init
```
### 查看帮助 `--help`
支持获取各级命令的使用方式
```bash
# 列出所有支持的命令和品类
wecom-cli --help
# 列出指定品类下的所有工具
wecom-cli <category> --help
# 列出指定工具的所需要的输入
wecom-cli <category> <method> --help
```
说明:
- 分类工具列表和工具 schema 都需要动态获取,因此“查看帮助”需要凭证与网络。
### 调用工具
通用格式:
```bash
wecom-cli <category> <method> [json_args]
```
其中 `category` 为业务品类标识,支持以下值:
| category | 品类 |
| ---------- | ------------- |
| `contact` | 通讯录 |
| `doc` | 文档/智能表格 |
| `meeting` | 会议 |
| `msg` | 消息 |
| `schedule` | 日程 |
| `todo` | 待办 |
工具调用行为:
- `wecom-cli <category>` 获取该品类下的支持调用工具
- `wecom-cli <category> <method> --help` 获取该指定工具的参数定义
- `wecom-cli <category> <method>` 执行调用工具并指定参数为'{}'
- `wecom-cli <category> <method> 'json_args'` 执行该工具调用
示例:
```bash
## 调用工具 — 获取通讯录可见范围内的成员列表
wecom-cli contact get_userlist '{}'
## 调用工具 — 创建文档
wecom-cli doc create_doc '{"doc_type": 3, "doc_name": "项目周报"}'
```
补充说明:
- 工具调用默认超时为 30 秒;`get_msg_media` 超时为 120 秒。
- `get_msg_media`会把媒体文件下载到本地临时目录,返回结果字段`local_path`为文件保存的路径 。
## 运行时路径
| 项目 | 默认位置 | 备注 |
| --- | --- | --- |
| 配置目录 | `~/.config/wecom` | 可由 `WECOM_CLI_CONFIG_DIR` 覆盖 |
| 机器人凭证 | `<config_dir>/bot.enc` | 配置凭证时创建 |
| MCP 配置缓存 | `<config_dir>/mcp_config.enc` | 配置凭证后更新 |
| 媒体临时目录 | `<system_tmp>/wecom/media` | 可由 `WECOM_CLI_TMP_DIR` 覆盖根目录 |
## 环境变量
| 变量 | 作用 |
| --- | --- |
| `WECOM_CLI_CONFIG_DIR` | 覆盖默认配置目录 |
| `WECOM_CLI_TMP_DIR` | 覆盖媒体临时目录的根目录 |
| `WECOM_CLI_LOG_LEVEL` | 打开 stderr 日志并设置过滤级别 |
| `WECOM_CLI_LOG_FILE` | 打开 JSON 日志输出,按天写入 `ww.log` |
| `WECOM_CLI_MCP_CONFIG_ENDPOINT` | 覆盖默认 MCP 配置接口地址 |
+24
View File
@@ -0,0 +1,24 @@
# 开发说明
这页面向仓库维护者和贡献者,记录源码结构、常用本地命令和打包边界。
## 仓库结构
| 路径 | 说明 |
| --- | --- |
| `src/` | Rust CLI 主实现,包括命令解析、认证、JSON-RPC、日志和媒体处理 |
| `bin/wecom.js` | npm 入口脚本,负责定位并执行当前平台的二进制 |
| `packages/*` | 各平台的 npm 二进制包 |
| `skills/*` | Agent Skills 及其补充参考资料 |
| `docs/` | 持续维护的使用与开发文档 |
| `README.md` | 项目首页 |
## 本地开发
仓库的 Rust crate 使用 `edition = "2024"`,开发时建议使用较新的 stable Rust 工具链。
说明:
- 根包名为 `@wecom/cli`,实际可执行入口是 `bin/wecom.js`
- 平台二进制通过 `optionalDependencies` 分发,位于 `packages/*`
- `pnpm-workspace.yaml` 当前只管理 `packages/*` 工作区。
+16
View File
@@ -0,0 +1,16 @@
# Skills 导航
仓库当前内置 Agent Skills,位于 `skills/` 目录下。这里负责给出分类和入口;每个 Skill 的具体工作流、参数示例和补充参考仍以各自的 `SKILL.md` 为准。
## Agent Skills
内置 Agent Skills 列表,可被 AI 工具直接调用:
| Skill | 品类 | 说明 |
| ----- | ---- | ---- |
| `wecomcli-contact` | contact | 查询通讯录成员 |
| `wecomcli-todo` | todo | 待办列表查询、查询待办详情、创建待办、更新待办、删除待办、变更待办状态 |
| `wecomcli-meeting` | meeting | 创建预约会议、取消会议、更新参会成员、查询会议列表和详情 |
| `wecomcli-msg` | msg | 查询会话列表、查询会话的消息记录、下载会话中的媒体文件、发送文本消息 |
| `wecomcli-schedule` | schedule | 查询日程列表、查询日程详情、取消日程、管理日程参与人、查询用户日程闲忙状态 |
| `wecomcli-doc` | doc | 创建文档、覆盖写文档、读取文档内容、管理智能表格子表与字段、增删改查智能表行记录 |
+21
View File
@@ -0,0 +1,21 @@
import eslint from '@eslint/js';
import globals from 'globals';
import prettier from 'eslint-plugin-prettier/recommended';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
prettier,
{
ignores: ['node_modules/', 'packages/*/bin/', 'target/'],
},
{
files: ['**/*.{js,ts}'],
languageOptions: {
globals: {
...globals.node,
},
},
},
);
+48
View File
@@ -0,0 +1,48 @@
{
"name": "@wecom/cli",
"version": "0.1.9",
"description": "The official CLI for WeCom",
"keywords": [
"wecom-cli",
"mcp"
],
"homepage": "https://github.com/WecomTeam/wecom-cli#readme",
"bugs": {
"url": "https://github.com/WecomTeam/wecom-cli/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/WecomTeam/wecom-cli.git"
},
"license": "MIT",
"type": "module",
"bin": {
"wecom-cli": "./bin/wecom.js"
},
"files": [
"bin",
"README.md"
],
"scripts": {
"prepare": "git config core.hooksPath .githooks"
},
"devDependencies": {
"@changesets/cli": "^2.30.0",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^10.0.1",
"@tsconfig/node-lts": "^24.0.0",
"@types/node": "^25.5.0",
"eslint": "^10.1.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^17.4.0",
"prettier": "^3.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.57.2"
},
"packageManager": "pnpm@10.32.1",
"engines": {
"node": ">=18"
}
}
+15
View File
@@ -0,0 +1,15 @@
# @wecom/cli-darwin-arm64
## 0.1.9
### Patch Changes
- 3700b1b: update cmds
## 0.1.8
### Patch Changes
- 7774ba3: update init process
## 0.1.7
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 WeCom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27
View File
@@ -0,0 +1,27 @@
# @wecom/cli-darwin-arm64
Platform-specific binary package for **macOS ARM64 (Apple Silicon)**.
## About
This package contains the pre-built `wecom-cli` binary for macOS on Apple Silicon (M1/M2/M3/M4) architecture.
**You should not install this package directly.** It is automatically installed as an optional dependency of [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) when running on a compatible platform.
## Supported Platform
| OS | CPU |
|--------|-------|
| macOS | arm64 |
## Usage
Install the main package instead:
```bash
npm install -g @wecom/cli
```
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@wecom/cli-darwin-arm64",
"version": "0.1.9",
"description": "The darwin-arm64 binary for @wecom/cli",
"license": "MIT",
"files": [
"bin",
"README.md"
],
"engines": {
"node": ">=18"
},
"os": [
"darwin"
],
"cpu": [
"arm64"
]
}
+15
View File
@@ -0,0 +1,15 @@
# @wecom/cli-darwin-x64
## 0.1.9
### Patch Changes
- 3700b1b: update cmds
## 0.1.8
### Patch Changes
- 7774ba3: update init process
## 0.1.7
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 WeCom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27
View File
@@ -0,0 +1,27 @@
# @wecom/cli-darwin-x64
Platform-specific binary package for **macOS x64 (Intel)**.
## About
This package contains the pre-built `wecom-cli` binary for macOS on Intel x64 architecture.
**You should not install this package directly.** It is automatically installed as an optional dependency of [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) when running on a compatible platform.
## Supported Platform
| OS | CPU |
|--------|-----|
| macOS | x64 |
## Usage
Install the main package instead:
```bash
npm install -g @wecom/cli
```
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@wecom/cli-darwin-x64",
"version": "0.1.9",
"description": "The darwin-x64 binary for @wecom/cli",
"license": "MIT",
"files": [
"bin",
"README.md"
],
"engines": {
"node": ">=18"
},
"os": [
"darwin"
],
"cpu": [
"x64"
]
}
+15
View File
@@ -0,0 +1,15 @@
# @wecom/cli-linux-arm64
## 0.1.9
### Patch Changes
- 3700b1b: update cmds
## 0.1.8
### Patch Changes
- 7774ba3: update init process
## 0.1.7
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 WeCom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27
View File
@@ -0,0 +1,27 @@
# @wecom/cli-linux-arm64
Platform-specific binary package for **Linux ARM64 (aarch64)**.
## About
This package contains the pre-built `wecom-cli` binary for Linux on ARM64 (aarch64) architecture.
**You should not install this package directly.** It is automatically installed as an optional dependency of [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) when running on a compatible platform.
## Supported Platform
| OS | CPU |
|-------|-------|
| Linux | arm64 |
## Usage
Install the main package instead:
```bash
npm install -g @wecom/cli
```
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@wecom/cli-linux-arm64",
"version": "0.1.9",
"description": "The linux-arm64 binary for @wecom/cli",
"license": "MIT",
"files": [
"bin",
"README.md"
],
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"arm64"
]
}
+15
View File
@@ -0,0 +1,15 @@
# @wecom/cli-linux-x64
## 0.1.9
### Patch Changes
- 3700b1b: update cmds
## 0.1.8
### Patch Changes
- 7774ba3: update init process
## 0.1.7
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 WeCom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27
View File
@@ -0,0 +1,27 @@
# @wecom/cli-linux-x64
Platform-specific binary package for **Linux x64**.
## About
This package contains the pre-built `wecom-cli` binary for Linux on x64 architecture.
**You should not install this package directly.** It is automatically installed as an optional dependency of [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) when running on a compatible platform.
## Supported Platform
| OS | CPU |
|-------|-----|
| Linux | x64 |
## Usage
Install the main package instead:
```bash
npm install -g @wecom/cli
```
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@wecom/cli-linux-x64",
"version": "0.1.9",
"description": "The linux-x64 binary for @wecom/cli",
"license": "MIT",
"files": [
"bin",
"README.md"
],
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}
+15
View File
@@ -0,0 +1,15 @@
# @wecom/cli-win32-x64
## 0.1.9
### Patch Changes
- 3700b1b: update cmds
## 0.1.8
### Patch Changes
- 7774ba3: update init process
## 0.1.7
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 WeCom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27
View File
@@ -0,0 +1,27 @@
# @wecom/cli-win32-x64
Platform-specific binary package for **Windows x64**.
## About
This package contains the pre-built `wecom-cli.exe` binary for Windows on x64 architecture.
**You should not install this package directly.** It is automatically installed as an optional dependency of [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) when running on a compatible platform.
## Supported Platform
| OS | CPU |
|---------|-----|
| Windows | x64 |
## Usage
Install the main package instead:
```bash
npm install -g @wecom/cli
```
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@wecom/cli-win32-x64",
"version": "0.1.9",
"description": "The win32-x64 binary for @wecom/cli",
"license": "MIT",
"files": [
"bin",
"README.md"
],
"engines": {
"node": ">=18"
},
"os": [
"win32"
],
"cpu": [
"x64"
]
}
+2244
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
packages:
- '.'
- 'packages/*'
+6
View File
@@ -0,0 +1,6 @@
/**
* @type {import('prettier').Config}
*/
export default {
singleQuote: true,
};
+5
View File
@@ -0,0 +1,5 @@
edition = "2024"
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true
use_try_shorthand = true
+178
View File
@@ -0,0 +1,178 @@
---
name: wecomcli-contact
description: 通讯录成员查询技能,获取当前用户可见范围内的通讯录成员,支持按姓名/别名本地筛选匹配。返回 userid、姓名和别名。⚠️ 仅返回当前用户有权限查看的成员,非全量成员。
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli contact --help"
---
# 通讯录成员查询技能
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
获取当前用户可见范围内的通讯录成员,并在本地按姓名/别名进行筛选匹配。
## 操作
### 1. 获取全量通讯录成员
获取当前用户可见范围内的所有企业成员信息:
**调用示例:**
```bash
wecom-cli contact get_userlist '{}'
```
**返回格式:**
```json
{
"errcode": 0,
"errmsg": "ok",
"userlist": [
{
"userid": "zhangsan",
"name": "张三",
"alias": "Sam"
},
{
"userid": "lisi",
"name": "李四",
"alias": ""
}
]
}
```
**返回字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `userlist` | array | 用户列表 |
| `userlist[].userid` | string | 用户唯一 ID |
| `userlist[].name` | string | 用户姓名 |
| `userlist[].alias` | string | 用户别名,可能为空 |
---
### 2. 按姓名/别名搜索人员
`get_userlist` 返回全量成员后,在本地对结果进行筛选匹配:
- **精确匹配**`name``alias` 与关键词完全一致,直接使用
- **模糊匹配**`name``alias` 包含关键词,返回所有匹配结果
- **无结果**:告知用户未找到对应人员
**搜索示例:**
用户问:"帮我找一下张三是谁?"
1. 调用 `get_userlist` 获取全量成员
2.`userlist` 中筛选 `name``alias` 包含"张三"的成员
3. 返回匹配结果
---
## 注意事项
- `get_userlist` 返回的是当前用户**可见范围内**的成员,需经过可见性规则过滤,不一定是全公司所有人员;返回字段仅包含 `userid``name`(姓名)和 `alias`(别名)
- ⚠️ **超过 10 人时接口将报错**:若 `userlist` 返回成员数量超过 10 人,视为异常,应立即停止处理并向用户说明:
> 当前通讯录可见成员数量超过了本技能支持的上限(10 人)。
> 本技能仅适用于可见范围较小的场景,无法在大范围通讯录中使用。
> 建议缩小可见范围后重试,或通过其他方式查询目标人员。
- `userid` 是用户的唯一标识,在需要传递用户 ID 给其他接口时使用此字段
- `alias` 字段可能为空字符串,搜索时需做空值判断
- 若搜索结果有多个同名人员,需将所有候选人展示给用户选择,不得自行决定
-`errcode` 不为 `0`,说明接口调用失败,需告知用户错误信息(`errmsg`
---
## 典型工作流
### 工作流 1:查询人员信息
用户问:"帮我查一下 Sam 是谁?"
1.
```bash
wecom-cli contact get_userlist '{}'
```
获取全量成员列表
2. 在结果中筛选 `alias``Sam``name` 包含 `Sam` 的成员
3. 若找到唯一匹配,直接展示结果:
```
📇 找到成员:
- 姓名:张三
- 别名:Sam
- 用户IDzhangsan
```
4. 若找到多个匹配,展示候选列表请用户确认:
```
🔍 找到多个匹配成员,请确认您要查询的是哪位:
1. 张三(别名:SamIDzhangsan
2. 张三丰(别名:Sam2IDzhangsan2
请问您要查询的是哪一位?
```
---
### 工作流 2:为其他功能提供 userid 转换
用户问:"帮我发消息给张三"
1.
```bash
wecom-cli contact get_userlist '{}'
```
获取全量成员
2. 筛选 `name` 为"张三"的成员,确认 `userid`
3.`userid` 传递给消息发送接口
---
### 工作流 3:批量查询多个人员
用户问:"帮我查一下张三和李四分别是谁?"
1.
```bash
wecom-cli contact get_userlist '{}'
```
获取全量成员列表
2. 分别筛选"张三"和"李四"的匹配结果
3. 汇总后一并展示
> 注意:只需调用一次 `get_userlist`,在本地对结果进行多次筛选,避免重复调用接口。
---
## 快速参考
### 接口说明
| 接口 | 用途 | 输入 | 返回 |
|------|------|------|------|
| `get_userlist` | 获取可见范围内全量通讯录成员 | 无 | 用户列表(userid、name、alias |
### 本地筛选策略
| 场景 | 策略 |
|------|------|
| 精确匹配(name 或 alias 完全一致) | 直接使用,无需用户确认 |
| 模糊匹配(name 或 alias 包含关键词),唯一结果 | 直接使用,向用户展示结果 |
| 模糊匹配,多个结果 | 展示候选列表,请用户选择 |
| 无匹配结果 | 告知用户未找到对应人员 |
+141
View File
@@ -0,0 +1,141 @@
---
name: wecomcli-doc
description: 企业微信文档(doc)管理技能。提供普通文档的新建、内容读取(Markdown)、内容覆写能力。适用场景:(1) 从零新建空白文档 (2) 以 Markdown 格式读取文档完整内容 (3) 用 Markdown 覆写文档正文。支持通过 docid 或文档 URL 定位文档。当用户提到「企业微信文档」「企微文档」「创建文档」「写个文档」,或链接形如 `https://doc.weixin.qq.com/doc/xxx` 时触发该技能。注意:在线表格(`/sheet/*`)请用 `wecomcli-sheet`;智能表格(`/smartsheet/*`)请用 `wecomcli-smartsheet`;智能文档/智能主页(`/smartpage/*`)请用 `wecomcli-smartpage`
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli doc --help"
---
# 企业微信文档管理
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
资源型技能,负责普通doc文档的新建、内容读取与覆写。文档接口支持通过 `docid``url` 二选一定位文档。
## URL 品类识别与接口路由
企业微信文档有多种品类,**URL 格式不同,所用的接口/技能也不同**。请通过 URL 严格区分:
| URL 模式 | 品类 | 处理方式 |
|---|---|---|
| `https://doc.weixin.qq.com/doc/*` | **文档** | **本 skill** |
| `https://doc.weixin.qq.com/sheet/*` | **在线表格** | 参阅 `wecomcli-sheet` skill |
| `https://doc.weixin.qq.com/smartsheet/*` | **智能表格** | 参阅 `wecomcli-smartsheet` skill |
| `https://doc.weixin.qq.com/smartpage/*` | **智能文档**(原名智能主页) | 参阅 `wecomcli-smartpage` skill |
## 调用方式
通过 `wecom-cli` 调用,品类为 `doc`
```bash
wecom-cli doc <tool_name> '<json_params>'
```
## 返回格式说明
所有接口返回 JSON 对象,包含以下公共字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功,非 `0` 表示失败 |
| `errmsg` | string | 错误信息,成功时为 `"ok"` |
`errcode` 不为 `0` 时,说明接口调用失败,可重试 1 次;若仍失败,将 `errcode``errmsg` 展示给用户。
### 特殊错误码
| errcode | errmsg | 含义 | 处理方式 |
|---------|--------|------|----------|
| `851002` | `incompatible doc type` | 文档品类与所调用的接口不匹配 | 根据文档 URL 重新确认品类(参见上方「URL 品类识别与接口路由」表),然后跳转到该品类对应的 skill |
## 接口详述
### 新建文档
新建一篇空白企微文档(`doc_type` 固定为 3)。创建成功后返回 `docid``url`
**命令**
```bash
wecom-cli doc create_doc '<JSON 参数>'
```
**参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `doc_type` | int | 是 | — | 固定传 `3`(文档) |
| `doc_name` | string | 是 | — | 文档标题,最多 255 个字符,超过会被截断 |
**返回**
| 字段 | 类型 | 说明 |
|---|---|---|
| `docid` | string | 新建文档的 docid,需妥善保存 |
| `url` | string | 新建文档的访问链接 |
**注意事项**
- 本接口仅创建**空白**文档,不携带初始内容;如需写入正文,请在创建后调用 `edit_doc_content`
### 读取完整内容
获取文档的完整内容数据,统一以 Markdown 格式返回。采用**异步轮询机制**:首次调用无需传 `task_id`,接口返回 `task_id`;若 `task_done``false`,需携带该 `task_id` 再次调用,直到 `task_done``true` 时返回完整内容。
**命令**
```bash
wecom-cli doc get_doc_content '<JSON 参数>'
```
**参数**
| 字段 | 类型 | 必填 | 默认值 | 语义 |
|---|---|---|---|---|
| `docid` | string | 与 `url` 二选一 | — | 文档的 docid |
| `url` | string | 与 `docid` 二选一 | — | 文档的访问链接 |
| `type` | int | 是 | — | 内容返回格式,固定传 `2`Markdown |
| `task_id` | string | 否 | — | 任务 ID,首次不传,轮询时填上次返回的 `task_id` |
**返回**
| 字段 | 类型 | 说明 |
|---|---|---|
| `content` | string | `task_done``true` 时返回的完整 Markdown 内容 |
| `task_id` | string | 任务 ID,未完成时用于下次轮询 |
| `task_done` | bool | 任务是否完成,`false` 时需携带 `task_id` 继续轮询 |
**使用规则**
- 首次调用不传 `task_id`;若 `task_done``false`,记录 `task_id` 后携带其再次调用,直到 `task_done``true``content`
### 覆写文档内容
用 Markdown 内容覆写文档正文。此操作为**覆写**,会替换文档全部内容。
**命令**
```bash
wecom-cli doc edit_doc_content '<JSON 参数>'
```
**参数**
| 字段 | 类型 | 必填 | 默认值 | 语义 |
|---|---|---|---|---|
| `docid` | string | 与 `url` 二选一 | — | 文档的 docid |
| `url` | string | 与 `docid` 二选一 | — | 文档的访问链接 |
| `content` | string | 是 | — | 覆写的文档内容(Markdown |
| `content_type` | int | 是 | — | 内容类型,固定传 `1`Markdown |
**使用规则**
- 此操作为覆写,会替换文档全部内容;建议先用 `get_doc_content` 了解当前内容再编辑。
- 成功判定:以返回的 `errcode == 0` 为准;非 0 时按「返回格式说明」处理(可重试 1 次)。
## 跨技能依赖
| 依赖技能 | 典型协作场景 | 数据流向 |
|---|---|---|
| `wecomcli-msg` | 用户要求把文档链接发给某人/某群 | 本 skill 新建后返回 `url``wecomcli-msg` 发送链接 |
+475
View File
@@ -0,0 +1,475 @@
---
name: wecomcli-meeting
description: 企业微信会议技能,支持创建预约会议、查询会议列表、获取会议详情、取消会议、更新会议成员。当用户需要"创建会议"、"预约会议"、"约会议"、"安排会议"、"查看会议"、"查询会议列表"、"会议详情"、"什么时候开会"、"有哪些会议"、"查找会议"、"取消会议"、"删除会议"、"修改会议成员"、"添加会议参与人"、"移除会议成员"时触发。
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli meeting --help"
---
# 企业微信会议技能
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
## 概述
wecomcli-meeting 提供企业微信会议的完整管理能力,包含以下功能:
1. **创建预约会议** - 创建会议,支持设置会议参数,邀请参与人等
2. **查询会议列表** - 按用户和时间范围查询会议 ID 列表 (限制: 当日及前后 30 天,上限 100 个)
3. **获取会议详情** - 通过会议 ID 查询完整会议信息
4. **取消会议** - 取消指定的预约会议
5. **更新会议受邀成员** - 修改会议的参与人列表
## 命令调用方式
执行指定命令:
```bash
wecom-cli meeting <tool_name> '<json_params>'
```
---
## 命令详细说明
### 1. 创建预约会议 (create_meeting)
创建一个预约会议,支持设置会议参数配置等。
#### 执行命令
```bash
wecom-cli meeting create_meeting '{"title": "<会议标题>", "meeting_start_datetime": "<会议开始时间>", "meeting_duration": <会议持续时长(秒)>}'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
| -------------------------- | ------- | ---- | ------------------------------------------------- |
| `title` | string | 是 | 会议标题 |
| `meeting_start_datetime` | string | 是 | 会议开始时间,格式:`YYYY-MM-DD HH:mm` |
| `meeting_duration` | integer | 是 | 会议持续时长 (秒),例如 3600 = 1 小时 |
| `description` | string | 否 | 会议描述 |
| `location` | string | 否 | 会议地点 |
| `invitees` | object | 否 | 被邀请人,格式:`{"userid": ["lisi", "wangwu"]}` |
| `settings` | object | 否 | 会议设置 (详见下方) |
> 被邀请人 userid 通过 `wecomcli-contact` 技能获取
**settings 字段:**
| 参数 | 类型 | 说明 |
| --------------------------- | ------- | --------------------------------------------- |
| `password` | string | 会议密码 |
| `enable_waiting_room` | boolean | 是否启用等候室 |
| `allow_enter_before_host` | boolean | 是否允许成员在主持人进入前加入 |
| `enable_enter_mute` | integer | 入会时静音设置 (枚举: 0: 关闭,1: 开启) |
| `allow_external_user` | boolean | 是否允许外部用户入会 |
| `enable_screen_watermark` | boolean | 是否开启屏幕水印 |
| `remind_scope` | integer | 提醒范围 (1: 不提醒,2: 仅提醒主持人,3: 提醒所有成员,4: 指定部分人响铃,默认仅提醒主持人) |
| `ring_users` | object | 响铃用户,格式:`{"userid": ["lisi"]}` |
> 响铃用户 userid 通过 `wecomcli-contact` 技能获取
#### 返回参数
```json
{
"errcode": 0,
"errmsg": "ok",
"meetingid": "会议ID字符串",
"meeting_code": "会议号码字符串",
"meeting_link": "会议链接URL",
"excess_users": ["无效会议账号的userid"]
}
```
| 字段 | 类型 | 说明 |
| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| `meetingid` | string | 会议 ID |
| `meeting_code` | string | 会议号码,向用户展示时需在回复**开头**单独一行纯文字展示,格式 `#会议号: xxx-xxx-xxx` (每3位用 `-` 分隔) |
| `meeting_link` | string | 会议链接 |
| `excess_users` | array | 参会人中包含无效会议账号的 userid,仅在购买会议专业版企业由于部分参会人无有效会议账号时返回 |
---
### 2. 查询会议列表 (list_user_meetings)
查询指定用户在时间范围内的会议 ID 列表。
#### 执行命令
```bash
wecom-cli meeting list_user_meetings '{"begin_datetime": "2026-03-01 00:00", "end_datetime": "2026-03-31 23:59", "limit": 100}'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
| ------------------ | ------- | ---- | --------------------------------------- |
| `begin_datetime` | string | 否 | 查询起始时间,格式:`YYYY-MM-DD HH:mm` |
| `end_datetime` | string | 否 | 查询结束时间,格式:`YYYY-MM-DD HH:mm` |
| `cursor` | string | 否 | 分页游标,用于获取下一页数据 |
| `limit` | integer | 否 | 每页返回条数,最大 100 |
> **限制**: 时间范围仅支持当日及前后 30 天。
#### 返回参数
```json
{
"errcode": 0,
"errmsg": "ok",
"next_cursor": "分页游标字符串,为空表示无更多",
"meetingid_list": ["会议ID_1", "会议ID_2"]
}
```
| 字段 | 类型 | 说明 |
| ------------------ | ------ | ------------------------------ |
| `meetingid_list` | array | 会议 ID 列表 |
| `next_cursor` | string | 下一页游标,为空表示无更多数据 |
---
### 3. 获取会议详情 (get_meeting_info)
通过会议 ID 查询会议的完整详情。
#### 执行命令
```bash
wecom-cli meeting get_meeting_info '{"meetingid": "<会议id>"}'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | --------------- |
| `meetingid` | string | 是 | 会议 ID,通过 `list_user_meetings` 获取 |
| `meeting_code` | string | 否 | 会议号码 |
| `sub_meetingid` | string | 否 | 子会议 ID |
#### 返回参数
> 完整的返回参数结构和字段说明详见 [references/response-get-meeting-info.md](references/response-get-meeting-info.md)
**核心字段速览:**
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `title` | string | 会议标题 |
| `meeting_start_datetime` | string | 会议开始时间 |
| `meeting_duration` | integer | 会议时长 (秒) |
| `status` | integer | 会议状态 (1: 待开始,2: 会议中,3: 已结束,4: 已取消,5: 已过期) |
| `meeting_type` | integer | 会议类型 (0: 一次性,1: 周期性,2: 微信专属,3: Rooms 投屏,5: 个人会议号,6: 网络研讨会) |
| `meeting_code` | string | 会议号码 |
| `meeting_link` | string | 会议链接 |
| `description` | string | 会议描述 |
| `location` | string | 会议地点 |
| `attendees.member[].status` | integer | 与会状态 (1: 已参与,2: 未参与) |
---
### 4. 取消会议 (cancel_meeting)
取消指定的预约会议。
#### 执行命令
```bash
wecom-cli meeting cancel_meeting '{"meetingid": "<会议id>"}'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------------------- |
| `meetingid` | string | 是 | 会议 ID,通过 `list_user_meetings` + `get_meeting_info` 获取 |
#### 返回参数
```json
{
"errcode": 0,
"errmsg": "ok"
}
```
---
### 5. 更新会议受邀成员 (set_invite_meeting_members)
更新会议的受邀成员列表(全量覆盖)。
#### 执行命令
```bash
wecom-cli meeting set_invite_meeting_members '{"meetingid": "<会议id>", "invitees": [{"userid": "lisi"}, {"userid": "wangwu"}]}'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
| ------------- | ------ | ---- | -------------------------------------- |
| `meetingid` | string | 是 | 会议 ID,通过 `list_user_meetings` + `get_meeting_info` 获取 |
| `invitees` | array | 是 | 受邀成员列表,每项包含 `userid` 字段 |
> **注意**: invitees 为全量覆盖,传入的列表将替换现有成员列表。
> invitees 的 userid 通过 `wecomcli-contact` 技能获取
#### 返回参数
```json
{
"errcode": 0,
"errmsg": "ok"
}
```
---
## 典型工作流
### 工作流 1: 最简创建 (无邀请人)
**用户意图**: "帮我约一个明天下午3点的会议,主题是周例会,时长1小时"
**步骤:**
1. **解析用户意图**: 时间 + 主题已有,邀请人未提及则默认留空,直接创建。
2. **调用创建命令**:
```bash
wecom-cli meeting create_meeting '{"title": "周例会", "meeting_start_datetime": "2026-03-18 15:00", "meeting_duration": 3600}'
```
3. **展示结果**:
#会议号: <会议号>
```
✅ 会议创建成功!
📅 <会议标题>
🕐 时间: <开始时间>,时长 <时长>
🔗 会议链接: <会议链接>
```
### 工作流 2: 带邀请人 + 地点 + 描述创建
**用户意图**: "帮我约一个明天下午3点的会议,主题是技术方案评审,邀请张三和李四,地点在3楼会议室,时长1小时"
**步骤:**
1. **解析用户意图**: 有邀请人,需先查询通讯录获取 userid。
2. **通讯录查询**: 调用 `wecomcli-contact` 技能获取通讯录成员,按姓名筛选出参与者的 userid。
```bash
wecom-cli contact get_userlist '{}'
```
在返回的 `userlist` 中筛选 `name` 包含 "张三" 和 "李四" 的成员,获取其 `userid`
3. **信息已充分,直接调用创建命令** (禁止暴露内部 ID):
```bash
wecom-cli meeting create_meeting '{"title": "技术方案评审", "meeting_start_datetime": "2026-03-18 15:00", "meeting_duration": 3600, "location": "3楼会议室", "invitees": {"userid": ["zhangsan", "lisi"]}}'
```
4. **展示结果**:
#会议号: <会议号>
```
✅ 会议创建成功!
📅 <会议标题>
🕐 时间: <开始时间>,时长 <时长>
👥 参与人: <参与者姓名列表>
🔗 会议链接: <会议链接>
```
---
### 工作流 3: 查询会议列表
**示例**: 用户说 "帮我查一下本周有哪些会议"
**步骤:**
1. **确定时间范围**: 根据当前日期计算本周的起止时间。
2. **查询会议 ID 列表**:
```bash
wecom-cli meeting list_user_meetings '{"begin_datetime": "2026-03-16 00:00", "end_datetime": "2026-03-22 23:59", "limit": 100}'
```
3. **逐个查询会议详情** (对返回的每个 meetingid):
```bash
wecom-cli meeting get_meeting_info '{"meetingid": "<会议id1>"}'
```
```bash
wecom-cli meeting get_meeting_info '{"meetingid": "<会议id2>"}'
```
4. **汇总展示**:
```
📋 本周会议列表 (共 3 场):
1. 📅 技术方案评审
🕐 2026-03-17 10:00 - 11:00
👥 张三,李四,王五
2. 📅 产品需求沟通
🕐 2026-03-18 14:00 - 15:00
👥 赵六,钱七
3. 📅 周五周会
🕐 2026-03-21 09:00 - 10:00
👥 全组成员
```
> **分页处理**: 如果 `next_cursor` 不为空,使用 `cursor` 参数继续拉取下一页。
---
### 工作流 4: 获取会议详情
**示例**: 用户说 "帮我看下技术方案评审会议的详情"
**步骤:**
1. **定位会议**: 先通过会议列表查询找到目标会议的 meetingid (按关键词匹配)。
2. **查询详情**:
```bash
wecom-cli meeting get_meeting_info '{"meetingid": "<target_meetingid>"}'
```
3. **展示结果**:
#会议号: <会议号>
```
📅 <会议标题>
🕐 时间: <开始时间>,时长 <时长>
📍 地点: <会议地点>
📝 描述: <会议描述>
👤 创建者: <创建者姓名>
👥 参与者: <参与者姓名列表>
🔗 会议链接: <会议链接>
```
---
### 工作流 5: 根据关键词查找会议
**示例**: 用户说 "技术评审会议是什么时候?"
**查询策略:**
1. **确定查询范围**: 默认查当日前后 30 天 (接口限制范围)。
2. **拉取会议列表**:
```bash
wecom-cli meeting list_user_meetings '{"begin_datetime": "2026-02-15 00:00", "end_datetime": "2026-04-16 23:59", "limit": 100}'
```
3. **逐个查询详情并匹配标题关键词**
4. **找到匹配后停止查询,展示结果**:
#会议号: <会议号>
```
✅ 找到会议: "<会议标题>"
📅 时间: <开始时间>,时长 <时长>
📍 地点: <会议地点>
👥 参与者: <参与者姓名列表>
🔗 会议链接: <会议链接>
```
5. **未找到处理**: 告知用户在前后 30 天范围内未找到匹配会议,请确认会议名称。
---
### 工作流 6: 取消会议
**示例**: 用户说 "帮我取消明天的技术方案评审会议"
**步骤:**
1. **定位会议**: 通过 `list_user_meetings` + `get_meeting_info` 查询会议列表 + 关键词匹配找到目标会议。
2. **直接执行取消**:
```bash
wecom-cli meeting cancel_meeting '{"meetingid": "<target_meetingid>"}'
```
3. **展示结果**:
```
✅ 会议已取消: 技术方案评审
```
---
### 工作流 7: 更新会议成员
**示例**: 用户说 "把王五加到技术方案评审会议里"
**步骤:**
1. **定位会议**: 通过 `list_user_meetings` + `get_meeting_info` 查询会议列表 + 匹配找到目标会议。
2. **获取当前受邀成员**: `set_invite_meeting_members` 为全量覆盖,必须先通过 `get_meeting_info` 获取会议详情,获取现有成员后再合并。
3. **通讯录查询**: 调用 `wecomcli-contact` 技能获取通讯录成员,按姓名筛选出王五的 userid。
```bash
wecom-cli contact get_userlist '{}'
```
在返回的 `userlist` 中筛选 `name` 包含 "王五" 的成员,获取其 `userid`
4. **合并成员列表**: 将现有成员 + 新增成员合并 (全量覆盖)。
5. **执行更新**:
```bash
wecom-cli meeting set_invite_meeting_members '{"meetingid": "<target_meetingid>", "invitees": [{"userid": "zhangsan"}, {"userid": "lisi"}, {"userid": "wangwu"}]}'
```
6. **展示结果**:
```
✅ 会议成员已更新: 技术方案评审
👥 当前成员: 张三,李四,王五
```
---
## 复杂场景样例
按场景按需加载,避免一次性引入过多无关示例:
| 文件 | 适用场景 |
| ---- | -------- |
| [references/response-get-meeting-info.md](references/response-get-meeting-info.md) | 获取会议详情完整返回参数结构和字段说明 |
| [references/example-security.md](references/example-security.md) | 会议密码,等候室,外部用户限制 |
| [references/example-reminder.md](references/example-reminder.md) | 响铃提醒,指定部分人响铃 |
| [references/example-full.md](references/example-full.md) | 全参数综合场景 (含静音,屏幕水印,等候室等设置) |
---
## 注意事项
- **信息追问**: 缺少时间或主题时,简洁追问用户;未提及邀请人则默认留空
- **通讯录查询**: 涉及参与人时,需先通过 `wecomcli-contact` 技能的 `get_userlist` 接口获取全量通讯录成员,再按姓名/别名本地筛选匹配出对应的 `userid`。该接口无入参,返回当前用户可见范围内的成员列表 (含 `userid``name``alias`)
- **直接创建**: 时间 + 主题已知即可直接创建,邀请人有则带上,无则留空;无论信息是一次性提供还是上下文可推断,非必要则均不请求确认,直接创建即可
- **时间格式**: 统一使用 `YYYY-MM-DD HH:mm` 格式
- **会议列表时间范围限制**: 仅支持查询当日及前后 30 天内的会议
- **查询详情需两步**: 先通过 `list_user_meetings` 获取会议 ID 列表,再通过 `get_meeting_info` 逐个获取详情
- **定位会议**: 取消会议和更新成员等管理操作需先通过查询定位到目标会议的 meetingid
- **成员更新为全量覆盖**: `set_invite_meeting_members` 传入的列表将替换现有成员列表,需先获取当前成员再合并
- **参与人仅支持企业内成员**,不支持外部人员
@@ -0,0 +1,30 @@
# 创建会议 - 全参数综合场景示例
## 场景 : 高规格会议 (全参数)
**用户意图**: "帮我创建一个高规格的季度战略会议: 下周一上午9点,时长4小时,邀请全团队,设置密码,开启等候室,开启屏幕水印,全员静音"
```json
{
"title": "Q2季度战略规划会",
"meeting_start_datetime": "2026-03-23 09:00",
"meeting_duration": 14400,
"description": "Q2季度战略规划,请各部门负责人提前准备汇报材料",
"location": "总部大会议室",
"invitees": {
"userid": ["zhangsan", "lisi", "wangwu", "zhaoliu", "sunqi"]
},
"settings": {
"password": "2026",
"enable_waiting_room": true,
"allow_enter_before_host": false,
"enable_enter_mute": 1,
"allow_external_user": false,
"enable_screen_watermark": true,
"remind_scope": 3,
"ring_users": {
"userid": ["zhangsan", "lisi", "wangwu", "zhaoliu", "sunqi"]
}
}
}
```
@@ -0,0 +1,43 @@
# 创建会议 - 响铃提醒场景示例
## 场景 1: 仅提醒主持人
**用户意图**: "帮我创建一个会议,只提醒主持人,其他人不要响铃"
```json
{
"title": "项目启动会",
"meeting_start_datetime": "2026-03-21 10:00",
"meeting_duration": 3600,
"invitees": {
"userid": ["zhangsan", "lisi"]
},
"settings": {
"remind_scope": 2
}
}
```
---
## 场景 2: 指定部分人响铃 (remind_scope=4)
**用户意图**: "帮我创建一个会议,只响铃提醒张三和李四,其他人不提醒"
```json
{
"title": "紧急故障复盘",
"meeting_start_datetime": "2026-03-18 20:00",
"meeting_duration": 3600,
"invitees": {
"userid": ["zhangsan", "lisi", "wangwu", "zhaoliu"]
},
"settings": {
"remind_scope": 4,
"ring_users": {
"userid": ["zhangsan", "lisi"]
},
"allow_enter_before_host": true
}
}
```
@@ -0,0 +1,22 @@
# 创建会议 - 安全设置场景示例
## 场景 : 会议密码 + 等候室 + 主持人设置
**用户意图**: "帮我创建一个重要的客户汇报会议,需要设置密码1234,开启等候室,不允许外部人员入会"
```json
{
"title": "客户汇报会议",
"meeting_start_datetime": "2026-03-19 14:00",
"meeting_duration": 5400,
"invitees": {
"userid": ["zhangsan", "lisi", "wangwu"]
},
"settings": {
"password": "1234",
"enable_waiting_room": true,
"allow_enter_before_host": false,
"allow_external_user": false
}
}
```
@@ -0,0 +1,148 @@
# 获取会议详情 (get_meeting_info) - 返回参数
## 返回参数
```json
{
"errcode": 0,
"errmsg": "ok",
"creator_userid": "创建者userid",
"admin_userid": "会议管理userid (与 creator_userid 有且仅返回一个)",
"title": "会议标题",
"meeting_start_datetime": "YYYY-MM-DD HH:mm",
"meeting_duration": "会议时长秒数",
"description": "会议描述文本",
"location": "会议地点文本",
"main_department": "创建者主部门ID",
"status": "会议状态枚举值",
"meeting_type": "会议类型枚举值",
"attendees": {
"member": [
{
"userid": "内部成员userid",
"status": "与会状态枚举值",
"first_join_datetime": "YYYY-MM-DD HH:mm",
"last_quit_datetime": "YYYY-MM-DD HH:mm",
"total_join_count": "加入次数",
"cumulative_time": "累计在会时长秒数"
}
],
"tmp_external_user": [
{
"tmp_external_userid": "外部临时用户ID",
"status": "与会状态枚举值",
"first_join_datetime": "YYYY-MM-DD HH:mm",
"last_quit_datetime": "YYYY-MM-DD HH:mm",
"total_join_count": "加入次数",
"cumulative_time": "累计在会时长秒数"
}
]
},
"settings": {
"remind_scope": "提醒范围枚举值",
"need_password": "是否需要密码布尔值",
"password": "会议密码",
"enable_waiting_room": "是否启用等候室布尔值",
"allow_enter_before_host": "是否允许提前入会布尔值",
"enable_enter_mute": "入会静音枚举值",
"allow_unmute_self": "是否允许自我解除静音布尔值",
"allow_external_user": "是否允许外部用户布尔值",
"enable_screen_watermark": "是否开启水印布尔值",
"watermark_type": "水印类型枚举值",
"auto_record_type": "录制类型枚举字符串",
"attendee_join_auto_record": "参会者加入自动录制布尔值",
"enable_host_pause_auto_record": "主持人可暂停录制布尔值",
"enable_doc_upload_permission": "允许上传文档布尔值",
"enable_enroll": "是否开启报名布尔值",
"enable_host_key": "是否启用主持人密钥布尔值",
"host_key": "主持人密钥字符串",
"hosts": {"userid": ["主持人userid列表"]},
"current_hosts": {"userid": ["当前主持人userid列表"]},
"co_hosts": {"userid": ["联席主持人userid列表"]},
"ring_users": {"userid": ["响铃用户userid列表"]}
},
"meeting_code": "会议号码字符串",
"meeting_link": "会议链接URL",
"has_vote": "是否有投票布尔值",
"has_more_sub_meeting": "是否还有更多子会议枚举值",
"remain_sub_meetings": "剩余子会议场数",
"current_sub_meetingid": "当前子会议ID",
"guests": [
{
"area": "国际区号",
"phone_number": "手机号字符串",
"guest_name": "嘉宾姓名"
}
],
"reminders": {
"is_repeat": "是否周期性枚举值",
"repeat_type": "重复类型枚举值",
"repeat_until_type": "结束类型枚举值",
"repeat_until_count": "限定次数",
"repeat_until_datetime": "YYYY-MM-DD HH:mm",
"repeat_interval": "重复间隔数值",
"is_custom_repeat": "是否自定义重复枚举值",
"repeat_day_of_week": ["星期几数组"],
"repeat_day_of_month": ["日期数组"],
"remind_before": ["提醒秒数数组"]
},
"sub_meetings": [
{
"sub_meetingid": "子会议ID",
"status": "子会议状态枚举值",
"start_datetime": "YYYY-MM-DD HH:mm",
"end_datetime": "YYYY-MM-DD HH:mm",
"title": "子会议标题",
"repeat_id": "周期性会议分段ID"
}
],
"sub_repeat_list": [
{
"repeat_id": "周期性会议分段ID",
"repeat_type": "重复类型枚举值",
"repeat_until_type": "结束类型枚举值",
"repeat_until_count": "限定次数",
"repeat_until_datetime": "YYYY-MM-DD HH:mm",
"repeat_interval": "重复间隔数值",
"is_custom_repeat": "是否自定义重复枚举值",
"repeat_day_of_week": ["星期几数组"],
"repeat_day_of_month": ["日期数组"]
}
]
}
```
## 关键返回字段
| 字段 | 类型 | 说明 |
| ----------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `creator_userid` | string | 创建者 userid,与 `admin_userid` 有且仅返回一个 |
| `admin_userid` | string | 会议管理 userid,与 `creator_userid` 有且仅返回一个 |
| `title` | string | 会议标题 |
| `meeting_start_datetime` | string | 会议开始时间 |
| `meeting_duration` | integer | 会议时长 (秒) |
| `main_department` | integer | 创建者所属主部门 |
| `status` | integer | 会议状态 (1: 待开始,2: 会议中,3: 已结束,4: 已取消,5: 已过期) |
| `meeting_type` | integer | 会议类型 (0: 一次性会议,1: 周期性会议,2: 微信专属会议,3: Rooms 投屏会议,5: 个人会议号会议,6: 网络研讨会) |
| `meeting_code` | string | 会议号码 |
| `meeting_link` | string | 会议链接 |
| `attendees.member` | array | 内部参与者列表 |
| `attendees.member[].status` | integer | 与会状态 (1: 已参与,2: 未参与) |
| `attendees.tmp_external_user` | array | 外部参与者 (临时 ID) |
| `attendees.tmp_external_user[].status` | integer | 与会状态 (1: 已参与,2: 未参与) |
| `guests` | array | 外部嘉宾列表,每项含 `area``phone_number``guest_name` |
| `current_sub_meetingid` | string | 当前子会议 ID |
| `settings.ring_users` | object | 响铃用户列表 |
| `settings.need_password` | boolean | 是否需要密码 (只读字段) |
| `settings.enable_doc_upload_permission` | boolean | 是否允许成员上传文档 |
| `settings.hosts` | object | 主持人列表 |
| `settings.current_hosts` | object | 当前主持人列表 |
| `settings.co_hosts` | object | 联席主持人列表 |
| `reminders` | object | 周期性配置 |
| `has_vote` | boolean | 是否有投票 (仅会议创建人和主持人有权限查询) |
| `has_more_sub_meeting` | integer | 是否还有更多子会议特例 (0: 无更多,1: 有更多) |
| `remain_sub_meetings` | integer | 剩余子会议场数 |
| `sub_meetings` | array | 子会议列表 |
| `sub_meetings[].status` | integer | 子会议状态 (0: 默认/存在,1: 已删除) |
| `sub_meetings[].repeat_id` | string | 周期性会议分段 ID,用于关联子会议所属分段 |
| `sub_repeat_list` | array | 周期性会议分段信息,修改周期性会议某一场后可能产生不同分段,各分段有不同重复规则 |
+164
View File
@@ -0,0 +1,164 @@
---
name: wecomcli-msg
description: 企业微信消息技能。提供会话列表查询、消息记录拉取(支持文本/图片/文件/语音/视频)、多媒体文件获取和文本消息发送能力。当用户需要"查看消息"、"看聊天记录"、"发消息给某人"、"最近有什么消息"、"给群里发消息"、"看看发了什么图片/文件"时触发。
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli msg --help"
---
# 企业微信消息技能
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
通过 `wecom-cli msg <接口名> '<json入参>'` 与企业微信消息系统交互。
---
## 接口列表
### get_msg_chat_list — 获取会话列表
```bash
wecom-cli msg get_msg_chat_list '{"begin_time": "2026-03-11 00:00:00", "end_time": "2026-03-17 23:59:59"}'
```
按时间范围查询有消息的会话列表,支持分页。参见 [API 详情](references/get-msg-chat-list.md)。
### get_message — 拉取会话消息
```bash
wecom-cli msg get_message '{"chat_type": 1, "chatid": "zhangsan", "begin_time": "2026-03-17 09:00:00", "end_time": "2026-03-17 18:00:00"}'
```
根据会话类型和 ID 拉取指定时间范围内的消息记录,支持分页。支持 text/image/file/voice/video 消息类型,仅支持 7 天内。参见 [API 详情](references/get-message.md)。
### get_msg_media — 获取消息文件内容
```bash
wecom-cli msg get_msg_media '{"media_id": "MEDIAID_xxxxxx"}'
```
根据文件 ID 自动下载文件到本地,返回文件的本地路径(`local_path`)、名称、类型、大小及 MIME 类型。用于获取图片、文件、语音、视频等非文本消息的实际内容。参见 [API 详情](references/get-msg-media.md)。
### send_message — 发送文本消息
```bash
wecom-cli msg send_message '{"chat_type": 1, "chatid": "zhangsan", "msgtype": "text", "text": {"content": "hello world"}}'
```
向单聊或群聊发送文本消息。参见 [API 详情](references/send-message.md)。
---
## 核心规则
### 时间范围规则
- **格式**:所有时间参数使用 `YYYY-MM-DD HH:mm:ss` 格式
- **默认范围**:用户未指定时,默认使用最近7天(当前时间往前推7天)
- **限制**:开始时间不能早于当前时间的7天前,不能晚于当前时间
- **相对时间支持**:支持"昨天"、"最近三天"等自动推算
### chatid查找规则
- 当用户提供人名或群名而非ID时:
1. 调用 `get_msg_chat_list` 获取会话列表(时间范围与目标查询一致)
2.`chats` 中按 `chat_name` 匹配
3. **匹配策略**
- 精确匹配唯一结果:直接使用
- 模糊匹配多个结果:展示候选列表让用户选择
- 无匹配结果:告知用户未找到
- **chat_type 判断**`get_msg_chat_list` 返回中不含会话类型字段,需根据上下文推断:用户明确提到「群」时使用 `chat_type=2`,否则默认 `chat_type=1`(单聊)
### userid 转 name
**流程**
1. 调用 `wecomcli-contact` 技能的 `get_userlist` 获取用户列表
2. 建立 userid 到 name 的映射关系
3. **展示策略**
- 精确匹配:显示 name
- 无匹配:保持显示 userid
### 强制交互步骤(不可跳过)
以下步骤在涉及非文本消息下载时**必须逐一执行**,不得合并、省略或跳过,即使用户未主动询问也必须执行:
1. **必须主动告知文件位置**:下载完成后必须立即向用户展示所有文件的完整路径和存放目录
2. **必须询问是否删除**:告知位置后必须立即询问用户是否需要清理临时文件
---
## 典型工作流
### 查看会话列表
**用户query示例**
- "看看我最近一周有哪些聊天"
- "这几天谁给我发过消息"
**执行流程**
1. 确定时间范围(用户指定或默认最近7天)
2. 调用 `get_msg_chat_list` 获取会话列表
3. 展示会话名称、最后消息时间、消息数量
4.`has_more``true`,告知用户还有更多会话可继续查看
### 查看聊天记录
**用户query示例**
- "帮我看看和张三最近的聊天记录"
- "看看项目群里最近的消息"
**执行流程**
1. 确定时间范围(用户指定或默认最近7天)
2. 通过 **chatid查找规则** 确定目标会话的 `chatid``chat_type`
3. 调用 `get_message` 拉取消息列表
4. 调用 `wecomcli-contact` 技能的 `get_userlist` 获取通讯录,建立 userid→姓名 映射
5. **统计非文本消息**:遍历消息列表,统计 `msgtype``text` 的消息(image/file/voice/video)数量和类型
6. 展示消息时将 `userid` 替换为可读姓名,格式:
- 文本消息:`姓名 [时间]: 内容`
- 图片消息:`姓名 [时间]:[图片]`
- 文件消息:`姓名 [时间]:[文件] 文件名称`
- 语音消息:`姓名 [时间]:[语音] 语音内容`
- 视频消息:`姓名 [时间]:[视频]`
7. **非文本消息处理**:展示完消息后,如果存在非文本消息:
- **主动询问是否下载**:告知用户非文本消息数量和类型(如:"以上聊天中包含 2 张图片、1 个文件,是否需要下载到本地?")
- 用户确认后,逐个调用 `get_msg_media` 接口,接口会自动下载文件并返回 `local_path`
- **检查文件后缀**:每个文件下载完成后,检查 `local_path` 对应的文件是否具有正确的后缀名:
- 根据 `get_msg_media` 返回的 `content_type`MIME 类型)和 `name` 字段判断:
- 如果文件名缺少后缀(如 `screenshot` 而非 `screenshot.png`),根据 `content_type` 自动补上正确后缀(如 `image/png``.png``application/pdf``.pdf``audio/amr``.amr``video/mp4``.mp4`
- 如果文件名后缀与 `content_type` 不一致,以 `content_type` 为准进行修正
- 补全或修正后缀后,将文件重命名为正确的文件名
- 确认文件可正常读取(文件大小 > 0),若文件为空或损坏则告知用户该文件下载异常
- ⚠️ **不要对下载的文件使用 `MEDIA:` 指令**:这些文件是从聊天记录中下载的历史附件,仅需告知用户本地存放路径即可,**严禁**通过 `MEDIA:` 指令重新发送给用户
8. ⚠️ **必须主动告知文件位置**(此步骤不可跳过):所有文件下载并检查完成后,**必须立即、主动**以汇总形式向用户展示文件存放目录和每个文件的完整路径,不要等用户询问。示例:
> 📁 文件已下载到以下位置:
> - 图片:`xxx/yyy.png`
> - 文件:`xxx/yyy.pdf`
>
> 你可以在 `xxx/yyy/` 目录下找到所有下载的文件。
9. ⚠️ **必须询问是否删除**(此步骤不可跳过):告知文件位置后,**必须立即、主动**询问用户是否需要删除已下载的临时文件(如:"如果不再需要这些文件,是否需要我帮你清理?")
- 用户确认删除后,删除 `local_path` 对应的文件
- 用户不需要删除则保留文件
10.`next_cursor` 不为空,告知用户还有更多消息可继续查看
### 发送消息
**用户query示例**
- "帮我给张三发一条消息:明天会议改到下午3点"
- "在项目群里发一条消息:今天下午3点开会"
**执行流程**
1. 通过 **chatid查找规则** 确定目标会话的 `chatid``chat_type`
2. **发送前确认**:向用户确认发送对象和内容(如:"即将向 张三 发送:'明天会议改到下午3点',确认发送吗?"),用户确认后再执行
3. 调用 `send_message` 发送(`msgtype` 固定为 `text`
4. 展示发送结果
### 查看消息并回复
**用户query示例**
- "看看张三给我发了什么,然后帮我回复收到"
**执行流程**
1. 先执行"查看聊天记录"流程(复用已获取的 `chatid``chat_type`
2. 展示消息后,执行"发送消息"流程(需确认后再发送)
---
## 错误处理
- **时间范围超限**:告知用户7天限制并调整为有效范围
- **会话未找到**:明确告知用户未找到对应会话
- **API错误**:展示具体错误信息,必要时重试
- **网络问题**HTTP错误时主动重试最多3次
@@ -0,0 +1,99 @@
# get_message API
根据会话类型和会话 ID,拉取指定时间范围内的消息记录。支持文本、图片、文件、语音、视频类型消息。
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `chat_type` | integer | ✅ | 会话类型,`1`-单聊,`2`-群聊 |
| `chatid` | string | ✅ | 会话 ID,单聊时为 userid,群聊时为群 ID,最大 256 字节 |
| `begin_time` | string | ✅ | 拉取开始时间,格式:`YYYY-MM-DD HH:mm:ss`,仅支持请求时刻往前 **7 天**内 |
| `end_time` | string | ✅ | 拉取结束时间,格式:`YYYY-MM-DD HH:mm:ss`,必须 ≥ `begin_time` |
| `cursor` | string | ❌ | 分页游标,首次请求不传,后续传入上次响应的 `next_cursor`,最大 256 字节 |
## 请求示例
单聊:
```bash
wecom-cli msg get_message '{"chat_type": 1, "chatid": "zhangsan", "begin_time": "2026-03-17 09:00:00", "end_time": "2026-03-17 18:00:00"}'
```
群聊:
```bash
wecom-cli msg get_message '{"chat_type": 2, "chatid": "wrxxxxxxxx", "begin_time": "2026-03-17 09:00:00", "end_time": "2026-03-17 18:00:00"}'
```
分页请求:
```bash
wecom-cli msg get_message '{"chat_type": 1, "chatid": "zhangsan", "begin_time": "2026-03-17 09:00:00", "end_time": "2026-03-17 18:00:00", "cursor": "CURSOR_xxxxxx"}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `messages` | array | 消息列表 |
| `messages[].userid` | string | 消息发送者的 userid |
| `messages[].send_time` | string | 消息发送时间(北京时间),格式:`YYYY-MM-DD HH:mm:ss` |
| `messages[].msgtype` | string | 消息类型,`text`-文本消息,`image`-图片消息,`file`-文件消息,`voice`-语音消息,`video`-视频消息 |
| `messages[].text` | object | 文本消息内容,`msgtype``text` 时返回 |
| `messages[].text.content` | string | 消息内容 |
| `messages[].image` | object | 图片消息内容,`msgtype``image` 时返回 |
| `messages[].image.media_id` | string | 图片的 media_id,可通过 `get_msg_media` 接口下载 |
| `messages[].image.name` | string | 图片文件名称 |
| `messages[].file` | object | 文件消息内容,`msgtype``file` 时返回 |
| `messages[].file.media_id` | string | 文件的 media_id,可通过 `get_msg_media` 接口下载 |
| `messages[].file.name` | string | 文件名称 |
| `messages[].voice` | object | 语音消息内容,`msgtype``voice` 时返回 |
| `messages[].voice.media_id` | string | 语音的 media_id,可通过 `get_msg_media` 接口下载 |
| `messages[].video` | object | 视频消息内容,`msgtype``video` 时返回 |
| `messages[].video.media_id` | string | 视频的 media_id,可通过 `get_msg_media` 接口下载 |
| `next_cursor` | string | 分页游标,为空表示已拉取完毕 |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"messages": [
{
"userid": "zhangsan",
"send_time": "2026-03-17 09:30:00",
"msgtype": "text",
"text": {
"content": "你好"
}
},
{
"userid": "lisi",
"send_time": "2026-03-17 09:35:00",
"msgtype": "image",
"image": {
"media_id": "MEDIAID_xxxxxx",
"name": "screenshot.png"
}
},
{
"userid": "zhangsan",
"send_time": "2026-03-17 09:40:00",
"msgtype": "file",
"file": {
"media_id": "MEDIAID_yyyyyy",
"name": "report.pdf"
}
}
],
"next_cursor": "CURSOR_xxxxxx"
}
```
## 非文本消息处理
`msgtype``image``file``voice``video` 时,消息体中包含 `media_id`。需要调用 [get_msg_media](get-msg-media.md) 接口获取文件的本地路径(`local_path`),再进行展示。
@@ -0,0 +1,62 @@
# get_msg_chat_list API
获取指定时间范围内有消息的会话列表,支持分页查询。
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `begin_time` | string | ✅ | 拉取开始时间,格式:`YYYY-MM-DD HH:mm:ss` |
| `end_time` | string | ✅ | 拉取结束时间,格式:`YYYY-MM-DD HH:mm:ss` |
| `cursor` | string | ❌ | 分页游标,首次请求不传,后续传入上次响应的 `next_cursor`,最大长度 256 |
## 请求示例
```bash
wecom-cli msg get_msg_chat_list '{"begin_time": "2026-03-11 00:00:00", "end_time": "2026-03-17 23:59:59"}'
```
分页请求:
```bash
wecom-cli msg get_msg_chat_list '{"begin_time": "2026-03-11 00:00:00", "end_time": "2026-03-17 23:59:59", "cursor": "NEXT_CURSOR"}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `chats` | array | 会话列表 |
| `chats[].chat_id` | string | 会话 ID |
| `chats[].chat_name` | string | 会话名称 |
| `chats[].last_msg_time` | string | 最后一条消息时间,格式:`YYYY-MM-DD HH:mm:ss` |
| `chats[].msg_count` | integer | 消息数量 |
| `has_more` | boolean | 是否还有更多数据 |
| `next_cursor` | string | 分页游标,用于下一次请求 |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"chats": [
{
"chat_id": "CHAT_ID",
"chat_name": "张三",
"last_msg_time": "2026-03-17 15:30:45",
"msg_count": 128
},
{
"chat_id": "CHAT_ID_2",
"chat_name": "项目讨论群",
"last_msg_time": "2026-03-16 09:12:33",
"msg_count": 56
}
],
"has_more": true,
"next_cursor": "NEXT_CURSOR"
}
```
@@ -0,0 +1,46 @@
# get_msg_media API
获取消息文件内容。根据文件 ID 自动下载文件到本地,返回本地文件路径、文件名称、类型、大小及内容类型。
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `media_id` | string | ✅ | 文件 ID,长度 1~256 |
## 请求示例
```bash
wecom-cli msg get_msg_media '{"media_id": "MEDIAID_xxxxxx"}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `media_item` | object | 文件内容 |
| `media_item.media_id` | string | 文件 ID |
| `media_item.name` | string | 文件名称 |
| `media_item.type` | string | 文件类型,`image`-图片,`voice`-语音,`video`-视频,`file`-普通文件 |
| `media_item.local_path` | string | 文件下载后的本地路径 |
| `media_item.size` | integer | 文件大小(字节) |
| `media_item.content_type` | string | 文件 MIME 类型,如 `image/png``application/pdf` 等 |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"media_item": {
"media_id": "MEDIAID_xxxxxx",
"name": "screenshot.png",
"type": "image",
"local_path": "xxx/yyy/screenshot.png",
"size": 102400,
"content_type": "image/png"
}
}
```
@@ -0,0 +1,43 @@
# send_message API
向单聊或群聊发送文本消息。
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `chat_type` | integer | ✅ | 会话类型,`1`-单聊,`2`-群聊 |
| `chatid` | string | ✅ | 会话 ID,单聊时为 userid,群聊时为群 ID,最大 256 字节 |
| `msgtype` | string | ✅ | 消息类型,目前仅支持 `text` |
| `text` | object | ✅ | 文本消息内容 |
| `text.content` | string | ✅ | 消息内容,最大 2048 字节 |
## 请求示例
单聊:
```bash
wecom-cli msg send_message '{"chat_type": 1, "chatid": "zhangsan", "msgtype": "text", "text": {"content": "hello world"}}'
```
群聊:
```bash
wecom-cli msg send_message '{"chat_type": 2, "chatid": "wrxxxxxxxx", "msgtype": "text", "text": {"content": "大家好"}}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok"
}
```
+176
View File
@@ -0,0 +1,176 @@
---
name: wecomcli-schedule
description: 企业微信日程管理技能。适用于用户对企业微信日程的各类管理需求。当用户需要:(1) 查询指定时间范围内的日程列表或获取日程详细信息(标题、时间、地点、参与者等),(2) 创建新日程并设置提醒、参与人等,(3) 修改已有日程的标题、时间、地点等信息或取消日程,(4) 添加或移除日程参与人,(5) 查询多个成员的闲忙状态并分析共同空闲时段以安排会议时使用此技能。
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli schedule --help"
---
# 企业微信日程管理技能
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
通过 `wecom-cli schedule <接口名> '<json入参>'` 与企业微信日程系统交互。
## 注意事项
- 日程列表查询仅支持**当日前后 30 天**,时间格式 `YYYY-MM-DD``YYYY-MM-DD HH:mm:ss`
- 涉及参与者 userid 时,需先使用 **wecomcli-contact** 技能获取;存在同名时展示候选让用户选择(禁止暴露 userid)
- 创建/修改/取消前,先确认目标日程和参与者信息
- `errcode != 0` 时展示错误信息;返回的 `start_time`/`end_time` 为 Unix 时间戳(秒),需转为可读格式
- **注意时间格式转换**:接口入参使用字符串格式(如 `YYYY-MM-DD HH:mm:ss`),但返回值多为 Unix 时间戳,使用时需进行格式转换
---
## 接口列表
### get_schedule_list_by_range — 查询日程 ID 列表
```bash
wecom-cli schedule get_schedule_list_by_range '{"start_time": "YYYY-MM-DD HH:mm:ss", "end_time": "YYYY-MM-DD HH:mm:ss"}'
```
返回 `schedule_id_list` 数组。仅支持当日前后 30 天。
### get_schedule_detail — 获取日程详情
```bash
wecom-cli schedule get_schedule_detail '{"schedule_id_list": ["SCHEDULE_ID_1", "SCHEDULE_ID_2"]}'
```
支持 1~50 个 ID,返回日程标题、时间、地点、参与者等。参见 [API 详情](references/get-schedule-detail.md)。
### create_schedule — 创建日程
```bash
wecom-cli schedule create_schedule '{"schedule": {"start_time": "YYYY-MM-DD HH:mm:ss", "end_time": "YYYY-MM-DD HH:mm:ss", "summary": "日程标题", "attendees": [{"userid": "USER_ID"}], "reminders": {"is_remind": 1, "remind_before_event_secs": 3600, "timezone": 8}}}'
```
参见 [API 详情](references/create-schedule.md) | [reminders 字段](references/ref-reminders.md)。
### update_schedule — 修改日程
只需传入需修改的字段,未传字段保持不变。
```bash
wecom-cli schedule update_schedule '{"schedule": {"schedule_id": "SCHEDULE_ID", "summary": "更新后的标题"}}'
```
参见 [API 详情](references/update-schedule.md)。
### cancel_schedule — 取消日程
```bash
wecom-cli schedule cancel_schedule '{"schedule_id": "SCHEDULE_ID"}'
```
### add_schedule_attendees / del_schedule_attendees — 管理参与人
- 添加参与人:
```bash
wecom-cli schedule add_schedule_attendees '{"schedule_id": "SCHEDULE_ID", "attendees": [{"userid": "USER_ID"}]}'
```
- 移除参与人:
```bash
wecom-cli schedule del_schedule_attendees '{"schedule_id": "SCHEDULE_ID", "attendees": [{"userid": "USER_ID"}]}'
```
### check_availability — 查询闲忙
```bash
wecom-cli schedule check_availability '{"check_user_list": ["USER_ID_1", "USER_ID_2"], "start_time": "YYYY-MM-DD HH:mm:ss", "end_time": "YYYY-MM-DD HH:mm:ss"}'
```
支持 1~10 个用户,返回各用户的忙碌时段列表。参见 [API 详情](references/check-availability.md)。
---
## 典型工作流
### 查询日程
**经典 query 示例:**
- "我今天有哪些日程?"
- "帮我看看这周三下午有没有会议"
- "明天的日程安排是什么?"
- "查一下最近有没有关于项目评审的日程"
- "我下周一到周五的日程都有哪些?"
**流程:**
1. 根据用户意图计算时间范围(如"今天"→当日 00:00:00 至 23:59:59"这周"→本周一至周日)
2. 调用 `get_schedule_list_by_range` 获取日程 ID 列表
3. 调用 `get_schedule_detail` 批量获取详情,将 Unix 时间戳转为可读时间
4. 若用户提到关键词(如"项目评审"),在 `summary` 中匹配筛选;未找到则逐步扩大范围至前后 30 天上限
5. 展示日程列表时包含标题、时间、地点、参与者等关键信息,方便用户快速了解
### 创建日程
**经典 query 示例:**
- "帮我创建一个明天下午 2 点到 3 点的会议,标题叫需求评审"
- "安排一个周五全天的团建活动"
- "创建日程:后天上午 10 点和张三、李四开产品方案讨论会,地点在 3 楼会议室"
- "帮我建个日程,下周一 14:00-15:00,提前 15 分钟提醒"
- "约一个明天上午的日程,邀请王伟参加"
**流程:**
1. 解析用户意图,提取时间、标题、地点、参与人、提醒设置等信息
2. 若涉及参与人,先通过 **wecomcli-contact** 查询 userid;存在同名时展示候选让用户选择
3. 若用户未指定提醒,默认设置提前 15 分钟提醒(`remind_before_event_secs: 900`
4. 若用户说"全天",设置 `is_whole_day: 1`,时间设为当天 00:00:00 至 23:59:59
5. 向用户确认日程信息(标题、时间、地点、参与人等)后调用 `create_schedule`
### 修改日程
**经典 query 示例:**
- "把明天的需求评审改到后天下午 3 点"
- "帮我修改下今天下午的会议标题,改成技术方案评审"
- "我今天 14 点的日程地点改成线上腾讯会议"
- "把周五的团建活动推迟一个小时"
- "帮我给明天的周会加个描述:讨论 Q2 规划"
**流程:**
1. 先通过查询工作流定位目标日程(根据用户提到的时间、标题等关键词匹配)
2. 若匹配到多个日程,展示候选列表让用户确认
3. 向用户确认要修改的字段和目标值
4. 调用 `update_schedule`,只传入需修改的字段
### 取消日程
**经典 query 示例:**
- "取消明天下午的需求评审"
- "帮我把周五的团建日程删掉"
- "我不想开今天 15 点的会了,帮我取消"
**流程:**
1. 先通过查询工作流定位目标日程
2. 向用户确认取消的日程信息(标题、时间等),避免误操作
3. 确认后调用 `cancel_schedule`
### 管理参与人
**经典 query 示例:**
- "把张三加到明天的需求评审会议里"
- "帮我把李四从周五的日程里移除"
- "明天下午的会议再邀请一下王伟和赵敏"
- "把我后天那个技术分享的参与人里去掉刘强"
**流程:**
1. 通过 **wecomcli-contact** 获取目标人员 userid;存在同名时展示候选让用户选择
2. 通过查询工作流定位目标日程
3. 调用 `add_schedule_attendees``del_schedule_attendees` 完成添加/移除
### 查询闲忙并安排会议
**经典 query 示例:**
- "帮我看看张三和李四明天下午有没有空"
- "查一下我和王伟这周的空闲时间,想约个会"
- "我想跟产品组的小明、小红开个会,看看大家什么时候有空"
- "找一个明天下午大家都有空的时段,安排一个 1 小时的会议"
**流程:**
1. 通过 **wecomcli-contact** 获取相关人员 userid
2. 调用 `check_availability` 查询指定时间范围内各用户的忙碌时段
3. 分析所有用户的忙碌时段,计算出共同空闲时段并推荐给用户
4. 用户确认时段后,调用 `create_schedule` 创建会议并自动添加参与人
@@ -0,0 +1,58 @@
# check_availability API
查询指定用户在某时间范围内的忙碌时段。
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `check_user_list` | array | ✅ | 用户 ID 列表,1~10 个 |
| `start_time` | string | ✅ | 查询开始时间 |
| `end_time` | string | ✅ | 查询结束时间 |
## 请求示例
```bash
wecom-cli schedule check_availability '{"check_user_list": ["USER_ID_1", "USER_ID_2"], "start_time": "YYYY-MM-DD HH:mm:ss", "end_time": "YYYY-MM-DD HH:mm:ss"}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `user_busy_list` | array | 用户忙碌时段列表 |
### user_busy_list[] 数组中每项字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `userid` | string | 用户 ID |
| `busy_slots` | array | 忙碌时段列表 |
| `busy_slots[].start_time` | string | 忙碌时段开始时间 |
| `busy_slots[].end_time` | string | 忙碌时段结束时间 |
| `busy_slots[].schedule_id` | string | 关联的日程 ID |
| `busy_slots[].subject` | string | 日程标题 |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"user_busy_list": [
{
"userid": "USER_ID",
"busy_slots": [
{
"start_time": "YYYY-MM-DD HH:mm:ss",
"end_time": "YYYY-MM-DD HH:mm:ss",
"schedule_id": "SCHEDULE_ID",
"subject": "日程标题"
}
]
}
]
}
```
@@ -0,0 +1,40 @@
# create_schedule API
创建新日程,支持设置标题、时间、地点、参与者、提醒和重复规则。
## 参数说明(`schedule` 对象内)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `start_time` | string | ✅ | 开始时间 |
| `end_time` | string | ✅ | 结束时间 |
| `summary` | string | ❌ | 日程标题,最长 128 字 |
| `description` | string | ❌ | 日程描述,最长 1000 字 |
| `location` | string | ❌ | 地点,最长 128 字 |
| `is_whole_day` | integer | ❌ | 是否全天:`0`-否(默认),`1`-是 |
| `attendees` | array | ❌ | 参与者列表,每项含 `userid` |
| `reminders` | object | ❌ | 提醒与重复设置(见 [reminders 字段参考](ref-reminders.md) |
## 请求示例
```bash
wecom-cli schedule create_schedule '{"schedule": {"start_time": "YYYY-MM-DD HH:mm:ss", "end_time": "YYYY-MM-DD HH:mm:ss", "summary": "日程标题", "attendees": [{"userid": "USER_ID"}], "reminders": {"is_remind": 1, "remind_before_event_secs": 3600, "timezone": 8}, "location": "会议地点"}}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `schedule_id` | string | 创建成功的日程 ID |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"schedule_id": "SCHEDULE_ID"
}
```
@@ -0,0 +1,83 @@
# get_schedule_detail API
通过日程 ID 批量获取日程详细信息。
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `schedule_id_list` | array | ✅ | 日程 ID 列表,1~50 个 |
## 请求示例
```bash
wecom-cli schedule get_schedule_detail '{"schedule_id_list": ["SCHEDULE_ID_1", "SCHEDULE_ID_2"]}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
| `schedule` | array | 日程详情列表 |
### schedule[] 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `schedule_id` | string | 日程唯一 ID |
| `summary` | string | 日程标题 |
| `description` | string | 日程描述 |
| `start_time` | integer | 开始时间(Unix 时间戳,秒) |
| `end_time` | integer | 结束时间(Unix 时间戳,秒) |
| `location` | string | 地点 |
| `status` | integer | `0`-正常,`1`-已取消 |
| `is_whole_day` | integer | `0`-否,`1`-是 |
| `admins` | array | 管理员 userid 列表 |
| `attendees` | array | 参与者列表 |
| `attendees[].userid` | string | 参与者 userid |
| `attendees[].response_status` | integer | 响应状态(见下表) |
| `reminders` | object | 提醒设置(见 [reminders 字段参考](ref-reminders.md) |
### response_status 枚举
| 值 | 含义 |
|----|------|
| `1` | 待定 |
| `2` | 接受 |
| `3` | 接受单次 |
| `4` | 拒绝 |
| `5` | 接受本次及未来 |
| `6` | 待定单次 |
| `7` | 待定本次及未来 |
| `8` | 拒绝单次 |
| `9` | 拒绝本次及未来 |
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"schedule": [
{
"schedule_id": "SCHEDULE_ID",
"summary": "日程标题",
"start_time": 1700000000,
"end_time": 1700003600,
"location": "会议室",
"status": 0,
"is_whole_day": 0,
"attendees": [
{"userid": "USER_ID","tmp_external_userid": "tmp_external_userid_example","response_status": 2}
],
"reminders": {
"is_remind": 1,
"remind_before_event_secs": 3600,
"timezone": 8
}
}
]
}
```
@@ -0,0 +1,24 @@
# reminders 字段参考
提醒设置对象,用于 `create_schedule``update_schedule` 接口。
## 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `is_remind` | integer | 是否提醒:`0`-否,`1`-是 |
| `remind_before_event_secs` | integer | 提前提醒秒数,可选值:`0`/`300`/`900`/`3600`/`86400` |
| `remind_time_diffs` | array | 提醒时间差(秒),可选值:`-604800`/`-172800`/`-86400`/`-3600`/`-900`/`-300`/`0`/`32400` |
| `timezone` | integer | 时区,`-12` ~ `12`,中国为 `8` |
## 使用示例
### 基本提醒(提前 1 小时)
```json
{
"is_remind": 1,
"remind_before_event_secs": 3600,
"timezone": 8
}
```
@@ -0,0 +1,32 @@
# update_schedule API
修改已有日程,只需传入需要修改的字段,未传字段保持不变。
## 参数说明(`schedule` 对象内)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `schedule_id` | string | ✅ | 目标日程 ID |
| `start_time` | string | ❌ | 开始时间 |
| `end_time` | string | ❌ | 结束时间 |
| `summary` | string | ❌ | 日程标题,最长 128 字 |
| `description` | string | ❌ | 日程描述,最长 1000 字 |
| `location` | string | ❌ | 地点,最长 128 字 |
| `is_whole_day` | integer | ❌ | 是否全天:`0`-否,`1`-是 |
| `attendees` | array | ❌ | 参与者列表,每项含 `userid` |
| `reminders` | object | ❌ | 提醒与重复设置(见 [reminders 字段参考](ref-reminders.md) |
> 仅传需修改的字段,其余保持不变。
## 请求示例
```bash
wecom-cli schedule update_schedule '{"schedule": {"schedule_id": "SCHEDULE_ID", "summary": "更新后的标题", "start_time": "YYYY-MM-DD HH:mm:ss", "end_time": "YYYY-MM-DD HH:mm:ss"}}'
```
## 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功 |
| `errmsg` | string | 错误信息 |
+127
View File
@@ -0,0 +1,127 @@
---
name: wecomcli-sheet
description: 企业微信在线表格(sheet)管理技能。提供在线表格的新建、内容读取、内容修改、追加行数据,以及子工作表的增删管理。适用场景:(1) 新建空白在线表格 (2) 读取表格完整内容(Markdown)(3) 读取基础信息与子表列表 (4) 读取表格子表数据 (5) 修改指定区域内容 (6) 末尾追加一行数据 (7) 添加/删除子工作表。当用户提到「企业微信表格」「企业微信在线表格」「企微 Excel 表格」,或链接形如 `https://doc.weixin.qq.com/sheet/xxx` 时触发该技能。注意:智能表格(`/smartsheet/*`)请用 `wecomcli-smartsheet`;普通文档(`/doc/*`)请用 `wecomcli-doc`;智能文档/智能主页(`/smartpage/*`)请用 `wecomcli-smartpage`
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli doc --help"
---
# 企业微信在线表格管理
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
资源型技能,负责**在线表格**`/sheet/*`)的新建、内容读写以及子工作表管理。
## 调用方式
通过 `wecom-cli` 调用,品类为 `doc`
```bash
wecom-cli doc <tool_name> '<json_params>'
```
## 接口路由表
> **硬规则**:第二列是 `references/xxx.md` 链接的,命中这一行后**先 `read` 对应 references 文件,再构造命令**。写入/读取子表数据前,先用 `sheet_get_info` 拿到目标子表的 `sheet_id`。
| 用户意图 | 参考位置 |
|---|---|
| 读取在线表格完整内容(Markdown 概览) | 见下方「读取完整内容」 |
| 读取在线表格基础信息与子表列表 | 见下方「读取基础信息」 |
| 从零新建在线表格(空白) | 见下方「新建在线表格」 |
| 修改在线表格指定区域内容 | [references/sheet-update-range-data.md](references/sheet-update-range-data.md) |
| 在线表格末尾追加一行数据 | [references/sheet-append-data.md](references/sheet-append-data.md) |
| 添加在线表格子工作表 | [references/sheet-add-sub.md](references/sheet-add-sub.md) |
| 删除在线表格子工作表 | [references/sheet-delete-sub.md](references/sheet-delete-sub.md) |
## 接口详述
### 新建在线表格
从零新建一篇企微**在线表格**:空白。创建成功后返回 `docid``url`
**命令**
```bash
wecom-cli doc create_doc '<JSON 参数>'
```
**参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|--------|---|---|-------------|
| `doc_type` | int | 是 | — | 固定传 `4`(在线表格) |
| `doc_name` | string | 是 | — | 表格标题 |
**注意事项**
- 本接口仅创建**空白**在线表格,不支持携带初始内容;如需写入数据,请在创建后先用 `sheet_get_info` 拿到子表 `sheet_id`,再通过 `sheet_update_range_data` / `sheet_append_data` 写入。
### 读取完整内容
获取**在线表格**的完整内容数据,统一以 Markdown 格式返回。采用**异步轮询机制**:首次调用无需传 `task_id`,接口返回 `task_id`;若 `task_done``false`,需携带该 `task_id` 再次调用,直到 `task_done``true` 时返回完整内容。适合快速概览或读取整篇表格内容。
**命令**
```bash
wecom-cli doc get_doc_content '<JSON 参数>'
```
**参数**
| 字段 | 类型 | 必填 | 默认值 | 语义 |
|---|---|---|---|---|
| `docid` | string | 与 `url` 二选一 | — | 在线表格的 docid |
| `url` | string | 与 `docid` 二选一 | — | 在线表格的访问链接 |
| `type` | int | 是 | — | 内容返回格式,固定传 `2`Markdown |
| `task_id` | string | 否 | — | 任务 ID,首次不传,轮询时填上次返回的 `task_id` |
**返回**
| 字段 | 类型 | 说明 |
|---|---|---|
| `content` | string | `task_done``true` 时返回的完整 Markdown 内容 |
| `task_id` | string | 任务 ID,未完成时用于下次轮询 |
| `task_done` | bool | 任务是否完成,`false` 时需携带 `task_id` 继续轮询 |
**使用规则**
- 首次调用不传 `task_id`;若 `task_done``false`,记录 `task_id` 后携带其再次调用,直到 `task_done``true``content`
### 读取基础信息
读取**在线表格**的基础信息,包括工作表列表、文档名称与访问链接。所有后续需要 `sheet_id` 的接口(`sheet_update_range_data` / `sheet_append_data` / `sheet_delete_sub`等)的 `sheet_id` 都从本接口返回的 `sheets[]` 中取。
**命令**
```bash
wecom-cli doc sheet_get_info '<JSON 参数>'
```
**参数**
| 字段 | 类型 | 必填 | 默认值 | 语义 |
|---|---|---|---|---|
| `docid` | string | 与 `url` 二选一 | — | 在线表格的 docid |
| `url` | string | 与 `docid` 二选一 | — | 在线表格的访问链接 |
**返回**
| 字段 | 类型 | 说明 |
|---|---|---|
| `sheets` | array | 工作表列表;每项含 `sheet_id` / `title` / `row_count` / `column_count` / `data_range` 等基础信息 |
| `url` | string | 文档访问链接 |
| `name` | string | 文档名称 |
**使用规则**
- `docid``url` 二选一,至少传其一。
- **读 → 写 链路**:当需要往具体子表写内容(`sheet_update_range_data` / `sheet_append_data`)时,应先用 `get_doc_content` 读取并识别出各子表的**标题**与**具体数据**,再调用本接口拿到 `sheets[]` 中每个子表的 `sheet_id` 与行列数(`row_count` / `column_count`);通过**子表标题与 `title` 匹配**确定目标 `sheet_id`,并结合行列数核对写入区域范围,从而打通「读 → 写」的完整链路。
## 跨技能依赖
| 依赖技能 | 典型协作场景 | 数据流向 |
|---|---|---|
| `wecomcli-contact` | 表格里需要写入人员信息时按姓名查 userid | `get_userlist` 查到 userid → 本 skill 写入 |
| `wecomcli-msg` | 用户要求把在线表格链接发给某人/某群 | 本 skill 新建后返回 `url``wecomcli-msg` 发送链接 |
@@ -0,0 +1,56 @@
# sheet sheet_add_sub API
向**在线表格**添加一个新的子工作表,返回新增子表信息(含 `sheet_id`)。
## 命令
```bash
wecom-cli doc sheet_add_sub '<JSON 参数>'
```
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| docid | string | 是 | 在线表格 ID |
| sheet | object | 是 | 子表信息 |
| sheet.title | string | 是 | 工作表名称 |
| sheet.row_count | int | 否 | 表格总行数 |
| sheet.column_count | int | 否 | 表格总列数 |
| index | int | 否 | 插入位置:`0` 表示插入到最后,`1` 表示插入到第一个位置 |
## 返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `sheet` | object | 新增的子表信息;含 `sheet_id`(唯一标识)/ `title` / `row_count` / `column_count` / `data_range`(新建时为空) |
## 请求示例
```json
{
"docid": "DOCID",
"sheet": {
"title": "新子表",
"row_count": 100,
"column_count": 26
},
"index": 0
}
```
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"sheet": {
"sheet_id": "SHEET_ID",
"title": "新子表",
"row_count": 100,
"column_count": 26,
"data_range": ""
}
}
```
@@ -0,0 +1,66 @@
# sheet sheet_append_data API
向**在线表格**的指定子工作表末尾自动追加一行数据,无需指定行号——数据将写到最末一行之后。适合逐行写入。
## 命令
```bash
wecom-cli doc sheet_append_data '<JSON 参数>'
```
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| docid | string | 是 | 在线表格 ID |
| sheet_id | string | 是 | 工作表 ID,通过 `sheet sheet_get_info` 获取 |
| row | object | 是 | 追加的一行数据 |
| row.values | array | 是 | 单元格数组,按列顺序排列 |
`row.values[]` 对象结构:
| 子字段 | 类型 | 说明 |
|---|---|---|
| `cell_value` | object | 单元格值,形如 `{"text": "<文本>"}` / `{"link": {"url": "<URL>", "text": "<显示文本>"}}` / `{"formula": "=SUM(A1,A2)"}` |
| `cell_format` | object | 单元格样式;传空对象 `{}` 表示默认样式 |
## 返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `row` | object | 写入的行数据,结构与入参 `row` 一致 |
## 请求示例
```json
{
"docid": "DOCID",
"sheet_id": "SHEET_ID",
"row": {
"values": [
{ "cell_value": { "text": "新任务" }, "cell_format": {} },
{ "cell_value": { "text": "李四" }, "cell_format": {} }
]
}
}
```
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"row": {
"values": [
{ "cell_value": { "text": "新任务" } },
{ "cell_value": { "text": "李四" } }
]
}
}
```
## 使用规则
- **逐行写入场景**:本接口会自动定位到子表最末一行之后追加;批量写不同区域请用 `sheet_update_range_data`
- **读 → 写 链路**:写入前先用 `get_doc_content` 识别出目标子表的**标题**与**具体数据**,再用 `sheet sheet_get_info` 拿到 `sheets[]`,按**子表标题匹配 `title`** 确定 `sheet_id`,并参考其 `column_count` 核对每行单元格数量与列对齐。
@@ -0,0 +1,40 @@
# sheet sheet_delete_sub API
根据 `docid``sheet_id` 删除**在线表格**的指定子工作表,**操作不可逆**。
## 命令
```bash
wecom-cli doc sheet_delete_sub '<JSON 参数>'
```
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| docid | string | 是 | 在线表格 ID |
| sheet_id | string | 是 | 要删除的工作表 ID,通过 `sheet sheet_get_info` 获取 |
## 请求示例
```json
{
"docid": "DOCID",
"sheet_id": "SHEET_ID"
}
```
## 响应示例
删除成功返回空对象(仅含公共字段):
```json
{
"errcode": 0,
"errmsg": "ok"
}
```
## 注意事项
- **操作不可逆**,删除前请通过 `sheet sheet_get_info` 确认目标 `sheet_id`
@@ -0,0 +1,97 @@
# sheet sheet_update_range_data API
修改**在线表格**指定区域的内容与格式,通过 `grid_data` 指定写入的起始位置与各单元格数据。适合批量写入某个区域。
## 命令
```bash
wecom-cli doc sheet_update_range_data '<JSON 参数>'
```
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| docid | string | 是 | 在线表格 ID |
| sheet_id | string | 是 | 工作表 ID,通过 `sheet sheet_get_info` 获取 |
| grid_data | object | 是 | 写入区域的数据 |
| grid_data.start_row | int | 是 | 起始行号,从 0 起 |
| grid_data.start_column | int | 是 | 起始列号,从 0 起 |
| grid_data.rows | array | 是 | 各行数据 |
`grid_data.rows[].values[]` 对象结构:
| 子字段 | 类型 | 说明 |
|---|---|---|
| `cell_value` | object | 单元格值,根据内容类型选择对应字段(见下表)|
| `cell_format` | object | 单元格样式;传空对象 `{}` 表示默认样式 |
`cell_value` 的字段必须按内容类型选择:
| 内容类型 | 必须使用的字段 | 示例 |
|---|---|---|
| 普通文本/数字 | `text` | `{"text": "张三"}``{"text": "100"}` |
| 超链接 | `link` | `{"link": {"url": "https://...", "text": "显示文本"}}` |
| 公式(以 `=` 开头) | `formula` | `{"formula": "=SUM(A1:A2)"}` |
> 公式必须用 `formula` 字段,禁止写成 `{"text": "=SUM(A1:A2)"}`
## 返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `grid_data` | object | 写入的数据,结构与入参 `grid_data` 一致 |
## 请求示例
```json
{
"docid": "DOCID",
"sheet_id": "SHEET_ID",
"grid_data": {
"start_row": 0,
"start_column": 0,
"rows": [
{
"values": [
{ "cell_value": { "text": "完成需求文档" }, "cell_format": {} },
{ "cell_value": { "text": "张三" }, "cell_format": {} }
]
},
{
"values": [
{ "cell_value": { "text": "合计" }, "cell_format": {} },
{ "cell_value": { "formula": "=SUM(B1:B1)" }, "cell_format": {} }
]
}
]
}
}
```
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"grid_data": {
"start_row": 0,
"start_column": 0,
"rows": [
{
"values": [
{ "cell_value": { "text": "完成需求文档" } },
{ "cell_value": { "text": "张三" } }
]
}
]
}
}
```
## 使用规则
- 批量写不同区域用本接口;逐行追加到子表末尾请用 `sheet_append_data`
- 公式一律用 `formula` 字段:任何以 `=` 开头的内容(如 `=SUM(...)``=A1+B1``=IF(...)`)都必须写成 `{"formula": "=..."}`,不可写成 `{"text": "=..."}`,否则只会被当作文本显示而不计算。
- **读 → 写 链路**:写入前先用 `get_doc_content` 识别出目标子表的**标题**与**具体数据**,再用 `doc sheet_get_info` 拿到 `sheets[]`,按**子表标题匹配 `title`** 确定 `sheet_id`,并参考其 `row_count` / `column_count` 核对 `grid_data` 的写入区域是否越界。
+111
View File
@@ -0,0 +1,111 @@
---
name: wecomcli-smartpage
description: 企业微信智能文档(原名智能主页,smartpage)管理技能。提供智能文档的创建(将本地 Markdown 文件发布为智能文档)与内容导出(异步导出为 Markdown)能力。适用场景:(1) 将一个或多个本地 Markdown 文件创建为智能文档 (2) 异步导出智能文档内容为 Markdown。支持通过 docid 或文档 URL 定位文档。当用户明确提到「智能文档」「智能主页」,或链接形如 `https://doc.weixin.qq.com/smartpage/xxx` 时触发该技能。注意:普通文档(`/doc/*`)请用 `wecomcli-doc`;在线表格(`/sheet/*`)请用 `wecomcli-sheet`;智能表格(`/smartsheet/*`)请用 `wecomcli-smartsheet`
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli doc --help"
---
# 企业微信智能文档管理
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
资源型技能,负责**智能文档**(原名智能主页,`/smartpage/*`)的创建与内容导出。
## 调用方式
通过 `wecom-cli` 调用,品类为 `doc`
```bash
wecom-cli doc <tool_name> '<json_params>'
```
## 返回格式说明
所有接口返回 JSON 对象,包含以下公共字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功,非 `0` 表示失败 |
| `errmsg` | string | 错误信息,成功时为 `"ok"` |
`errcode` 不为 `0` 时,说明接口调用失败,可重试 1 次;若仍失败,将 `errcode``errmsg` 展示给用户。
### 特殊错误码
| errcode | errmsg | 含义 | 处理方式 |
|---------|--------|------|----------|
| `851002` | `incompatible doc type` | 文档品类与所调用的接口不匹配 | 确认目标 URL 为 `/smartpage/*`;若不是,请跳转到对应品类的 skill |
## 接口详述
### 创建智能文档
创建智能文档(原名智能主页),支持传入标题和多个子页面。每个子页面可指定标题、内容类型和本地文件路径。创建成功返回 `docid``url`
> **特殊语法**:此命令必须使用 `+smartpage_create`(带 `+` 前缀),加号不可省略;该 `+` 仅适用于此命令,不要泛化到其他 `doc` 子命令。
**命令**
```bash
wecom-cli doc +smartpage_create '<JSON 参数>'
```
**参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `title` | string | 否 | — | 智能文档标题 |
| `pages` | array | 是 | — | 子页面列表 |
| `pages[].page_title` | string | 否 | — | 子页面标题 |
| `pages[].content_type` | int | 否 | 1 | 内容类型:1-Markdown0-Text(纯文本) |
| `pages[].page_filepath` | string | 否 | — | 子页面内容对应的本地文件路径 |
**注意事项**
- `content_type` **必须与文件实际内容匹配**`.md` 文件或包含 Markdown 语法的内容必须传 `1`,仅纯文本才传 `0`。绝大多数场景应传 `1`
- `docid` 仅在创建时返回,需妥善保存。
- 每个子页面的 Markdown 文件大小不得超过 **10MB**,超过会导致创建失败;如文件过大,需先拆分为多个子页面再创建。
- 智能文档还支持背景块(`<card>`)、分栏(`<grid>`)等扩展语法,详见 [references/smartpage-create.md](references/smartpage-create.md)。
### 导出智能文档内容
获取智能文档的完整内容,导出为 Markdown。采用**异步两步操作**:先用 `smartpage_export_task` 提交导出任务拿到 `task_id`,再用 `smartpage_get_export_result` 轮询任务,直到 `task_done``true` 时返回 `content`
**第一步:提交导出任务**
```bash
# 通过 docid
wecom-cli doc smartpage_export_task '{"docid": "DOCID", "content_type": 1}'
# 通过 url
wecom-cli doc smartpage_export_task '{"url": "https://doc.weixin.qq.com/smartpage/xxx", "content_type": 1}'
```
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `docid` | string | 与 `url` 二选一 | — | 智能文档的 docid |
| `url` | string | 与 `docid` 二选一 | — | 智能文档的访问链接 |
| `content_type` | int | 是 | — | 导出内容格式,目前仅支持 `1`Markdown |
**第二步:轮询导出结果**
```bash
wecom-cli doc smartpage_get_export_result '{"task_id": "TASK_ID"}'
```
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `task_id` | string | 是 | — | 由 `smartpage_export_task` 返回的任务 ID |
**使用规则**
- 第一步获取 `task_id` 后,携带其调用第二步;若 `task_done``false` 则继续轮询,直到 `task_done``true`,返回的 `content` 字段即为完整 Markdown 内容。
参见 [API 详情](references/smartpage-export.md)。
## 跨技能依赖
| 依赖技能 | 典型协作场景 | 数据流向 |
|---|---|---|
| `wecomcli-msg` | 用户要求把智能文档链接发给某人/某群 | 本 skill 创建后返回 `url``wecomcli-msg` 发送链接 |
@@ -0,0 +1,134 @@
# smartpage_create API
创建智能文档(原智能主页)。支持传入多个子页面,每个子页面可指定标题、内容类型和本地文件路径。创建成功后返回文档访问链接和 docid。
> ⚠️ **特殊语法**:此命令必须使用 `+smartpage_create`(带 `+` 前缀),加号不可省略;该 `+` 仅适用于此命令,不要泛化到其他 `doc` 子命令。
## 命令
```bash
wecom-cli doc +smartpage_create '<JSON 参数>'
```
## 技能定义
```json
{
"name": "smartpage_create",
"description": "创建智能文档(原智能主页)。支持传入标题和多个子页面配置,每个子页面可指定标题、内容类型(Text/Markdown)和本地文件路径。创建成功后返回 docid 和 url(docid 仅在创建时返回,需妥善保存)。",
"inputSchema": {
"properties": {
"title": {
"description": "智能文档标题",
"title": "Title",
"type": "string"
},
"pages": {
"description": "子页面列表",
"title": "Pages",
"type": "array",
"items": {
"type": "object",
"properties": {
"page_title": {
"description": "子页面标题",
"title": "Page Title",
"type": "string"
},
"content_type": {
"description": "内容类型。1: Markdown(包含Markdown语法的内容),0: Text(纯文本,不含任何Markdown语法)",
"enum": [0, 1],
"default": 1,
"title": "Content Type",
"type": "integer"
},
"page_filepath": {
"description": "子页面内容对应的本地文件路径",
"title": "Page Filepath",
"type": "string"
}
}
}
}
},
"required": ["pages"],
"title": "smartpage_createArguments",
"type": "object"
}
}
```
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| title | string | 否 | 智能文档标题 |
| pages | array | 是 | 子页面列表 |
| pages[].page_title | string | 否 | 子页面标题 |
| pages[].content_type | integer | 否 | 内容类型:1-Markdown0-Text(纯文本)。**默认应传 1**,仅纯文本内容才传 0 |
| pages[].page_filepath | string | 否 | 子页面内容对应的本地文件路径,需确保文件存在且可读 |
## ContentType 枚举
| 值 | 含义 | 适用场景 |
|---|---|---|
| 1 | Markdown | 文件内容包含 Markdown 语法(标题、列表、链接、代码块等) |
| 0 | Text(纯文本) | 文件内容为纯文本,不含任何 Markdown 语法 |
除了标准的 markdown 格式以外,智能文档还支持扩展语法以提升表示的丰富性,包括:
1. 背景块
```markdown
<card color="green">
## 在扩展标签里面可以任意嵌套 markdown 语法
- 背景块常用于展示重要信息
- 颜色的使用根据需要表达的语义进行选择,卡片背景由 `color` 指定;支持 `green`, `blue`, `red`, `yellow`, `gray`, `purple`, `orange`, `cyan`, `indigo`,也支持 `dark_green`, `dark_blue`, `dark_red`, `dark_yellow`, `dark_gray`, `dark_purple`, `dark_orange`, `dark_cyan`, `dark_indigo` 等深色系。
</card>
```
背景颜色推荐使用浅色背景,以完成区隔/高亮并且保持低饱和度确保正文内容的良好显示。
2. 分栏
使用分栏可以并列显示内容,常用于展示对比或者并列信息
```markdown
<grid>
<area width-ratio="0.5">占据50%的空间</area>
<area width-ratio="0.5">占据50%的空间</area>
</grid>
```
width-ratio:子容器宽度占比,范围 0.1~1.0,所有的子容器宽度占比之和为 1
## 请求示例
```json
{
"title": "项目概览",
"pages": [
{
"page_title": "需求文档",
"content_type": 1,
"page_filepath": "/path/to/requirements.md"
},
{
"page_title": "设计说明",
"content_type": 1,
"page_filepath": "/path/to/design.md"
}
]
}
```
## 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"docid": "DOCID",
"url": "https://doc.weixin.qq.com/smartpage/a1_xxxxxx"
}
```
## 注意事项
- `docid` 仅在创建时返回,后续无法再获取,务必保存
- `page_filepath` 指向本地文件,需确保文件存在且可读
- **`content_type` 必须与文件实际内容格式匹配**`.md` 文件或包含 Markdown 语法的内容必须传 `1`,不要传 `0`
- 每个子页面的 Markdown 文件大小不得超过 **10MB**,超过会导致创建失败;如果文件过大,需先拆分为多个子页面
@@ -0,0 +1,171 @@
# smartpage_export_task / smartpage_get_export_result API
导出智能文档(原智能主页)内容。采用异步两步操作:先通过 `smartpage_export_task` 提交导出任务获取 `task_id`,再通过 `smartpage_get_export_result` 轮询任务状态,直到任务完成后返回完整文档内容。
---
## 第一步:smartpage_export_task — 提交导出任务
发起智能文档内容导出任务(异步)。传入 `docid``url``content_type`,返回 `task_id`
### 命令
```bash
wecom-cli doc smartpage_export_task '<JSON 参数>'
```
### 技能定义
```json
{
"name": "smartpage_export_task",
"description": "发起智能文档(原智能主页)内容导出任务(异步)。传入 docid(或 url)和 content_type,返回 task_id。需配合 smartpage_get_export_result 轮询查询导出进度,直到任务完成后获取文档内容。",
"inputSchema": {
"properties": {
"docid": {
"description": "智能文档的 docid,与 url 二选一传入",
"title": "Doc ID",
"type": "string"
},
"url": {
"description": "智能文档的访问链接,与 docid 二选一传入",
"title": "URL",
"type": "string"
},
"content_type": {
"description": "导出内容格式。目前仅支持 1Markdown 格式)",
"enum": [1],
"title": "Content Type",
"type": "integer"
}
},
"oneOf": [
{ "required": ["docid", "content_type"] },
{ "required": ["url", "content_type"] }
],
"title": "smartpage_export_taskArguments",
"type": "object"
}
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| docid | string | 与 url 二选一 | 智能文档的 docid |
| url | string | 与 docid 二选一 | 智能文档的访问链接 |
| content_type | integer | 是 | 导出内容格式,目前仅支持 `1`Markdown 格式) |
### 请求示例
```json
// 通过 docid
{
"docid": "DOCID",
"content_type": 1
}
// 通过 url
{
"url": "https://doc.weixin.qq.com/smartpage/a1_xxxxxx",
"content_type": 1
}
```
### 响应示例
```json
{
"errcode": 0,
"errmsg": "ok",
"task_id": "TASK_ID"
}
```
---
## 第二步:smartpage_get_export_result — 查询导出结果
查询智能文档导出任务进度。传入 `task_id` 进行轮询,当 `task_done``true` 时返回完整文档内容。
### 命令
```bash
wecom-cli doc smartpage_get_export_result '<JSON 参数>'
```
### 技能定义
```json
{
"name": "smartpage_get_export_result",
"description": "查询智能文档(原智能主页)导出任务进度。传入 task_id 轮询,当 task_done 为 true 时返回 content 字段,包含导出的完整文档内容。",
"inputSchema": {
"properties": {
"task_id": {
"description": "导出任务 ID,由 smartpage_export_task 返回",
"title": "Task ID",
"type": "string"
}
},
"required": ["task_id"],
"title": "smartpage_get_export_resultArguments",
"type": "object"
}
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| task_id | string | 是 | 导出任务 ID,由 `smartpage_export_task` 返回 |
### 请求示例
```json
{
"task_id": "TASK_ID"
}
```
### 响应示例
任务未完成:
```json
{
"errcode": 0,
"errmsg": "ok",
"task_done": false
}
```
任务完成:
```json
{
"errcode": 0,
"errmsg": "ok",
"task_done": true,
"content": "# 项目周报\n\n## 本周进展\n\n1. 完成了用户模块开发\n2. 修复了3个线上Bug"
}
```
---
## 异步轮询机制
1. **调用 smartpage_export_task**:传入 `docid`(或 `url`)和 `content_type: 1`,获取 `task_id`
2. **首次轮询**:传入 `task_id` 调用 `smartpage_get_export_result`
3. **检查响应**:若 `task_done``false`,继续轮询
4. **获取内容**:当 `task_done``true` 时,`content` 字段包含完整的 Markdown 内容
## 注意事项
- `smartpage_export_task` 是异步操作的第一步,调用后仅返回 `task_id`
- `content_type` 目前仅支持 `1`Markdown 格式)
- `docid``url` 二选一传入即可,无需同时传入
- 任务完成后 `content` 字段直接包含完整文档内容,无需额外读取文件
- 如果轮询多次仍未完成,建议适当增加轮询间隔
+266
View File
@@ -0,0 +1,266 @@
---
name: wecomcli-smartsheet
description: 企业微信智能表格(smartsheet)管理技能。提供智能表格的新建(doc_type=10)、结构管理(子表、字段/列)和数据管理(记录增删改查)。适用场景:(1) 从零新建智能表格 (2) 管理智能表格子表和字段/列 (3) 查询、添加、更新、删除智能表格记录。支持通过 docid 或文档 URL 定位文档。当用户提到「企业微信智能表格」「智能表格」,或链接形如 `https://doc.weixin.qq.com/smartsheet/xxx` 时触发该技能。注意:普通文档(`/doc/*`)请用 `wecomcli-doc`;在线表格(`/sheet/*`)请用 `wecomcli-sheet`;智能文档/智能主页(`/smartpage/*`)请用 `wecomcli-smartpage`
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli doc --help"
---
# 企业微信智能表格管理
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
资源型技能,负责**智能表格**`/smartsheet/*`doc_type=10)的新建、结构(子表、字段/列)与数据(记录)管理。所有接口支持通过 `docid``url` 二选一定位文档。
## 调用方式
通过 `wecom-cli` 调用,品类为 `doc`
```bash
wecom-cli doc <tool_name> '<json_params>'
```
> 智能表格各接口的 `docid`/`url` 二选一传入即可,以下示例以 `docid` 为主,URL 传入方式以此类推。
## 返回格式说明
所有接口返回 JSON 对象,包含以下公共字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `errcode` | integer | 返回码,`0` 表示成功,非 `0` 表示失败 |
| `errmsg` | string | 错误信息,成功时为 `"ok"` |
`errcode` 不为 `0` 时,说明接口调用失败,可重试 1 次;若仍失败,将 `errcode``errmsg` 展示给用户。
### 特殊错误码
| errcode | errmsg | 含义 | 处理方式 |
|---------|--------|------|----------|
| `851002` | `incompatible doc type` | 文档品类与所调用的接口不匹配 | 确认目标 URL 为 `/smartsheet/*`;若不是,请跳转到对应品类的 skill |
## 接口路由表
> **硬规则**:第二列是 `references/xxx.md` 链接的,命中这一行后**先 `read` 对应 references 文件,再构造命令**。写入/读取记录前,先用 `smartsheet_get_sheet` 拿到目标子表的 `sheet_id`,并用 `smartsheet_get_fields` 了解字段类型。
| 用户意图 | 参考位置 |
|---|---|
| 从零新建智能表格(空白) | 见下方「新建智能表格」 |
| 查询文档中所有子表 | 见下方「查询子表」 |
| 添加 / 改名 / 删除子表 | 见下方「子表管理」 |
| 查询子表字段/列 | 见下方「查询字段」 |
| 添加字段/列 | [references/smartsheet-field-types.md](references/smartsheet-field-types.md) |
| 改名 / 删除字段 | 见下方「字段管理」 |
| 查询子表记录 | [references/smartsheet-get-records.md](references/smartsheet-get-records.md) |
| 添加记录 / 更新记录 | [references/smartsheet-cell-value-formats.md](references/smartsheet-cell-value-formats.md) |
| 删除记录 | 见下方「删除记录」 |
---
## 一、新建智能表格
### 新建智能表格
从零新建一篇企微**智能表格**(doc_type=10):空白。创建成功后返回 `docid``url`
**命令**
```bash
wecom-cli doc create_doc '<JSON 参数>'
```
**参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `doc_type` | int | 是 | — | 固定传 `10`(智能表格) |
| `doc_name` | string | 是 | — | 表格标题,最多 255 个字符,超过会被截断 |
**注意事项**
- 新建智能表格文档**默认已含一个子表**,可通过 `smartsheet_get_sheet` 查询其 `sheet_id`,仅需多个子表时才调用 `smartsheet_add_sheet`
- `docid` 仅在创建时返回,后续无法再获取,务必保存。
---
## 二、智能表格结构管理
### 查询子表
查询文档中所有子表信息,返回 `sheet_id``title`、类型等。
```bash
# 通过 docid 查询
wecom-cli doc smartsheet_get_sheet '{"docid": "DOCID"}'
# 通过 url 查询
wecom-cli doc smartsheet_get_sheet '{"url": "https://doc.weixin.qq.com/smartsheet/xxx"}'
```
### 子表管理
**添加子表** —— 添加空子表。新子表不含视图、记录和字段,需通过其他接口补充。
```bash
wecom-cli doc smartsheet_add_sheet '{"docid": "DOCID", "properties": {"title": "新子表"}}'
```
> 注意:新建智能表格文档默认已含一个子表,仅需多个子表时调用。
**修改子表标题** —— 需提供 `sheet_id` 和新 `title`
```bash
wecom-cli doc smartsheet_update_sheet '{"docid": "DOCID", "properties":{"sheet_id":"SHEET_ID", "title":"新子表"}}'
```
**删除子表** —— 永久删除子表,**操作不可逆**。
```bash
wecom-cli doc smartsheet_delete_sheet '{"docid": "DOCID", "sheet_id": "SHEETID"}'
```
### 查询字段
查询子表的所有字段信息,返回 `field_id``field_title``field_type`
```bash
wecom-cli doc smartsheet_get_fields '{"docid": "DOCID", "sheet_id": "SHEETID"}'
```
### 字段管理
**添加字段** —— 向子表添加一个或多个字段。单个子表最多 150 个字段。
```bash
wecom-cli doc smartsheet_add_fields '{"docid": "DOCID", "sheet_id": "SHEETID", "fields": [{"field_title": "任务名称", "field_type": "FIELD_TYPE_TEXT"}]}'
```
在添加字段前,请先参阅所有字段类型和定义 [字段类型参考](references/smartsheet-field-types.md)。
> 注意:如果是首次创建表并调用这个方法添加字段的情况下,调用本接口前,你必须确认已完成以下操作,否则会多出一个无用的默认列:
> 1. 已调用 `smartsheet_get_fields` 查看子表现有字段(新子表会自带一个默认文本字段)
> 2. 已调用 `smartsheet_update_fields` 将该默认字段重命名为你需要的第一个字段名,然后在本接口中只传入剩余的字段(不包含第一个字段)。
**更新字段标题** —— **只能改名,不能改类型**`field_type` 必须传原始类型)。`field_title` 不能更新为原值。
```bash
wecom-cli doc smartsheet_update_fields '{"docid": "DOCID", "sheet_id": "SHEETID", "fields": [{"field_id": "FIELDID", "field_title": "新标题", "field_type": "FIELD_TYPE_TEXT"}]}'
```
**删除字段** —— 删除一列或多列字段,**操作不可逆**。`field_id` 可通过 `smartsheet_get_fields` 获取。
```bash
wecom-cli doc smartsheet_delete_fields '{"docid": "DOCID", "sheet_id": "SHEETID", "field_ids": ["FIELDID"]}'
```
---
## 三、智能表格数据管理
### 查询记录
查询子表全部记录。
```bash
# 通过 docid
wecom-cli doc smartsheet_get_records '{"docid": "DOCID", "sheet_id": "SHEETID"}'
# 或通过 URL
wecom-cli doc smartsheet_get_records '{"url": "https://doc.weixin.qq.com/smartsheet/xxx", "sheet_id": "SHEETID"}'
```
参见 [API 详情](references/smartsheet-get-records.md)。
### 添加记录(不带图片或文件)
添加一行或多行记录,单次建议 500 行内。
**调用前**必须先了解目标表的字段类型(通过 `smartsheet_get_fields`),重点关注 `field_type`。对于单选/多选(Option)字段,需注意匹配已有选项的 `id`
```bash
wecom-cli doc smartsheet_add_records '{"docid": "DOCID", "sheet_id": "SHEETID", "records": [{"values": {"任务名称": [{"type": "text", "text": "完成需求文档"}], "优先级": [{"text": "高"}]}}]}'
```
各字段类型的值格式参见 [单元格值格式参考](references/smartsheet-cell-value-formats.md)。
### 添加记录(带图片或文件)
添加一行或多行记录,单次建议 500 行内。与 `smartsheet_add_records` 不同之处在于,可支持本地路径传入图片、文件。对于需要添加带图片或文件的记录,请使用此接口。传入后台后,后台将自动存储并转换为 `image_url`
```bash
wecom-cli doc +smartsheet_add_records_auto_file '{"docid":"DOCID","sheet_id":"SHEETID","records":[{"values":{"图片":[{"image_path":"/path/to/image.jpg"}],"文件":[{"file_path":"/path/to/file.txt"}]}}]}'
```
### 更新记录(不带图片或文件)
更新一行或多行记录,单次建议在 500 行内。需提供 `record_id`(通过 `smartsheet_get_records` 获取)。支持通过 `key_type` 指定 values 的 key 使用字段标题或字段 ID:
- `CELL_VALUE_KEY_TYPE_FIELD_TITLE`key 为字段标题
- `CELL_VALUE_KEY_TYPE_FIELD_ID`key 为字段 ID
```bash
wecom-cli doc smartsheet_update_records '{"docid": "DOCID", "sheet_id": "SHEETID", "key_type": "CELL_VALUE_KEY_TYPE_FIELD_TITLE", "records": [{"record_id": "RECORDID", "values": {"任务名称": [{"type": "text", "text": "更新后的内容"}]}}]}'
```
**注意**:创建时间、最后编辑时间、创建人、最后编辑人字段不可更新。
### 更新记录(更新图片或文件字段)
更新一行或多行记录,单次建议在 500 行内。与 `smartsheet_update_records` 不同之处在于,可支持本地路径传入图片、文件。对于需要更新记录中的图片或文件,请使用此接口。传入后台后,后台将自动存储并转换为 `image_url`
```bash
wecom-cli doc +smartsheet_update_records_auto_file '{"docid": "DOCID", "sheet_id": "SHEETID", "key_type": "CELL_VALUE_KEY_TYPE_FIELD_TITLE", "records": [{"record_id": "RECORDID", "values": {"values":{"图片":[{"image_path":"/path/to/image.jpg"}],"文件":[{"file_path":"/path/to/file.txt"}]}}}]}'
```
### 删除记录
删除一行或多行记录,单次必须在 500 行内。**操作不可逆**。`record_id` 通过 `smartsheet_get_records` 获取。极速版智能表格不支持此接口。
```bash
wecom-cli doc smartsheet_delete_records '{"docid": "DOCID", "sheet_id": "SHEETID", "record_ids": ["RECORDID1", "RECORDID2"]}'
```
---
## 典型工作流
### 新建并搭建表结构
1. **新建智能表格**
```bash
wecom-cli doc create_doc '{"doc_type": 10, "doc_name": "项目任务表"}'
```
,保存返回的 `docid`
2. **了解默认子表**`smartsheet_get_sheet` 拿到默认子表的 `sheet_id``smartsheet_get_fields` 查看默认字段。
3. **搭建列** → 先 `smartsheet_update_fields` 改默认字段名,再 `smartsheet_add_fields` 补充其余字段。
### 智能表格结构操作
1. **了解表结构**
```bash
wecom-cli doc smartsheet_get_sheet '{"docid": "DOCID"}'
```
```bash
wecom-cli doc smartsheet_get_fields '{"docid": "DOCID", "sheet_id": "SHEETID"}'
```
2. **创建表结构**`smartsheet_add_sheet` 添加子表 → `smartsheet_add_fields` 定义列
3. **修改表结构**`smartsheet_update_fields` 改列名 / `smartsheet_delete_fields` 删列
### 智能表格数据操作
1. **读取数据**
```bash
wecom-cli doc smartsheet_get_records '{"docid":"DOCID","sheet_id":"SHEETID"}'
```
2. **写入数据** → 先 `smartsheet_get_fields` 了解列类型 → 若涉及成员(USER)字段,先通过 `wecomcli-contact``get_userlist` 查找人员 userid → `smartsheet_add_records` 写入
3. **更新数据** → 先 `smartsheet_get_records` 获取 record_id → 若涉及成员(USER)字段,先通过 `wecomcli-contact``get_userlist` 查找人员 userid → `smartsheet_update_records` 更新
4. **删除数据** → 先 `smartsheet_get_records` 确认 record_id → `smartsheet_delete_records` 删除
## 跨技能依赖
| 依赖技能 | 典型协作场景 | 数据流向 |
|---|---|---|
| `wecomcli-contact` | 成员(USER)类型字段需填 `user_id`,不能直接用姓名 | `get_userlist` 按姓名查到 userid → 本 skill 写入 |
| `wecomcli-msg` | 用户要求把智能表格链接发给某人/某群 | 本 skill 新建后返回 `url``wecomcli-msg` 发送链接 |
> **注意**:成员(USER)类型字段需要填写 `user_id`,不能直接使用姓名。必须先通过 `wecomcli-contact` 技能的 `get_userlist` 接口按姓名查找到对应的 `userid` 后再使用。
@@ -0,0 +1,164 @@
# 单元格值格式参考
`smartsheet_add_records``+smartsheet_add_records_auto_file` 仅支持**字段标题**作为key。
`smartsheet_update_records``+smartsheet_update_records_auto_file` 支持通过 `key_type` 参数指定使用字段标题(`CELL_VALUE_KEY_TYPE_FIELD_TITLE`)或字段 ID`CELL_VALUE_KEY_TYPE_FIELD_ID`)。
## 各字段类型的值格式
### 1. 文本 (FIELD_TYPE_TEXT)
**必须**使用数组格式,外层方括号不可省略:
```json
"字段标题": [{"type": "text", "text": "内容"}]
```
### 2. 数字 (NUMBER) / 货币 (CURRENCY) / 百分比 (PERCENTAGE) / 进度 (PROGRESS)
直接传数字:
```json
"金额": 100,
"完成率": 0.6,
"进度": 80
```
### 3. 复选框 (CHECKBOX)
直接传布尔值:
```json
"已完成": true
```
### 4. 单选 (SINGLE_SELECT) / 多选 (SELECT)
**必须**使用数组格式,不能直接传字符串:
```json
"优先级": [{"text": "高"}],
"标签": [{"text": "紧急", "style": 17}, {"text": "重要", "style": 12}]
```
已存在的选项应通过 `id` 匹配(`id` 可从 `smartsheet_get_fields` 返回中获取),新增选项时不填 `id`。可附带 `style`(颜色 1-27),对照表如下:
| style | 颜色 |
|-------|------|
| 1 | 浅红1 |
| 2 | 浅橙1 |
| 3 | 浅天蓝1 |
| 4 | 浅绿1 |
| 5 | 浅紫1 |
| 6 | 浅粉红1 |
| 7 | 浅灰1 |
| 8 | 白 |
| 9 | 灰 |
| 10 | 浅蓝1 |
| 11 | 浅蓝2 |
| 12 | 蓝 |
| 13 | 浅天蓝2 |
| 14 | 天蓝 |
| 15 | 浅绿2 |
| 16 | 绿 |
| 17 | 浅红2 |
| 18 | 红 |
| 19 | 浅橙2 |
| 20 | 橙 |
| 21 | 浅黄1 |
| 22 | 浅黄2 |
| 23 | 黄 |
| 24 | 浅紫2 |
| 25 | 紫 |
| 26 | 浅粉红2 |
| 27 | 粉红 |
### 5. 日期时间 (DATE_TIME)
传日期时间字符串,系统自动按东八区转换:
```json
"截止日期": "2026-01-15 14:30:00",
"创建日期": "2026-01-15"
```
支持格式:`YYYY-MM-DD HH:mm:ss``YYYY-MM-DD HH:mm``YYYY-MM-DD`
### 6. 手机号 (PHONE_NUMBER) / 邮箱 (EMAIL) / 条码 (BARCODE)
直接传字符串:
```json
"电话": "13800138000",
"邮箱": "test@example.com"
```
### 7. 成员 (USER)
数组格式,需传 user_id。**user_id 不是姓名**,必须先通过 `wecomcli-contact` 技能查找目标人员的 `userid`,再填入此处。
具体步骤:先
```bash
wecom-cli contact get_userlist '{}'
```
获取通讯录成员列表,在返回结果中按姓名/别名筛选出目标人员,取其 `userid` 值填入。
```json
"负责人": [{"user_id": "zhangsan"}]
```
多个成员:
```json
"负责人": [{"user_id": "zhangsan"}, {"user_id": "lisi"}]
```
### 8. 超链接 (URL)
数组格式,目前仅支持一个链接:
```json
"参考链接": [{"type": "url", "text": "官网", "link": "https://example.com"}]
```
### 9. 图片 (IMAGE)
数组格式,支持传入本地路径:
```json
"封面": [{"image_path": "/path/to/img.png"}]
```
### 10. 地理位置 (LOCATION)
数组格式:
```json
"地点": [{"source_type": 1, "id": "地点ID", "latitude": "39.9", "longitude": "116.3", "title": "北京"}]
```
### 11. 文件
数组格式:
```json
"文件": [{"file_path": "/path/to/img.png"}]
```
## 完整添加记录示例
```json
{
"docid": "DOCID",
"sheet_id": "SHEETID",
"records": [{
"values": {
"任务名称": [{"type": "text", "text": "完成需求文档"}],
"优先级": [{"text": "高"}],
"截止日期": "2026-03-20",
"完成进度": 30,
"已完成": false
}
}]
}
```
@@ -0,0 +1,44 @@
# 智能表格字段类型参考
## 支持的字段类型
| 类型枚举值 | 说明 | 适用场景 |
|---|---|---|
| `FIELD_TYPE_TEXT` | 文本 | 名称、标题、描述、负责人姓名等自由文本 |
| `FIELD_TYPE_NUMBER` | 数字 | 金额、工时、数量等数值 |
| `FIELD_TYPE_CHECKBOX` | 复选框 | 是否完成等布尔值 |
| `FIELD_TYPE_DATE_TIME` | 日期时间 | 截止日期、创建时间等 |
| `FIELD_TYPE_IMAGE` | 图片 | 附件图片 |
| `FIELD_TYPE_USER` | 用户/成员 | 需传入 user_id;仅在明确知道成员 ID 时使用,若只有姓名应用 TEXT |
| `FIELD_TYPE_URL` | 链接 | 超链接 |
| `FIELD_TYPE_SELECT` | 多选 | 标签、分类等可多选的选项 |
| `FIELD_TYPE_PROGRESS` | 进度 | 完成进度(0-100 整数) |
| `FIELD_TYPE_PHONE_NUMBER` | 手机号 | 联系电话 |
| `FIELD_TYPE_EMAIL` | 邮箱 | 电子邮件 |
| `FIELD_TYPE_SINGLE_SELECT` | 单选 | 状态、优先级、严重程度等有固定选项的字段 |
| `FIELD_TYPE_LOCATION` | 位置 | 地理位置 |
| `FIELD_TYPE_CURRENCY` | 货币 | 货币金额 |
| `FIELD_TYPE_PERCENTAGE` | 百分比 | 比率类数值(完成率、转化率) |
| `FIELD_TYPE_BARCODE` | 条码 | 条形码/二维码 |
| `FIELD_TYPE_ATTACHMENT` | 文件 | 文件/附件 |
## 添加字段示例
```json
{
"docid": "DOCID",
"sheet_id": "SHEETID",
"fields": [
{ "field_title": "任务名称", "field_type": "FIELD_TYPE_TEXT" },
{ "field_title": "优先级", "field_type": "FIELD_TYPE_SINGLE_SELECT" },
{ "field_title": "截止日期", "field_type": "FIELD_TYPE_DATE_TIME" },
{ "field_title": "完成进度", "field_type": "FIELD_TYPE_PROGRESS" }
]
}
```
## 更新字段注意事项
- `smartsheet_update_fields` **只能更新字段标题**,不能更改字段类型
- `field_type` 必须传字段当前的原始类型
- `field_title` 不能更新为原值(即不能传与当前相同的标题)
@@ -0,0 +1,187 @@
# smartsheet_get_records API
查询智能表格中指定子表的记录信息,支持分页读取。支持通过 docid 或文档 URL 定位文档,二者传入其一即可。
## 技能定义
```json
{
"name": "smartsheet_get_records",
"description": "查询智能表格中指定子表的记录信息。支持分页查询(cursor + limit),不填 cursor 从第一行开始。支持通过 docid 或文档 URL 定位文档,二者传入其一即可。",
"inputSchema": {
"properties": {
"cursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "查询游标,不填代表从第一行记录开始查询",
"title": "Cursor"
},
"docid": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "文档的 docid,与 url 二选一传入",
"title": "Docid"
},
"limit": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "分页大小,每页返回多少条数据;当不填写该参数或将该参数设置为 0 时,如果总数大于 1000,一次性返回 1000 行记录,当总数小于 1000 时,返回全部记录;limit 最大值为 1000",
"title": "Limit"
},
"sheet_id": {
"description": "子表的 sheet_id,用于指定要查询的智能表格中的哪个子表",
"title": "Sheet Id",
"type": "string"
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "文档的访问链接,与 docid 二选一传入",
"title": "Url"
}
},
"required": [
"sheet_id"
],
"title": "smartsheet_get_recordsArguments",
"type": "object"
}
}
```
## 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cursor | string | 否 | 查询游标,不填代表从第一行记录开始查询 |
| docid | string | 与 url 二选一 | 文档的 docid |
| limit | integer | 否 | 分页大小,每页返回多少条数据;当不填写该参数或将该参数设置为 0 时,如果总数大于 1000,一次性返回 1000 行记录,当总数小于 1000 时,返回全部记录;limit 最大值为 1000 |
| sheet_id | string | 是 | 子表的 sheet_id,用于指定要查询的智能表格中的哪个子表 |
| url | string | 与 docid 二选一 | 文档的访问链接 |
## 请求示例
以传入docid为例:
```json
{
"docid": "DOCID",
"sheet_id": "123Abc"
}
```
以传url为例:
```json
{
"url": "https://doc.weixin.qq.com/smartsheet/xxx",
"sheet_id": "123Abc"
}
```
## 响应示例(含分页)
```json
{
"errcode": 0,
"errmsg": "ok",
"total": 100,
"has_more": true,
"next_cursor": "mock_cursor_token",
"records": [
{
"record_id": "rec_001",
"create_time": "1700000000000",
"update_time": "1700000000000",
"values": {
"成员": [
{"user_id": "real.user001", "id_type": 1}
],
"序号": [
{"text": "1", "type": "text"}
],
"附件": [
{
"doc_type": 2,
"file_ext": "xlsx",
"file_type": "Wedrive",
"file_url": "https://drive.weixin.qq.com/s?k=MOCK_TOKEN",
"name": "report.xlsx",
"size": 12345
}
]
},
"creator_name": "张三",
"updater_name": "张三"
},
{
"record_id": "rec_002",
"create_time": "1700000000000",
"update_time": "1700000000000",
"values": {
"截图": [
{
"height": 1080,
"id": "img_mock_id",
"image_url": "https://wdcdn.qpic.cn/mocked_path?w=1920&h=1080",
"title": "screenshot_001",
"width": 1920
}
],
"序号": [
{"text": "2", "type": "text"}
]
},
"creator_name": "张三",
"updater_name": "李四"
}
]
}
```
## 响应数据结构
- `total`: 总记录数(integer
- `has_more`: 是否还有更多数据(boolean)
- `next_cursor`: 下一页游标,用于分页(string,仅当 has_more 为 true 时存在)
- `records`: 记录数组
## 分页查询
```bash
# 第一页
wecom-cli doc smartsheet_get_records '{"docid": "DOCID", "sheet_id": "SHEETID"}'
# 下一页(使用上一条的 next_cursor
wecom-cli doc smartsheet_get_records '{"docid": "DOCID", "sheet_id": "SHEETID", "cursor": "上一条的 next_cursor 值"}'
```
+306
View File
@@ -0,0 +1,306 @@
---
name: wecomcli-todo
description: 企业微信待办事项管理技能,支持创建待办、更新待办、更改用户在待办中的状态、获取待办列表、批量获取待办详情、删除待办。在用户说"帮我创建一个待办"、"把这个任务分派给张三"、"标记待办完成"、"接受/拒绝这个待办"、"把我在这个待办里的状态改成已完成"、"删掉那个待办"、"帮我建个提醒"、"更新一下待办内容"、"把提醒时间改到下周"、"看看这个待办的详情"、"待办分派给谁了"、"看看我有哪些待办"、"列一下我最近的待办"等需要对待办进行读写操作的场景时使用。
metadata:
requires:
bins: ["wecom-cli"]
cliHelp: "wecom-cli todo --help"
---
# 企业微信待办事项管理技能
> `wecom-cli` 是企业微信提供的命令行程序,所有操作通过执行 `wecom-cli` 命令完成。
## 概述
wecomcli-todo 提供企业微信待办事项的管理能力,包含以下功能:
1. **搜索 userid** - 根据姓名或别名搜索用户的 userid,用于在创建/更新待办、更改用户状态、查询待办列表时把姓名解析为 userid
2. **创建待办** - 创建新的待办事项,可指定内容、参与人、截止时间和提醒方式
3. **更新待办** - 修改已有待办的内容、参与人、待办状态、截止时间和提醒方式
4. **更改用户状态** - 更改某位参与人在某个待办中的状态(拒绝/接受/已完成)
5. **获取待办列表** - 获取指定用户的待办列表,支持按创建时间和提醒时间过滤
6. **获取待办详情** - 根据待办 ID 列表批量获取完整信息
7. **删除待办** - 删除指定的待办事项
## 命令调用方式
执行指定命令:
```bash
wecom-cli todo <tool_name> '<json_params>'
```
---
## 命令详细说明
### 1. 搜索 userid (search_todo_userid)
基础接口,根据姓名或别名搜索用户的 userid,用于在创建待办、更新待办时把用户姓名解析为 userid 进行传参。
#### 执行命令
```bash
wecom-cli todo search_todo_userid '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `keyword` | string | 是 | 姓名或别名 |
#### 调用示例
```bash
wecom-cli todo search_todo_userid '{"keyword": "张三"}'
```
#### 返回字段说明
- 返回与关键字匹配的用户的 `userid`(可能为多个候选)。
- 若有多个同名/相似用户,展示候选列表让用户确认后再使用;展示时只用姓名/别名/部门等可读信息区分,**禁止向用户暴露 `userid`**。
- 拿到的 `userid` 用于 `create_todo` / `update_todo``follower_list`,仅供内部传参,不在对话中展示。
- 调用前可先检查对话上下文是否已有该用户的 `userid`,若有则直接复用、无需再次查询;若本接口返回错误(查询失败、无结果等),则改用通讯录 `wecomcli-contact` 技能搜索(详见注意事项第 2 条)。
---
### 2. 创建待办 (create_todo)
创建一个新的待办事项,可指定内容、参与人、截止时间和提醒方式。
#### 执行命令
```bash
wecom-cli todo create_todo '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|----|-----------------------------------------------------------------|
| `content` | string | 是 | 待办内容 |
| `follower_list` | object | 是 | 参与人信息,对象中包含 `followers` 数组,格式见注意事项第 3 条 |
| `end_time` | string | 条件必填 | 截止时间(到期时间),格式:`YYYY-MM-DD HH:mm:ss`;提醒方式基于此时间计算偏移。当 `remind_type_list` 含非 `0`(即设置了实际提醒)时**必填** |
| `remind_type_list` | uint32[] | 否 | 提醒方式,相对 `end_time` 计算,可传入多个。取值:`0`-不提醒,`1`-到期时,`3`-提前 15 分钟,`5`-提前 1 小时,`6`-提前 2 小时,`7`-提前 1 天,`8`-提前 2 天,`9`-提前 1 周(详见注意事项第 4 条) |
#### 调用示例
```bash
wecom-cli todo create_todo '{"content": "<待办的内容>", "follower_list": {"followers": [{"follower_id": "FOLLOWER_ID", "follower_status": 1}]}, "end_time": "2025-06-01 09:00:00", "remind_type_list": [3, 7]}'
```
#### 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `todo_id` | string | 新创建的待办唯一 ID |
---
### 3. 更新待办 (update_todo)
修改已有待办事项的内容、参与人、待办状态、截止时间或提醒方式。
> 说明:本接口可更改待办状态(`todo_status`),但**不能更改参与人状态(`follower_status`)**。如需更改当前用户在某个待办中的状态,请使用 `change_todo_user_status`。
#### 执行命令
```bash
wecom-cli todo update_todo '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|----------------------------------------------------------|
| `todo_id` | string | 是 | 待办 ID |
| `content` | string | 否 | 新的待办内容 |
| `follower_list` | object | 否 | 新的参与人信息(全量替换,非追加),对象中包含 `followers` 数组,格式见注意事项第 3 条。若要新增参与人,需先查出现有参与人,合并后一起提交。**本接口不会更改参与人状态(`follower_status`),如需更改用户状态请使用 `change_todo_user_status`** |
| `todo_status` | uint32 | 否 | 新的待办状态:`0`-已完成,`1`-进行中 |
| `end_time` | string | 否 | 新的截止时间(到期时间),格式:`YYYY-MM-DD HH:mm:ss`;提醒方式基于此时间计算偏移 |
| `remind_type_list` | uint32[] | 否 | 新的提醒方式,相对 `end_time` 计算,可传入多个。取值:`0`-不提醒,`1`-到期时,`3`-提前 15 分钟,`5`-提前 1 小时,`6`-提前 2 小时,`7`-提前 1 天,`8`-提前 2 天,`9`-提前 1 周(详见注意事项第 4 条) |
#### 调用示例
```bash
wecom-cli todo update_todo '{"todo_id": "TODO_ID", "content": "<待办的内容>", "end_time": "2025-07-01 09:00:00", "remind_type_list": [1]}'
```
---
### 4. 更改用户在某个待办的状态 (change_todo_user_status)
更改某位参与人在某个待办中的状态(拒绝/接受/已完成)。
#### 执行命令
```bash
wecom-cli todo change_todo_user_status '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `todo_id` | string | 是 | 待办 ID |
| `follower_id` | string | 是 | 参与人 `userid`,指定要修改哪一位参与人的状态。来源遵循注意事项第 2 条的 userid 获取规则(上下文已有 → `search_todo_userid``wecomcli-contact` 技能兜底),禁止自行猜测或构造 |
| `user_status` | uint32 | 是 | 用户状态:`0`-拒绝,`1`-接受,`2`-已完成 |
#### 调用示例
```bash
wecom-cli todo change_todo_user_status '{"todo_id": "TODO_ID", "follower_id": "FOLLOWER_ID", "user_status": 2}'
```
---
### 5. 获取待办列表 (get_todo_list)
获取指定用户(由 `follower_id` 指定,可为任意用户)通过机器人的待办列表,支持当天前后一个月的待办,可按创建时间、提醒时间、截止时间和待办状态过滤。
#### 执行命令
```bash
wecom-cli todo get_todo_list '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `follower_id` | string | 是 | 参与人 `userid`,指定要查询哪一位用户的待办列表(可为任意用户),来源遵循注意事项第 2 条的 userid 获取规则,禁止自行猜测或构造 |
| `create_begin_time` | string | 否 | 创建开始时间 |
| `create_end_time` | string | 否 | 创建结束时间 |
| `remind_begin_time` | string | 否 | 提醒开始时间 |
| `remind_end_time` | string | 否 | 提醒结束时间 |
| `deadline_begin_time` | string | 否 | 截止开始时间 |
| `deadline_end_time` | string | 否 | 截止结束时间 |
| `todo_status` | uint32 | 否 | 待办状态:`0`-已完成,`1`-进行中 |
| `limit` | uint32 | 否 | 最大返回数量,默认为 10,最大为 20 |
| `cursor` | string | 否 | 游标,用于分页 |
> **查询时间范围限制**
> - 用于筛选的时间(创建时间、提醒时间、截止时间)必须落在当天前后 30 天以内。
> - 若仅按 `todo_status` 筛选而未传任何时间范围,则默认按「创建时间在当天前后 30 天内」查询。
#### 调用示例
```bash
wecom-cli todo get_todo_list '{"follower_id": "FOLLOWER_ID", "create_begin_time": "2025-06-01 00:00:00", "create_end_time": "2025-06-30 23:59:59", "todo_status": 1, "limit": 20}'
```
#### 返回字段说明
- 返回指定用户(`follower_id`)的待办列表,以及用于分页的游标。
- 拿到的 `todo_id` 可用于 `get_todo_detail` / `update_todo` / `delete_todo`,仅供内部传参,**禁止向用户暴露**。
---
### 6. 获取待办详情 (get_todo_detail)
根据待办 ID 列表批量查询完整详情,包含待办内容和参与人信息。
#### 执行命令
```bash
wecom-cli todo get_todo_detail '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `todo_id_list` | string[] | 是 | 待办 ID 列表,至少 1 个,最多 20 个 |
#### 调用示例
```bash
wecom-cli todo get_todo_detail '{"todo_id_list": ["TODO_ID_1", "TODO_ID_2"]}'
```
#### 返回字段说明
| 字段 | 类型 | 说明 |
|---------------------------------------------------------|--------|-----------------------------------|
| `data_list` | array | 待办详情列表,最多 20 条 |
| `data_list[].todo_id` | string | 待办 ID |
| `data_list[].todo_status` | number | 待办状态:`0`-已完成,`1`-进行中,`2`-已删除 |
| `data_list[].content` | string | 待办内容 |
| `data_list[].follower_list` | object | 参与人列表 |
| `data_list[].follower_list.followers[].follower_id` | string | 参与人 ID(即 userid |
| `data_list[].follower_list.followers[].name` | string | 参与人姓名 |
| `data_list[].follower_list.followers[].follower_status` | number | 参与人状态:`0`-拒绝,`1`-接受,`2`-已完成 |
| `data_list[].follower_list.followers[].update_time` | string | 参与人状态更新时间 |
| `data_list[].creator_id` | string | 创建人 ID(即 userid |
| `data_list[].user_status` | number | 当前用户在该待办中的状态:`0`-拒绝,`1`-接受,`2`-已完成 |
| `data_list[].end_time` | string | 截止时间(到期时间) |
| `data_list[].remind_type_list` | array | 提醒方式列表,取值:`0`-不提醒,`1`-到期时,`3`-提前 15 分钟,`5`-提前 1 小时,`6`-提前 2 小时,`7`-提前 1 天,`8`-提前 2 天,`9`-提前 1 周 |
| `data_list[].create_time` | string | 创建时间 |
| `data_list[].update_time` | string | 更新时间 |
---
### 7. 删除待办 (delete_todo)
删除指定的待办事项。
#### 执行命令
```bash
wecom-cli todo delete_todo '<json格式的入参>'
```
#### 入参说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `todo_id` | string | 是 | 待办 ID |
#### 调用示例
```bash
wecom-cli todo delete_todo '{"todo_id": "TODO_ID"}'
```
---
## 注意事项
1. **todo_id 来源规则**
- `todo_id` 可来自 `create_todo` 返回的结果,或 `get_todo_list` 查询到的列表,请务必保存好,禁止自行推测或构造
2. **userid 来源与隐私规则**
- 分派待办时,`follower_list` 中的 `follower_id``userid`,按以下优先级获取,禁止根据用户姓名自行猜测或构造 userid:
1. **对话上下文已有**:若对话上下文中已经提到对应用户的 `userid`,直接使用,无需再次查询
2. **search_todo_userid 查询**:若上下文中没有,则可先询问用户在企业内的用户名称(通常是姓名),再通过 `search_todo_userid` 接口按姓名/别名查询获取
3. **通讯录技能兜底**:若 `search_todo_userid` 返回错误(如当前企业不适用等),则改用通讯录 `wecomcli-contact` 技能搜索获取 `userid`
- `userid` 属于敏感标识,仅用于接口传参,**禁止在回复或候选列表中向用户展示**;需要让用户区分人员时,只展示姓名/别名/部门等可读信息
3. **follower_list 格式说明**
- 作为入参时,`follower_list` 为对象,内含 `followers` 数组,格式如下:
```json
"follower_list": {
"followers": [
{
"follower_id": "FOLLOWER_ID",
"follower_status": 1
}
]
}
```
- `follower_id` 即用户的 `userid`,其来源遵循上文「userid 来源与隐私规则」的优先级(上下文已有 → `search_todo_userid` → `wecomcli-contact` 技能兜底),禁止自行猜测或构造
- `follower_status` 为参与人状态:`0`-拒绝,`1`-接受,`2`-已完成;该字段仅在 `create_todo` 中生效
- 在 `update_todo` 中为全量替换(非追加):若要新增参与人,需先用 `get_todo_detail` 查出现有参与人,合并后一并提交;`update_todo` **不会更改参与人状态(`follower_status`)**,如需更改当前用户在某个待办中的状态,请使用 `change_todo_user_status`
4. **remind_type_list 提醒方式取值**
`remind_type_list` 是一个 uint32 数组,可同时传入多个提醒:
| 值 | 含义 | 值 | 含义 |
|----|------|----|------|
| `0` | 不提醒 | `6` | 提前 2 小时 |
| `1` | 到期时 | `7` | 提前 1 天 |
| `3` | 提前 15 分钟 | `8` | 提前 2 天 |
| `5` | 提前 1 小时 | `9` | 提前 1 周 |
+78
View File
@@ -0,0 +1,78 @@
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use anyhow::Result;
use crate::{crypto, fs_util, paths};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bot {
// Bot ID
pub id: String,
// Bot Secret
pub secret: String,
// Creation timestamp (unix epoch seconds)
pub create_time: u64,
}
impl Bot {
/// Create a new Bot with `create_time` set to the current timestamp.
pub fn new(id: String, secret: String) -> Self {
let create_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
id,
secret,
create_time,
}
}
}
/// Read encrypted bot info from disk, decrypt and return it.
/// Returns `None` if the file does not exist or decryption fails.
pub fn get_bot_info() -> Option<Bot> {
let data = fs::read(bot_info_path()).ok()?;
crypto::try_decrypt_data(&data).ok()
}
/// Serialize bot info, encrypt and persist to disk.
/// The encryption key is stored in the system keyring when possible, otherwise falls back to an encrypted file.
pub fn set_bot_info(bot: &Bot) -> Result<()> {
// 1. Load or generate an encryption key
let key = crypto::load_existing_key().unwrap_or_else(|| {
let k = crypto::generate_random_key();
tracing::info!("已生成新的加密密钥");
k
});
// 2. Persist the key (prefer keyring, fall back to file)
crypto::save_key(&key)?;
// 3. Serialize bot info → JSON → encrypt
let encrypted = crypto::encrypt_data(bot, &key)?;
// 4. Write to file
let path = bot_info_path();
fs_util::atomic_write(&path, &encrypted, Some(0o600))?;
tracing::info!("企业微信机器人信息已保存到 {}", path.display());
Ok(())
}
/// Remove the stored Bot info from disk.
pub fn clear_bot_info() {
let path = bot_info_path();
if path.exists() {
let _ = fs::remove_file(&path);
tracing::info!("机器人信息已删除:{}", path.display());
}
}
/// Return the file path for the encrypted bot credentials.
fn bot_info_path() -> std::path::PathBuf {
paths::wecom_home_dir().join("bot.enc")
}
+5
View File
@@ -0,0 +1,5 @@
mod bot;
mod qrcode;
pub use bot::{Bot, clear_bot_info, get_bot_info, set_bot_info};
pub use qrcode::scan_qrcode_for_bot;
+186
View File
@@ -0,0 +1,186 @@
use std::io::IsTerminal;
use std::time::Duration;
use crate::browser;
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use super::bot::Bot;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const SOURCE: &str = "wecom_cli_external";
const QR_GENERATE_URL: &str = "https://work.weixin.qq.com/ai/qc/generate";
const QR_QUERY_URL: &str = "https://work.weixin.qq.com/ai/qc/query_result";
const QR_CODE_PAGE: &str = "https://work.weixin.qq.com/ai/qc/gen";
/// 轮询间隔 3 秒
const POLL_INTERVAL: Duration = Duration::from_secs(3);
/// 超时 5 分钟
const POLL_TIMEOUT: Duration = Duration::from_secs(300);
// ---------------------------------------------------------------------------
// Response types
// ---------------------------------------------------------------------------
#[derive(Deserialize)]
struct GenerateResponse {
data: Option<GenerateData>,
}
#[derive(Deserialize)]
struct GenerateData {
scode: Option<String>,
auth_url: Option<String>,
}
#[derive(Deserialize)]
struct QueryResponse {
data: Option<QueryData>,
}
#[derive(Deserialize)]
struct QueryData {
status: Option<String>,
bot_info: Option<BotInfoPayload>,
}
#[derive(Deserialize)]
struct BotInfoPayload {
botid: Option<String>,
secret: Option<String>,
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// 扫码接入完整流程:获取二维码 → 终端展示 → 轮询结果 → 返回 Bot
pub async fn scan_qrcode_for_bot(no_open: bool) -> Result<Bot> {
let client = build_client()?;
println!("正在获取二维码...");
let (scode, auth_url) = fetch_qrcode(&client).await?;
let qrcode_url = format!("{}?source={}&scode={}", QR_CODE_PAGE, SOURCE, scode);
println!("请打开二维码链接扫码: \n{}", qrcode_url);
println!("也可以使用企业微信扫描以下二维码:");
if std::io::stdout().is_terminal() {
render_qrcode(&auth_url)?;
} else {
render_qrcode_unicode(&auth_url)?;
}
// 同步在浏览器中打开二维码
if !no_open {
browser::open_url_by_browser(&qrcode_url);
}
println!("等待扫码中...");
let (bot_id, secret) = poll_result(&client, &scode).await?;
println!("✔ 扫码成功!Bot ID 和 Secret 已自动获取。");
Ok(Bot::new(bot_id, secret))
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
fn build_client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.build()
.context("创建 HTTP 客户端失败")
}
/// 获取二维码链接和轮询 scode
async fn fetch_qrcode(client: &reqwest::Client) -> Result<(String, String)> {
let url = format!(
"{}?source={}&plat={}",
QR_GENERATE_URL,
SOURCE,
get_plat_code()
);
let response: GenerateResponse = client.get(&url).send().await?.json().await?;
let Some(data) = response.data.as_ref() else {
bail!("获取二维码失败,响应格式异常");
};
let (Some(scode), Some(auth_url)) = (&data.scode, &data.auth_url) else {
bail!("获取二维码失败,响应格式异常");
};
Ok((scode.to_string(), auth_url.to_string()))
}
fn get_plat_code() -> u8 {
if cfg!(target_os = "macos") {
1
} else if cfg!(target_os = "windows") {
2
} else if cfg!(target_os = "linux") {
3
} else {
0
}
}
/// 在终端渲染二维码(TTY,带 ANSI 色彩)
fn render_qrcode(url: &str) -> Result<()> {
println!();
qr2term::print_qr(url).map_err(|e| anyhow::anyhow!("二维码渲染失败: {e}"))?;
Ok(())
}
/// 在 non-TTY 环境下用纯 Unicode 半块字符渲染二维码(无 ANSI escape
fn render_qrcode_unicode(url: &str) -> Result<()> {
use qrcode::QrCode;
use qrcode::render::unicode::Dense1x2;
let code = QrCode::new(url).map_err(|e| anyhow::anyhow!("二维码渲染失败: {e}"))?;
let string = code
.render::<Dense1x2>()
.dark_color(Dense1x2::Dark)
.light_color(Dense1x2::Light)
.build();
println!();
println!("{}", string);
Ok(())
}
/// 轮询扫码结果
async fn poll_result(client: &reqwest::Client, scode: &str) -> Result<(String, String)> {
let url = format!("{}?scode={}", QR_QUERY_URL, scode);
let start = std::time::Instant::now();
loop {
if start.elapsed() >= POLL_TIMEOUT {
bail!("扫码超时(5 分钟),请重试。");
}
let response: QueryResponse = client.get(&url).send().await?.json().await?;
if let Some(data) = &response.data {
if data.status.as_deref() == Some("success") {
let Some(bot_info) = &data.bot_info else {
anyhow::bail!("扫码成功但未获取到 Bot 信息");
};
let (Some(botid), Some(secret)) = (&bot_info.botid, &bot_info.secret) else {
anyhow::bail!("扫码成功但未获取到 Bot 信息");
};
return Ok((botid.to_string(), secret.to_string()));
}
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
+10
View File
@@ -0,0 +1,10 @@
/// 尝试使用系统默认浏览器打开指定 URL。
/// - 使用 `open` crate 跨平台打开浏览器(macOS: `open`、Windows: `ShellExecuteW`、Linux: `xdg-open`)。
/// - 打开失败不会中断主流程,仅记录日志。
pub fn open_url_by_browser(url: &str) {
tracing::debug!(url, "正在使用默认浏览器打开链接");
open::that(url).unwrap_or_else(|err| {
tracing::debug!(url, %err, "无法打开默认浏览器,请手动在浏览器中打开链接");
});
}
+59
View File
@@ -0,0 +1,59 @@
use crate::auth::{Bot, get_bot_info};
use anyhow::Result;
use clap::{ArgMatches, Args, Command, FromArgMatches, Subcommand};
#[derive(Subcommand)]
#[command(subcommand_required = true, arg_required_else_help = true)]
pub enum AuthSubcmds {
/// Show current authentication state
Show(ShowArgs),
}
#[derive(Args)]
pub struct ShowArgs {
/// 展示认证状态详情
#[arg(long)]
pub auth_status: bool,
}
pub fn build_auth_cmd() -> Command {
AuthSubcmds::augment_subcommands(Command::new("auth")).hide(true)
}
pub async fn handle_auth_cmd(matches: &ArgMatches) -> Result<()> {
match AuthSubcmds::from_arg_matches(matches)? {
AuthSubcmds::Show(args) => handle_auth_show(&args),
}
}
fn handle_auth_show(args: &ShowArgs) -> Result<()> {
let bot = get_bot_info();
if args.auth_status {
return handle_auth_show_auth_status(bot);
}
handle_auth_show_default(bot)
}
fn handle_auth_show_default(bot: Option<Bot>) -> Result<()> {
if let Some(bot) = bot {
let view = serde_json::json!({
"id": bot.id,
"create_time": bot.create_time,
});
println!("{}", serde_json::to_string_pretty(&view)?);
} else {
println!("unauthorized");
}
Ok(())
}
fn handle_auth_show_auth_status(bot: Option<Bot>) -> Result<()> {
if bot.is_some() {
println!("authorized");
} else {
println!("unauthorized");
}
Ok(())
}
+124
View File
@@ -0,0 +1,124 @@
use std::fs;
use std::path::Path;
use anyhow::Result;
use clap::{ArgMatches, Command, FromArgMatches, Subcommand};
use serde_json::json;
use crate::paths;
/// 管理本地缓存
#[derive(Subcommand)]
pub enum CacheCmds {
/// 显示本地缓存状态
#[command(disable_help_flag = true)]
Status,
/// 清除本地缓存
#[command(disable_help_flag = true)]
Clear,
}
pub fn build_cache_cmd() -> Command {
CacheCmds::augment_subcommands(Command::new("cache"))
.disable_help_flag(true)
.disable_help_subcommand(true)
.subcommand_required(true)
.arg_required_else_help(true)
}
pub async fn handle_cache_cmd(matches: &ArgMatches) -> Result<()> {
let args = CacheCmds::from_arg_matches(matches);
let cache_dir = paths::cache_dir();
match args {
Ok(CacheCmds::Status) => handle_cache_status(&cache_dir),
Ok(CacheCmds::Clear) => handle_cache_clear(&cache_dir),
_ => anyhow::bail!("未知命令"),
}
}
/// 列出当前缓存目录下所有文件及其修改时间。
fn handle_cache_status(cache_dir: &Path) -> Result<()> {
let files = collect_cache_files(cache_dir);
let mut entries: Vec<serde_json::Value> = files
.iter()
.filter_map(|path| {
let metadata = fs::metadata(path).ok()?;
let modified = metadata
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
Some(json!({
"file": path.file_name().unwrap_or_default().to_string_lossy(),
"update_time": modified,
}))
})
.collect();
entries.sort_by(|a, b| {
a["file"]
.as_str()
.unwrap_or_default()
.cmp(b["file"].as_str().unwrap_or_default())
});
println!(
"{}",
serde_json::to_string_pretty(&entries).unwrap_or_default()
);
Ok(())
}
/// 清除缓存目录下所有文件。
fn handle_cache_clear(cache_dir: &Path) -> Result<()> {
let mut removed = Vec::new();
for path in collect_cache_files(cache_dir) {
match fs::remove_file(&path) {
Ok(()) => {
removed.push(
path.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
}
Err(e) => {
tracing::warn!(path = %path.display(), error = %e, "删除缓存文件失败");
}
}
}
let output = if removed.is_empty() {
json!({
"status": "success",
"message": "未找到需要删除的缓存文件",
})
} else {
json!({
"status": "success",
"message": format!("已删除 {} 个缓存文件", removed.len()),
"removed": removed,
})
};
println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
);
Ok(())
}
/// 收集缓存目录下所有常规文件的路径。
fn collect_cache_files(cache_dir: &Path) -> Vec<std::path::PathBuf> {
let Ok(read_dir) = fs::read_dir(cache_dir) else {
return Vec::new();
};
read_dir
.flatten()
.map(|e| e.path())
.filter(|p| p.is_file())
.collect()
}
+110
View File
@@ -0,0 +1,110 @@
use std::io::IsTerminal;
use crate::auth;
use crate::mcp;
use crate::mcp::config::McpBindSource;
use anyhow::Result;
use clap::{ArgMatches, Args, Command, FromArgMatches};
#[derive(Args)]
pub struct InitArgs {
/// 跳过交互选择,直接使用扫码方式接入
#[arg(long)]
pub noninteractive: bool,
/// 扫码时不自动在浏览器打开二维码页面
#[arg(long = "no-open")]
pub no_open: bool,
}
pub fn build_init_cmd() -> Command {
InitArgs::augment_args(Command::new("init").about("初始化企业微信机器人配置"))
.disable_help_flag(true)
}
/// Handle the `init` subcommand: prompt for bot credentials, persist them, and verify via MCP config fetch.
pub async fn handle_init_cmd(matches: &ArgMatches) -> Result<()> {
let args = InitArgs::from_arg_matches(matches)?;
if !args.noninteractive && !std::io::stderr().is_terminal() {
anyhow::bail!(
"当前环境不支持交互式操作,请使用 --noninteractive 参数:\n {} init --noninteractive",
env!("CARGO_BIN_NAME")
);
}
cliclack::intro("企业微信机器人初始化")?;
let (bot, bind_source) = if args.noninteractive {
(init_qrcode(args.no_open).await?, McpBindSource::Qrcode)
} else {
let method: &str = cliclack::select("请选择企微机器人接入方式:")
.item("qrcode", "扫码接入(推荐)", "")
.item("manual", "手动输入 Bot ID 和 Secret", "")
.interact()?;
match method {
"qrcode" => (init_qrcode(args.no_open).await?, McpBindSource::Qrcode),
_ => (init_manual().await?, McpBindSource::Interactive),
}
};
auth::set_bot_info(&bot)?;
verify_and_finish(bind_source).await
}
/// 扫码接入流程
async fn init_qrcode(no_open: bool) -> Result<auth::Bot> {
auth::scan_qrcode_for_bot(no_open).await
}
/// 手动输入 Bot ID 和 Secret
async fn init_manual() -> Result<auth::Bot> {
let bot_id: String = cliclack::input("企业微信机器人 Bot ID")
.placeholder("请输入企业微信机器人ID")
.interact()?;
let bot_secret: String = cliclack::password("企业微信机器人 Secret")
.mask('*')
.interact()?;
Ok(auth::Bot::new(bot_id, bot_secret))
}
/// 验证凭证并完成初始化
async fn verify_and_finish(bind_source: McpBindSource) -> Result<()> {
let spinner = cliclack::spinner();
spinner.start("正在验证企业微信机器人凭证...");
if let Err(e) = mcp::config::fetch_mcp_config(bind_source).await {
spinner.stop("企业微信机器人凭证验证失败");
let mut output_errmsg: String = "验证企业微信机器人凭证失败".to_owned();
match &e {
mcp::error::FetchMcpConfigError::Api(resp) => {
if let Some(ref msg) = resp.errmsg {
if !msg.is_empty() {
output_errmsg = msg.clone();
}
}
}
mcp::error::FetchMcpConfigError::Http(http_err) => {
output_errmsg = format!("{} HTTP返回状态码 {}", output_errmsg, http_err.status);
}
mcp::error::FetchMcpConfigError::Other(other_err) => {
output_errmsg = other_err.to_string();
}
}
// Credentials invalid or server unreachable — rollback
auth::clear_bot_info();
mcp::config::clear_mcp_config();
cliclack::outro("初始化失败 ❌")?;
anyhow::bail!(output_errmsg);
}
spinner.stop("企业微信机器人凭证验证成功");
cliclack::outro("初始化完成 ✅")?;
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod auth;
pub mod cache;
pub mod init;
+39
View File
@@ -0,0 +1,39 @@
const DEFAULT_MCP_CONFIG_ENDPOINT: &str =
"https://qyapi.weixin.qq.com/cgi-bin/aibot/cli/get_mcp_config";
pub mod env {
/// Config directory, defaults to ~/.config/wecom
pub const CONFIG_DIR: &str = "WECOM_CLI_CONFIG_DIR";
/// Temp directory, defaults to std::env::temp_dir().join("wecom")
pub const TMP_DIR: &str = "WECOM_CLI_TMP_DIR";
/// Log level
pub const LOG_LEVEL: &str = "WECOM_CLI_LOG_LEVEL";
/// Log file directory path
pub const LOG_FILE: &str = "WECOM_CLI_LOG_FILE";
/// MCP config URL (仅在启用 `custom-endpoint` feature 后使用)
#[cfg_attr(not(feature = "custom-endpoint"), allow(dead_code))]
pub const MCP_CONFIG_ENDPOINT: &str = "WECOM_CLI_MCP_CONFIG_ENDPOINT";
}
/// Return the MCP config endpoint URL (env override or the default WeCom API).
pub fn mcp_config_endpoint() -> String {
#[cfg(feature = "custom-endpoint")]
if let Ok(url) = std::env::var(env::MCP_CONFIG_ENDPOINT) {
return url;
}
DEFAULT_MCP_CONFIG_ENDPOINT.to_string()
}
pub fn get_user_agent() -> String {
format!(
"WeComCLI/{} distribution/{} {}/{}",
env!("CARGO_PKG_VERSION"),
option_env!("WECOM_CLI_DISTRIBUTION").unwrap_or_else(|| "unknown"),
std::env::consts::OS,
std::env::consts::ARCH,
)
}
+118
View File
@@ -0,0 +1,118 @@
use aes_gcm::aead::{Aead, OsRng};
use aes_gcm::{AeadCore, Aes256Gcm, Key, KeyInit};
use anyhow::Result;
/// AES-GCM nonce size (96 bits).
const NONCE_SIZE: usize = 12;
/// AES-GCM authentication tag size (128 bits).
const TAG_SIZE: usize = 16;
/// Encrypt `plaintext` with AES-256-GCM. Returns `nonce || ciphertext`.
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|e| anyhow::anyhow!("数据加密失败:{e}"))?;
let mut out = nonce.to_vec();
out.extend(ciphertext);
Ok(out)
}
/// Decrypt `data` (expected format: `nonce || ciphertext || tag`) with AES-256-GCM.
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
if data.len() < NONCE_SIZE + TAG_SIZE {
return Err(anyhow::anyhow!("数据解密失败(数据可能已损坏或被截断)",));
}
let (nonce_bytes, ciphertext) = data.split_at(NONCE_SIZE);
let nonce = aes_gcm::Nonce::from_slice(nonce_bytes);
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
cipher
.decrypt(nonce, ciphertext)
.map_err(|e| anyhow::anyhow!("数据解密失败:{e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::keystore::generate_random_key;
#[test]
fn encrypt_decrypt_roundtrip() {
let key = generate_random_key();
let plaintext = b"hello, AES-256-GCM!";
let encrypted = encrypt(&key, plaintext).unwrap();
let decrypted = decrypt(&key, &encrypted).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn encrypt_decrypt_empty_plaintext() {
let key = generate_random_key();
let encrypted = encrypt(&key, b"").unwrap();
let decrypted = decrypt(&key, &encrypted).unwrap();
assert_eq!(decrypted, b"");
}
#[test]
fn encrypted_output_has_expected_length() {
let key = generate_random_key();
let plaintext = b"test data";
let encrypted = encrypt(&key, plaintext).unwrap();
// nonce (12) + plaintext (9) + tag (16) = 37
assert_eq!(encrypted.len(), NONCE_SIZE + plaintext.len() + TAG_SIZE);
}
#[test]
fn decrypt_with_wrong_key_fails() {
let key1 = generate_random_key();
let key2 = generate_random_key();
let encrypted = encrypt(&key1, b"secret").unwrap();
assert!(decrypt(&key2, &encrypted).is_err());
}
#[test]
fn decrypt_too_short_data_fails() {
let key = generate_random_key();
// Less than NONCE_SIZE + TAG_SIZE = 28
assert!(decrypt(&key, &[0u8; 27]).is_err());
assert!(decrypt(&key, &[]).is_err());
assert!(decrypt(&key, &[0u8; 11]).is_err());
}
#[test]
fn decrypt_corrupted_data_fails() {
let key = generate_random_key();
let encrypted = encrypt(&key, b"important data").unwrap();
// Flip a byte in the ciphertext portion
let mut corrupted = encrypted.clone();
let last = corrupted.len() - 1;
corrupted[last] ^= 0xFF;
assert!(decrypt(&key, &corrupted).is_err());
}
#[test]
fn each_encryption_produces_different_output() {
let key = generate_random_key();
let plaintext = b"same plaintext";
let a = encrypt(&key, plaintext).unwrap();
let b = encrypt(&key, plaintext).unwrap();
// Different nonces → different ciphertext
assert_ne!(a, b);
// But both decrypt to the same plaintext
assert_eq!(decrypt(&key, &a).unwrap(), plaintext);
assert_eq!(decrypt(&key, &b).unwrap(), plaintext);
}
}
+252
View File
@@ -0,0 +1,252 @@
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use base64::prelude::*;
use rand::Rng;
use crate::{fs_util, paths};
use super::cipher;
const KEYRING_SERVICE: &str = "wecom-cli";
const KEYRING_USER: &str = "encryption-key";
// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------
/// Return the file path for the local encryption key fallback.
pub fn encryption_key_path() -> PathBuf {
paths::wecom_home_dir().join(".encryption_key")
}
// ---------------------------------------------------------------------------
// Encode / Decode
// ---------------------------------------------------------------------------
/// Encode a 32-byte key as a Base64 string.
fn encode_key(key: &[u8; 32]) -> String {
BASE64_STANDARD.encode(key)
}
/// Decode a Base64 string into a 32-byte key, returning an error on invalid input.
fn decode_key(s: &str) -> Result<[u8; 32]> {
let bytes = BASE64_STANDARD
.decode(s)
.map_err(|e| anyhow::anyhow!("base64 decode error: {e}"))?;
<[u8; 32]>::try_from(bytes.as_slice())
.map_err(|_| anyhow::anyhow!("Invalid encryption key length"))
}
// ---------------------------------------------------------------------------
// Key generation / loading / saving
// ---------------------------------------------------------------------------
/// Generate a fresh random 256-bit key.
pub fn generate_random_key() -> [u8; 32] {
rand::rng().random()
}
/// Load the key from keyring. Returns `None` if unavailable.
fn load_key_from_keyring() -> Option<[u8; 32]> {
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER).ok()?;
let b64 = entry.get_password().ok()?;
decode_key(b64.trim()).ok()
}
/// Load the key from the file fallback. Returns `None` if unavailable.
fn load_key_from_file() -> Option<[u8; 32]> {
let contents = fs::read_to_string(encryption_key_path()).ok()?;
decode_key(contents.trim()).ok()
}
/// Try to load an existing key.
///
/// Priority: process cache → file → keyring (last resort, may prompt).
/// The result is cached for the lifetime of the process.
pub fn load_existing_key() -> Option<[u8; 32]> {
load_key_from_file().or_else(load_key_from_keyring)
}
/// Persist the key. Writes to the file fallback always; writes to keyring
/// at most once per process to avoid repeated macOS Keychain prompts.
///
/// If the key is already cached and identical, this is a no-op.
pub fn save_key(key: &[u8; 32]) -> Result<()> {
let b64 = encode_key(key);
// Always write the file fallback.
let key_path = encryption_key_path();
fs_util::atomic_write(&key_path, b64.as_bytes(), Some(0o600))?;
if keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER)
.and_then(|entry| entry.set_password(&b64))
.is_err()
{
tracing::warn!("Keyring unavailable encryption key stored in file only");
}
Ok(())
}
// ---------------------------------------------------------------------------
// Encrypt / Decrypt helpers for serializable data
// ---------------------------------------------------------------------------
/// Encrypt serializable data: serialize → AES-256-GCM encrypt.
pub fn encrypt_data<T: serde::Serialize + ?Sized>(data: &T, key: &[u8; 32]) -> Result<Vec<u8>> {
let json =
serde_json::to_vec(data).map_err(|e| anyhow::anyhow!("JSON serialize error: {e:#}"))?;
cipher::encrypt(key, &json)
}
/// Decrypt data: AES-256-GCM decrypt → deserialize.
pub fn decrypt_data<T: serde::de::DeserializeOwned>(data: &[u8], key: &[u8; 32]) -> Result<T> {
let decrypted = cipher::decrypt(key, data)?;
serde_json::from_slice(&decrypted).map_err(|e| anyhow::anyhow!("JSON deserialize error: {e:#}"))
}
/// Try to decrypt data using the cached/keyring key first; on failure, fall back to the file key.
pub fn try_decrypt_data<T: serde::de::DeserializeOwned>(data: &[u8]) -> Result<T> {
// 1. Try cached key (covers both keyring and file sources)
if let Some(key) = load_key_from_file() {
if let Ok(result) = decrypt_data::<T>(data, &key) {
return Ok(result);
}
tracing::debug!("Cached key failed to decrypt, trying file key directly…");
}
// 2. Fall back to file key (in case cache holds a stale keyring key)
let key = load_key_from_file().ok_or(anyhow::anyhow!("解密数据失败(未找到有效密钥)",))?;
decrypt_data(data, &key)
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
// -----------------------------------------------------------------------
// encode_key / decode_key
// -----------------------------------------------------------------------
#[test]
fn encode_decode_roundtrip() {
let key = generate_random_key();
let encoded = encode_key(&key);
let decoded = decode_key(&encoded).unwrap();
assert_eq!(key, decoded);
}
#[test]
fn decode_invalid_base64_fails() {
assert!(decode_key("not-valid-base64!!!").is_err());
}
#[test]
fn decode_wrong_length_fails() {
// Valid base64 but only 16 bytes, not 32
let short = base64::prelude::BASE64_STANDARD.encode([0u8; 16]);
assert!(decode_key(&short).is_err());
}
#[test]
fn decode_trims_whitespace() {
let key = generate_random_key();
let encoded = format!(" {} \n", encode_key(&key));
let decoded = decode_key(encoded.trim()).unwrap();
assert_eq!(key, decoded);
}
// -----------------------------------------------------------------------
// generate_random_key
// -----------------------------------------------------------------------
#[test]
fn random_keys_are_unique() {
let a = generate_random_key();
let b = generate_random_key();
assert_ne!(a, b);
}
#[test]
fn random_key_is_32_bytes() {
let key = generate_random_key();
assert_eq!(key.len(), 32);
}
// -----------------------------------------------------------------------
// encrypt_data / decrypt_data
// -----------------------------------------------------------------------
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestPayload {
name: String,
value: u64,
}
#[test]
fn encrypt_decrypt_data_roundtrip() {
let key = generate_random_key();
let payload = TestPayload {
name: "test".into(),
value: 42,
};
let encrypted = encrypt_data(&payload, &key).unwrap();
let decrypted: TestPayload = decrypt_data(&encrypted, &key).unwrap();
assert_eq!(payload, decrypted);
}
#[test]
fn encrypt_decrypt_data_with_slice() {
let key = generate_random_key();
let items = vec![
TestPayload {
name: "a".into(),
value: 1,
},
TestPayload {
name: "b".into(),
value: 2,
},
];
let encrypted = encrypt_data(&items, &key).unwrap();
let decrypted: Vec<TestPayload> = decrypt_data(&encrypted, &key).unwrap();
assert_eq!(items, decrypted);
}
#[test]
fn decrypt_data_with_wrong_key_fails() {
let key1 = generate_random_key();
let key2 = generate_random_key();
let payload = TestPayload {
name: "secret".into(),
value: 99,
};
let encrypted = encrypt_data(&payload, &key1).unwrap();
assert!(decrypt_data::<TestPayload>(&encrypted, &key2).is_err());
}
#[test]
fn decrypt_data_with_corrupted_data_fails() {
let key = generate_random_key();
assert!(decrypt_data::<TestPayload>(b"garbage", &key).is_err());
}
#[test]
fn encrypt_decrypt_empty_vec() {
let key = generate_random_key();
let items: Vec<TestPayload> = vec![];
let encrypted = encrypt_data(&items, &key).unwrap();
let decrypted: Vec<TestPayload> = decrypt_data(&encrypted, &key).unwrap();
assert!(decrypted.is_empty());
}
}
+4
View File
@@ -0,0 +1,4 @@
mod cipher;
mod keystore;
pub use keystore::*;
+138
View File
@@ -0,0 +1,138 @@
use std::fs;
use std::io::Write;
use std::path::Path;
use anyhow::Result;
/// Atomically write `data` (bytes) to `path`, overwriting if the file already exists.
///
/// Uses `tempfile::NamedTempFile` to create a temporary file in the same directory,
/// then atomically renames it to the target path via `persist`.
pub fn atomic_write(path: &Path, data: &[u8], mode: Option<u32>) -> Result<()> {
let Some(parent) = path.parent() else {
anyhow::bail!("无效文件路径:{}", path.display());
};
fs::create_dir_all(parent)?;
// Create a temp file in the same directory (same filesystem → atomic rename).
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
// Set permissions before writing content (avoid permission window).
#[cfg(unix)]
if let Some(m) = mode {
use std::os::unix::fs::PermissionsExt;
tmp.as_file()
.set_permissions(fs::Permissions::from_mode(m))?;
}
tmp.write_all(data)?;
tmp.as_file().flush()?;
tmp.as_file().sync_all()?;
// Atomically rename to the target path.
tmp.persist(path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn tmp() -> TempDir {
tempfile::tempdir().unwrap()
}
// -----------------------------------------------------------------------
// atomic_write
// -----------------------------------------------------------------------
#[test]
fn write_creates_file_with_correct_content() {
let dir = tmp();
let path = dir.path().join("test.bin");
let data = b"hello world";
atomic_write(&path, data, None).unwrap();
assert_eq!(fs::read(&path).unwrap(), data);
}
#[test]
fn write_creates_parent_directories() {
let dir = tmp();
let path = dir.path().join("a").join("b").join("deep.bin");
atomic_write(&path, b"nested", None).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"nested");
}
#[test]
fn overwrite_replaces_existing_file() {
let dir = tmp();
let path = dir.path().join("overwrite.bin");
atomic_write(&path, b"first", None).unwrap();
atomic_write(&path, b"second", None).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"second");
}
#[test]
fn no_temp_file_left_after_success() {
let dir = tmp();
let path = dir.path().join("clean.bin");
atomic_write(&path, b"ok", None).unwrap();
let entries: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.collect();
// Only the target file should remain; no `.tmp.*` leftovers.
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].file_name(), "clean.bin");
}
#[test]
fn write_empty_data() {
let dir = tmp();
let path = dir.path().join("empty.bin");
atomic_write(&path, b"", None).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"");
}
// -----------------------------------------------------------------------
// Unix-specific: file permissions
// -----------------------------------------------------------------------
#[cfg(unix)]
mod unix {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn write_sets_file_permissions() {
let dir = tmp();
let path = dir.path().join("secret.bin");
atomic_write(&path, b"secret", Some(0o600)).unwrap();
let perms = fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o600);
}
#[test]
fn write_sets_readonly_permissions() {
let dir = tmp();
let path = dir.path().join("readonly.bin");
atomic_write(&path, b"ro", Some(0o400)).unwrap();
let perms = fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o400);
}
}
}
+5
View File
@@ -0,0 +1,5 @@
mod atomic_write;
mod sanitize_filename;
pub use atomic_write::*;
pub use sanitize_filename::*;
+15
View File
@@ -0,0 +1,15 @@
/// Sanitize a file name by removing illegal characters; returns `None` if the result is empty.
pub fn sanitize_filename(name: &str) -> Option<String> {
let options = sanitize_filename::Options {
truncate: true,
windows: true,
replacement: "_",
};
let sanitized = sanitize_filename::sanitize_with_options(name, options);
// Return None if the sanitized result is empty (all illegal characters), so the caller can fallback
if sanitized.is_empty() {
None
} else {
Some(sanitized)
}
}
+292
View File
@@ -0,0 +1,292 @@
# Helpers — AI Agent 实现指南
> 本文件面向 AI Agent。当你需要**创建、修改或维护** helper 时,阅读此文件即可完成任务。
> **无需阅读** `main.rs`、`service/` 或其他无关文件。
---
## 关键文件速查
你只需要关注以下文件:
| 文件 | 作用 | 何时需要 |
|------|------|---------|
| `src/helpers/registry.rs` | `Helper` trait 定义 + `HelperRegistry::new()` 注册表 | 新建 helper 时添加注册 |
| `src/helpers/mod.rs` | category 模块声明 | 新建 category 时添加 `mod` |
| `src/helpers/<category>/mod.rs` | 该 category 下的 helper 模块声明 | 新建 helper 时添加 `pub mod` |
| `src/helpers/<category>/<name>.rs` | helper 实现 | 新建或修改 helper |
| `src/json_rpc.rs` | `call_tool(category, method, args)` — 调用远程 MCP 工具 | helper 需要远程调用时参考 |
| `src/fs_util/` | 文件系统工具(`atomic_write`, `sanitize_filename`) | helper 需要安全写文件或处理文件名时使用 |
**不要修改**`main.rs``service/handler.rs``service/command.rs` — helper 通过 registry 自动注册到 CLI,无需改动调度层。
---
## 现有 Helpers
> **维护要求**:新增或删除 helper 后,请同步更新此表。
| command | category | struct | 文件 | 说明 |
|---------|----------|--------|------|------|
| `+smartsheet_add_records_auto_file` | `doc` | `SmartsheetAddRecordsAutoFileHelper` | `doc/smartsheet_add_records_auto_file.rs` | 添加智能表格记录(自动上传文件/图片) |
| `+smartsheet_update_records_auto_file` | `doc` | `SmartsheetUpdateRecordsAutoFileHelper` | `doc/smartsheet_update_records_auto_file.rs` | 更新智能表格记录(自动上传文件/图片) |
| `+smartpage_create` | `doc` | `SmartpageCreateHelper` | `doc/smartpage_create.rs` | 创建智能文档(自动读取本地文件内容作为子页面) |
---
## 什么是 Helper
Helper 是一个**本地子命令**,挂载在某个 service category(如 `doc``msg`)下,用于实现无法由远程 MCP 工具完成的**客户端侧逻辑**(文件处理、多步编排等)。
CLI 调用形式:`wecom <category> +<helper_name> [args]`
Helper 名称以 `+` 前缀与远程 method 区分。
---
## 架构概览
```
src/helpers/
├── mod.rs # 模块声明 + pub use HelperRegistry
├── registry.rs # Helper trait 定义 + HelperRegistry(注册表)
├── AGENTS.md # ← 你正在读的文件
├── HUMANS.md # 人类需求模板(你无需阅读)
└── <category>/ # 按 category 分目录
├── mod.rs # pub mod 声明
└── <name>.rs # 具体 helper 实现
```
### Helper trait
```rust
// src/helpers/registry.rs
pub trait Helper: Send + Sync {
/// 所属 category(必须匹配 service::categories 中的 name
fn category(&self) -> &'static str;
/// clap::Command 定义(名称必须以 + 开头)
fn command(&self) -> clap::Command;
/// 异步执行逻辑
fn execute<'a>(
&'a self,
matches: &'a ArgMatches,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
}
```
### 调度流程
1. `main.rs` 构建 CLI,将 helper command 注册为 category 的子命令
2. 用户输入匹配到 `<category> +<name>` 时,`handle_service_cmd` 优先查找 helper
3. 找到 helper → 调用 `helper.execute(matches)`;未找到 → 按远程 JSON-RPC 调用处理
---
## 添加新 Helper — 逐步操作
### 命名规范
| 项目 | 规范 | 示例 |
|------|------|------|
| 文件名 | `<category>_<功能描述>.rs`snake_case | `doc_upload_image.rs` |
| command 名 | `+<category>_<功能描述>` | `+doc_upload_image` |
| Args struct | `<Name>Args`PascalCase | `DocUploadImageArgs` |
| Helper struct | `<Name>Helper`PascalCase | `DocUploadImageHelper` |
### 1. 创建文件
`src/helpers/<category>/` 下新建 `<name>.rs`。如果该 category 目录不存在,同时创建目录和 `mod.rs`
### 2. 实现 Helper trait
```rust
// src/helpers/<category>/<name>.rs
use std::future::Future;
use std::pin::Pin;
use anyhow::Result;
use clap::{ArgMatches, Args, Command, FromArgMatches};
use crate::helpers::registry::Helper;
/// 参数定义(clap derive
#[derive(Args, Debug)]
pub struct MyArgs {
/// 参数说明
#[arg(long)]
pub some_param: String,
/// 可选参数
#[arg(long)]
pub optional_param: Option<String>,
}
pub struct MyHelper;
impl Helper for MyHelper {
fn category(&self) -> &'static str {
"<category>" // 如 "doc", "msg", "schedule" 等
}
fn command(&self) -> clap::Command {
MyArgs::augment_args(Command::new("+<name>"))
}
/// 在此简要描述 helper 的逻辑
fn execute<'a>(
&'a self,
matches: &'a ArgMatches,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async {
let args = MyArgs::from_arg_matches(matches)?;
// 实现逻辑,请注意提供对应注释...
Ok(())
})
}
}
```
### 3. 注册
**a)**`src/helpers/<category>/mod.rs` 中声明模块:
```rust
pub mod <name>;
```
**b)**`src/helpers/mod.rs` 中声明 category 模块(如果是新 category):
```rust
mod <category>;
```
**c)**`src/helpers/registry.rs``HelperRegistry::new()` 中添加:
- 顶部添加 use`use crate::helpers::<category>::<name>::<HelperStruct>;`
- `vec![]` 中添加:`Box::new(<HelperStruct>)`
### 4. 更新本文档
在上方「现有 Helpers」表中添加新行。
完成。无需修改 `main.rs``service/` 下的任何文件。
---
## 修改现有 Helper
1. 在「现有 Helpers」表中找到目标 helper 的文件路径
2. 直接修改对应的 `.rs` 文件
3. 如果修改了 command 名称或 category,需同步更新 `registry.rs` 中的 use 和注册,以及本文档的「现有 Helpers」表
4. 如果删除 helper,需从 `registry.rs``<category>/mod.rs` 中移除相关声明,并更新本文档
---
## 查询远程工具的参数格式
当你需要了解某个远程 MCP 工具的参数 schema 时,可以执行:
```bash
wecom <category> <method> --schema
```
这会输出该工具的完整定义(包含 `name``description``inputSchema`),例如:
```bash
wecom doc upload_doc_image --schema
```
在 helper 实现中,你可以根据 schema 构造正确的 `json_rpc::call_tool` 调用参数。
---
## 理解人类的接口描述
人类会用 `<category>.<method>` 简写来说明需要调用的远程接口,并用大括号描述其**请求/响应结构**。例如:
```
调用 example.example_upload {
"content": "base64", // 要上传的文件 base64
} {
"url": "URL",
"height": 234,
"width": 234,
"size": 81259,
}
```
你需要自行理解接口的参数和返回结构,并将其转化成对应的 JSON-RPC 调用:
```rust
let res = json_rpc::call_tool(
"example",
"example_upload",
json!({
"content": base64_of_image_file,
}),
).await?;
```
如果人类没有提供接口描述,你可以用 `wecom <category> <method> --schema` 自行查询。
---
## 调用远程 MCP 工具 — `json_rpc` 使用
Helper 内部经常需要调用远程 MCP 工具。使用 `crate::json_rpc`
```rust
use crate::json_rpc;
use serde_json::json;
// 标准调用(30s 超时)
let res = json_rpc::call_tool(
"doc", // category
"get_doc_content", // method name
json!({
"docid": "xxx"
}),
).await?;
// 结果在 res["result"] 中
let data = &res["result"];
```
### 错误处理
- JSON-RPC error code ≠ 0 → `JsonRpcError::Api(Value)`
- 其他错误 → `anyhow::Error`
Notes:
- 所有错误均可用 `?` 传播
- 所有错误信息均使用**中文**描述
---
## 可用 categories
| category | 说明 |
|---|---|
| `contact` | 通讯录 — 成员查询和搜索 |
| `doc` | 文档 — 文档/智能表格创建和管理 |
| `meeting` | 会议 — 创建/管理/查询视频会议 |
| `msg` | 消息 — 聊天列表、发送/接收消息、媒体下载 |
| `schedule` | 日程 — 日程增删改查和可用性查询 |
| `todo` | 待办事项 — 创建/查询/编辑待办项 |
---
## 新建 Helper Checklist
- [ ] helper 文件位于 `src/helpers/<category>/<name>.rs`
- [ ] 实现 `Helper` traitcommand 名称以 `+` 开头
- [ ] `category()` 返回值匹配上方「可用 categories」表
- [ ]`<category>/mod.rs``pub mod <name>`
- [ ]`helpers/mod.rs``mod <category>`(如果是新 category
- [ ]`registry.rs` 顶部添加 `use` 并在 `HelperRegistry::new()` 的 vec 中添加 `Box::new(...)`
- [ ] 使用 `json_rpc::call_tool` 调用远程工具(如需要)
- [ ] 错误使用 `anyhow::Result` + `?` 传播,错误信息使用中文
- [ ] 更新本文档「现有 Helpers」表
+17
View File
@@ -0,0 +1,17 @@
# Helpers — 人类需求模板
当你需要创建新 helper 时,请向模型提供以下信息:
**命令格式**
```bash
wecom <category> +<helper_name>
```
**行为描述**
e.g.
1. 调用 category.method { ...req_example } { ...res_example }
2. 随后调用 category.method2 { ...req_example }
3. 返回步骤 2 的结果
+317
View File
@@ -0,0 +1,317 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::{Context, Result};
use base64::Engine as _;
use serde_json::{Value, json};
use crate::json_rpc;
#[derive(Debug, Clone)]
pub enum DocIdentifier {
DocId(String),
Url(String),
}
/// 从参数中提取文档标识(`docid` 或 `url`)。
pub fn extract_doc_identifier(params: &Value) -> Result<DocIdentifier> {
if let Some(docid) = params.get("docid").and_then(|v| v.as_str()) {
if !docid.is_empty() {
return Ok(DocIdentifier::DocId(docid.to_string()));
}
}
if let Some(url) = params.get("url").and_then(|v| v.as_str()) {
if !url.is_empty() {
return Ok(DocIdentifier::Url(url.to_string()));
}
}
anyhow::bail!("参数中缺少 docid 或 url(文档访问链接),上传图片时需要文档标识信息")
}
/// 扫描 records 并自动上传本地文件/图片,将路径替换为上传结果。
pub async fn process_records(records: &mut [Value], doc_id: &DocIdentifier) -> Result<()> {
// 收集需要上传的本地路径
let mut image_paths: HashSet<String> = HashSet::new();
let mut file_paths: HashSet<String> = HashSet::new();
for record in records.iter() {
if let Some(values) = record.get("values").and_then(|v| v.as_object()) {
for (_field, cell) in values {
collect_upload_paths(cell, &mut image_paths, &mut file_paths);
}
}
}
// 上传图片
let image_map = if !image_paths.is_empty() {
upload_images(&image_paths.into_iter().collect::<Vec<_>>(), doc_id).await?
} else {
HashMap::new()
};
// 上传文件
let file_map = if !file_paths.is_empty() {
upload_files(&file_paths.into_iter().collect::<Vec<_>>()).await?
} else {
HashMap::new()
};
// 用上传结果替换本地路径
for record in records.iter_mut() {
if let Some(values) = record.get_mut("values").and_then(|v| v.as_object_mut()) {
for (_field, cell) in values.iter_mut() {
replace_upload_results(cell, &image_map, &file_map);
}
}
}
Ok(())
}
/// 获取 JSON 对象中指定字段的字符串值
fn get_str<'a>(item: &'a Value, key: &str) -> Option<&'a str> {
item.get(key).and_then(|v| v.as_str())
}
/// 收集 cell 中需要上传的本地路径
fn collect_upload_paths(
cell: &Value,
image_paths: &mut HashSet<String>,
file_paths: &mut HashSet<String>,
) {
if let Value::Array(arr) = cell {
for item in arr {
if let Some(p) = get_str(item, "image_path") {
image_paths.insert(p.to_string());
}
if let Some(p) = get_str(item, "file_path") {
file_paths.insert(p.to_string());
}
}
}
}
/// 用上传结果替换 cell 中的本地路径
fn replace_upload_results(
cell: &mut Value,
image_map: &HashMap<String, ImageUploadResult>,
file_map: &HashMap<String, FileUploadResult>,
) {
if let Value::Array(arr) = cell {
for item in arr.iter_mut() {
// 图片:用上传结果替换 image_path
if let Some(p) = get_str(item, "image_path").map(String::from) {
if let Some(result) = image_map.get(&p) {
if let Some(obj) = item.as_object_mut() {
obj.remove("image_path");
obj.insert("image_url".to_string(), Value::String(result.url.clone()));
if let Some(title) = &result.title {
obj.insert("title".to_string(), Value::String(title.clone()));
}
}
}
}
// 附件:用上传结果替换 file_path
if let Some(p) = get_str(item, "file_path").map(String::from) {
if let Some(result) = file_map.get(&p) {
if let Some(obj) = item.as_object_mut() {
obj.remove("file_path");
obj.insert("file_id".to_string(), Value::String(result.fileid.clone()));
}
}
}
}
}
}
#[derive(Debug, Clone)]
struct ImageUploadResult {
url: String,
title: Option<String>,
}
#[derive(Debug, Clone)]
struct FileUploadResult {
fileid: String,
}
/// 图片最大 30 MB
const MAX_IMAGE_SIZE: u64 = 30 * 1024 * 1024;
/// 文件最大 10 MB
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
/// 读取本地文件并编码为 base64,超过 `max_size` 时报错。
async fn read_file_as_base64(path: &str, max_size: u64) -> Result<String> {
let data = tokio::fs::read(path)
.await
.with_context(|| format!("读取文件失败: {}", path))?;
let size = data.len() as u64;
if size > max_size {
anyhow::bail!(
"文件 {} 大小为 {:.1} MB,超过限制 {:.1} MB",
path,
size as f64 / 1024.0 / 1024.0,
max_size as f64 / 1024.0 / 1024.0,
);
}
Ok(base64::engine::general_purpose::STANDARD.encode(&data))
}
/// 批量上传图片,返回 path → ImageUploadResult 映射。
async fn upload_images(
paths: &[String],
doc_id: &DocIdentifier,
) -> Result<HashMap<String, ImageUploadResult>> {
eprintln!("正在上传 {} 张图片...", paths.len());
let mut map = HashMap::new();
for path in paths {
let base64_content = read_file_as_base64(path, MAX_IMAGE_SIZE).await?;
// 构造请求参数
let mut args = json!({ "base64_content": base64_content });
match doc_id {
DocIdentifier::DocId(id) => {
args["docid"] = Value::String(id.clone());
}
DocIdentifier::Url(url) => {
args["url"] = Value::String(url.clone());
}
}
let res = json_rpc::call_json_tool("doc", "upload_doc_image", args)
.await
.with_context(|| format!("上传图片失败: {}", path))?;
let url = res
.get("url")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
if url.is_empty() {
anyhow::bail!("图片 {} 上传失败:返回结果中缺少 url", path);
}
// 文件名作为 title
let title = Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.map(String::from);
map.insert(path.clone(), ImageUploadResult { url, title });
}
eprintln!("图片上传完成,成功 {}", map.len());
Ok(map)
}
/// 批量上传文件,返回 path → FileUploadResult 映射。
async fn upload_files(paths: &[String]) -> Result<HashMap<String, FileUploadResult>> {
eprintln!("正在上传 {} 个文件...", paths.len());
let mut map = HashMap::new();
for path in paths {
let file_base64_content = read_file_as_base64(path, MAX_FILE_SIZE).await?;
// 提取文件名
let file_name = Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let args = json!({
"file_name": file_name,
"file_base64_content": file_base64_content,
});
let res = json_rpc::call_json_tool("doc", "upload_doc_file", args)
.await
.with_context(|| format!("上传文件失败: {}", path))?;
let fileid = res
.get("fileid")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
if fileid.is_empty() {
anyhow::bail!("文件 {} 上传失败:返回结果中缺少 fileid", path);
}
map.insert(path.clone(), FileUploadResult { fileid });
}
eprintln!("文件上传完成,成功 {}", map.len());
Ok(map)
}
/// 获取远程工具 schema,并新增 `image_path` / `file_path` 字段。
pub async fn get_modified_schema(remote_method: &str) -> Result<Value> {
let tools = crate::registry::get_category_tools("doc").await?;
let tool = tools
.iter()
.find(|t| t.name == remote_method)
.ok_or_else(|| anyhow::anyhow!("远程工具不存在: {}", remote_method))?;
let mut schema = serde_json::to_value(tool)?;
replace_image_value_schema(&mut schema);
replace_attachment_value_schema(&mut schema);
Ok(schema)
}
/// 替换 schema 中的 CellImageValue 定义,只保留 image_path 字段。
fn replace_image_value_schema(schema: &mut Value) {
if let Some(defs) = schema
.pointer_mut("/inputSchema/$defs")
.and_then(|d| d.as_object_mut())
{
defs.insert(
"CellImageValue".to_string(),
json!({
"description": "图片类型字段的单元值。只需传入本地图片路径,系统自动上传。",
"properties": {
"image_path": {
"description": "本地图片文件路径。传入本地图片路径(如 /path/to/image.png 或 ./photo.jpg),系统会自动上传到文档服务器。",
"title": "Image Path",
"type": "string"
}
},
"required": ["image_path"],
"title": "CellImageValue",
"type": "object"
}),
);
}
}
/// 替换 schema 中的 CellAttachmentValue 定义,只保留 file_path 字段。
fn replace_attachment_value_schema(schema: &mut Value) {
if let Some(defs) = schema
.pointer_mut("/inputSchema/$defs")
.and_then(|d| d.as_object_mut())
{
defs.insert(
"CellAttachmentValue".to_string(),
json!({
"description": "附件类型字段的单元值。只需传入本地文件路径,系统自动上传。",
"properties": {
"file_path": {
"description": "传入本地文件路径,系统会自动上传到文档服务器。",
"title": "File Path",
"type": "string"
}
},
"required": ["file_path"],
"title": "CellAttachmentValue",
"type": "object"
}),
);
}
}
+4
View File
@@ -0,0 +1,4 @@
mod auto_file_upload;
pub mod smartpage_create;
pub mod smartsheet_add_records_auto_file;
pub mod smartsheet_update_records_auto_file;
+134
View File
@@ -0,0 +1,134 @@
use std::future::Future;
use std::pin::Pin;
use crate::{helpers::registry::Helper, json_rpc};
use anyhow::{Context, Result};
use clap::{ArgMatches, Args, Command, FromArgMatches};
use serde_json::{Value, json};
/// 创建智能文档(自动读取本地文件版本)
///
/// 与远程 smartpage_create 相同,但 pages 中的 page_content 改为 page_filepath
/// 执行时自动以 UTF-8 编码读取本地文件内容,填入 page_content 字段后调用后台接口。
#[derive(Args, Debug)]
pub struct SmartpageCreateArgs {
/// JSON 格式的参数(与远程 smartpage_create 相同,但 page_content 改为 page_filepath
#[arg(hide = true, value_name = "args")]
pub args: Option<String>,
/// JSON 格式的参数
#[arg(long)]
pub json: Option<String>,
/// 输出该命令的参数 schema
#[arg(long, action = clap::ArgAction::SetTrue)]
pub schema: bool,
}
pub struct SmartpageCreateHelper;
impl SmartpageCreateHelper {
/// 获取修改后的 schema:将 pages.items.properties 中的 page_content 替换为 page_filepath
async fn get_modified_schema() -> Result<Value> {
let tools = crate::registry::get_category_tools("doc").await?;
let tool = tools
.iter()
.find(|t| t.name == "smartpage_create")
.ok_or_else(|| anyhow::anyhow!("远程工具不存在: smartpage_create"))?;
let mut schema = serde_json::to_value(tool)?;
// 修改 schema:将 pages.items.properties 中的 page_content 替换为 page_filepath
if let Some(props) = schema
.pointer_mut("/inputSchema/properties/pages/items/properties")
.and_then(|v| v.as_object_mut())
{
props.remove("page_content");
props.insert(
"page_filepath".to_string(),
json!({
"description": "本地文件路径,以 UTF-8 编码读取文件内容作为子页面内容",
"type": "string"
}),
);
}
Ok(schema)
}
/// 处理 pages 数组:将 page_filepath 替换为 page_content(读取本地文件内容)
async fn process_pages(pages: &mut [Value]) -> Result<()> {
for (i, page) in pages.iter_mut().enumerate() {
let obj = page
.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("pages[{}] 不是一个对象", i))?;
// 如果存在 page_filepath,读取文件内容并替换为 page_content
if let Some(filepath_val) = obj.remove("page_filepath") {
let filepath = filepath_val
.as_str()
.ok_or_else(|| anyhow::anyhow!("pages[{}].page_filepath 必须是字符串", i))?;
let content = tokio::fs::read_to_string(filepath)
.await
.with_context(|| format!("读取文件失败: {}", filepath))?;
obj.insert("page_content".to_string(), Value::String(content));
}
}
Ok(())
}
}
impl Helper for SmartpageCreateHelper {
fn category(&self) -> &'static str {
"doc"
}
fn command(&self) -> clap::Command {
SmartpageCreateArgs::augment_args(
Command::new("+smartpage_create")
.about("创建智能主页,支持通过 page_filepath 自动读取本地文件内容作为子页面内容"),
)
}
fn execute<'a>(
&'a self,
matches: &'a ArgMatches,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async {
let args = SmartpageCreateArgs::from_arg_matches(matches)?;
// 输出修改后的 schema
if args.schema {
let schema = Self::get_modified_schema().await?;
println!("{}", serde_json::to_string_pretty(&schema)?);
return Ok(());
}
// 解析 JSON 参数
let raw = args
.json
.as_deref()
.or(args.args.as_deref())
.ok_or_else(|| anyhow::anyhow!("请提供 JSON 格式的参数"))?;
let mut params: Value = serde_json::from_str(raw).context("JSON 参数解析失败")?;
// 提取并处理 pages 数组
let pages = params
.get_mut("pages")
.and_then(|v| v.as_array_mut())
.ok_or_else(|| anyhow::anyhow!("参数中缺少 pages 数组"))?;
// 读取本地文件,将 page_filepath 替换为 page_content
Self::process_pages(pages).await?;
// 调用后台接口 smartpage_create
let res = json_rpc::call_tool("doc", "smartpage_create", params).await?;
println!("{res}");
Ok(())
})
}
}
@@ -0,0 +1,80 @@
use std::future::Future;
use std::pin::Pin;
use crate::{helpers::registry::Helper, json_rpc};
use anyhow::{Context, Result};
use clap::{ArgMatches, Args, Command, FromArgMatches};
use super::auto_file_upload;
/// 添加智能表格记录,支持通过 image_path/file_path 自动上传本地文件
#[derive(Args, Debug)]
pub struct SmartsheetAddRecordsAutoFileArgs {
/// JSON 格式的参数(与 smartsheet_add_records 相同,但图片/附件字段可传本地文件路径)
#[arg(hide = true, value_name = "args")]
pub args: Option<String>,
/// JSON 格式的参数
#[arg(long)]
pub json: Option<String>,
/// 输出该命令的参数 schema
#[arg(long, action = clap::ArgAction::SetTrue)]
pub schema: bool,
}
pub struct SmartsheetAddRecordsAutoFileHelper;
impl Helper for SmartsheetAddRecordsAutoFileHelper {
fn category(&self) -> &'static str {
"doc"
}
fn command(&self) -> clap::Command {
SmartsheetAddRecordsAutoFileArgs::augment_args(Command::new(
"+smartsheet_add_records_auto_file",
))
}
fn execute<'a>(
&'a self,
matches: &'a ArgMatches,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async {
let args = SmartsheetAddRecordsAutoFileArgs::from_arg_matches(matches)?;
if args.schema {
let schema =
auto_file_upload::get_modified_schema("smartsheet_add_records").await?;
println!("{}", serde_json::to_string_pretty(&schema)?);
return Ok(());
}
let raw = args
.json
.as_deref()
.or(args.args.as_deref())
.ok_or_else(|| anyhow::anyhow!("请提供 JSON 格式的参数"))?;
let mut params: serde_json::Value =
serde_json::from_str(raw).context("JSON 参数解析失败")?;
// 提取文档标识(docid 或 url),用于图片上传
let doc_id = auto_file_upload::extract_doc_identifier(&params)?;
// 提取并处理 records
let records = params
.get_mut("records")
.and_then(|v| v.as_array_mut())
.ok_or_else(|| anyhow::anyhow!("参数中缺少 records 数组"))?;
// 扫描并上传本地文件/图片,替换路径
auto_file_upload::process_records(records, &doc_id).await?;
// 调用后台接口 smartsheet_add_records
let res = json_rpc::call_tool("doc", "smartsheet_add_records", params).await?;
println!("{res}");
Ok(())
})
}
}
@@ -0,0 +1,80 @@
use std::future::Future;
use std::pin::Pin;
use crate::{helpers::registry::Helper, json_rpc};
use anyhow::{Context, Result};
use clap::{ArgMatches, Args, Command, FromArgMatches};
use super::auto_file_upload;
/// 更新智能表格记录,支持通过 image_path/file_path 自动上传本地文件
#[derive(Args, Debug)]
pub struct SmartsheetUpdateRecordsAutoFileArgs {
/// JSON 格式的参数(与 smartsheet_update_records 相同,但图片/附件字段可传本地文件路径)
#[arg(hide = true, value_name = "args")]
pub args: Option<String>,
/// JSON 格式的参数
#[arg(long)]
pub json: Option<String>,
/// 输出该命令的参数 schema
#[arg(long, action = clap::ArgAction::SetTrue)]
pub schema: bool,
}
pub struct SmartsheetUpdateRecordsAutoFileHelper;
impl Helper for SmartsheetUpdateRecordsAutoFileHelper {
fn category(&self) -> &'static str {
"doc"
}
fn command(&self) -> clap::Command {
SmartsheetUpdateRecordsAutoFileArgs::augment_args(Command::new(
"+smartsheet_update_records_auto_file",
))
}
fn execute<'a>(
&'a self,
matches: &'a ArgMatches,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async {
let args = SmartsheetUpdateRecordsAutoFileArgs::from_arg_matches(matches)?;
if args.schema {
let schema =
auto_file_upload::get_modified_schema("smartsheet_update_records").await?;
println!("{}", serde_json::to_string_pretty(&schema)?);
return Ok(());
}
let raw = args
.json
.as_deref()
.or(args.args.as_deref())
.ok_or_else(|| anyhow::anyhow!("请提供 JSON 格式的参数"))?;
let mut params: serde_json::Value =
serde_json::from_str(raw).context("JSON 参数解析失败")?;
// 提取文档标识(docid 或 url),用于图片上传
let doc_id = auto_file_upload::extract_doc_identifier(&params)?;
// 提取并处理 records
let records = params
.get_mut("records")
.and_then(|v| v.as_array_mut())
.ok_or_else(|| anyhow::anyhow!("参数中缺少 records 数组"))?;
// 扫描并上传本地文件/图片,替换路径
auto_file_upload::process_records(records, &doc_id).await?;
// 调用后台接口 smartsheet_update_records
let res = json_rpc::call_tool("doc", "smartsheet_update_records", params).await?;
println!("{res}");
Ok(())
})
}
}

Some files were not shown because too many files have changed in this diff Show More