Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc22f53b92 | ||
|
|
654bcc4ecb | ||
|
|
2175f2fe59 | ||
|
|
81ca098db1 | ||
|
|
3e3c17d686 | ||
|
|
964855373e | ||
|
|
8fb1dcda86 | ||
|
|
94deafbaa9 | ||
|
|
2555447c7b | ||
|
|
6e91b2d142 |
@@ -9,7 +9,7 @@
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Go-1.25+-green?logo=go&logoColor=white" alt="Go 1.25+">
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue" alt="License Apache-2.0"></a>
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases"><img src="https://img.shields.io/badge/release-v1.1.0-red" alt="v1.1.0"></a>
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases"><img src="https://img.shields.io/badge/release-v1.0.4-red" alt="v1.0.4"></a>
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/actions/workflows/ci.yml"><img src="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<img src=".github/badges/coverage.svg" alt="Coverage">
|
||||
</p>
|
||||
@@ -139,7 +139,23 @@ export DWS_CLIENT_SECRET=<your-app-secret>
|
||||
dws auth login
|
||||
```
|
||||
|
||||
> CLI flags take precedence over environment variables. Credentials are used for DingTalk's OAuth device flow.
|
||||
<details>
|
||||
<summary><strong>Credential Configuration Priority</strong></summary>
|
||||
|
||||
`client-id` and `client-secret` support multiple configuration methods with the following priority (highest to lowest):
|
||||
|
||||
| Priority | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| 1 | CLI flags / Persisted config | `--client-id` / `--client-secret` command-line arguments; auto-saved after first successful login with `client-secret` stored in system Keychain |
|
||||
| 2 | Environment variables | `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET` |
|
||||
| 3 | Default values | Hardcoded defaults (for development only) |
|
||||
|
||||
**Recommended usage**:
|
||||
- **First login**: Use `--client-id` and `--client-secret` flags; credentials are securely persisted after successful login
|
||||
- **Subsequent use**: Run `dws` commands directly; token refresh automatically reads saved credentials from Keychain
|
||||
- **CI/CD environments**: Use environment variables
|
||||
|
||||
</details>
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -183,7 +199,7 @@ Agents don't need pre-built knowledge of every command. Use `dws schema` to dyna
|
||||
dws schema --jq '.products[] | {id, tool_count: (.tools | length)}'
|
||||
|
||||
# Step 2: Inspect target tool's parameter schema
|
||||
dws schema aitable.query_records --jq '.tool.input_schema'
|
||||
dws schema aitable.query_records --jq '.tool.parameters'
|
||||
|
||||
# Step 3: Construct the correct call
|
||||
dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
|
||||
@@ -191,7 +207,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
|
||||
|
||||
### Agent Skills
|
||||
|
||||
The repo ships Agent Skills (`SKILL.md` files) for every DingTalk product. After installing, tools like Claude Code / Cursor can use DingTalk capabilities directly:
|
||||
The repo ships a complete Agent Skill system (`skills/`). After installing, AI tools like Claude Code / Cursor can operate DingTalk directly through natural language:
|
||||
|
||||
```bash
|
||||
# Install skills into current project
|
||||
@@ -200,7 +216,40 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace
|
||||
|
||||
> `install.sh` installs to `$HOME/.agents/skills/dws` (global); `install-skills.sh` installs to `./.agents/skills/dws` (current project).
|
||||
|
||||
Author your own Agent Skills and orchestrate them with dws skills for cross-product workflows: **ISV Skill → dws Skill → DingTalk Open Platform API (enforced auth + full audit)**.
|
||||
**What's included:**
|
||||
|
||||
| Component | Path | Description |
|
||||
|-----------|------|-------------|
|
||||
| Master Skill | `SKILL.md` | Intent routing, decision tree, safety rules, error handling |
|
||||
| Product references | `references/products/*.md` | Per-product command reference (aitable, chat, calendar, etc.) |
|
||||
| Intent guide | `references/intent-guide.md` | Disambiguation for confusing scenarios (e.g. report vs todo) |
|
||||
| Global reference | `references/global-reference.md` | Auth, output formats, global flags |
|
||||
| Error codes | `references/error-codes.md` | Error codes + debugging workflows |
|
||||
| Recovery guide | `references/recovery-guide.md` | `RECOVERY_EVENT_ID` handling |
|
||||
| Ready-made scripts | `scripts/*.py` | 13 batch operation scripts (see below) |
|
||||
|
||||
<details>
|
||||
<summary><strong>Ready-made scripts</strong> — 13 Python scripts for common multi-step workflows</summary>
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `calendar_schedule_meeting.py` | Create event + add participants + find & book available meeting room |
|
||||
| `calendar_free_slot_finder.py` | Find common free slots across multiple people, recommend best meeting time |
|
||||
| `calendar_today_agenda.py` | View today/tomorrow/this week's schedule |
|
||||
| `import_records.py` | Batch import records from CSV/JSON into AITable |
|
||||
| `bulk_add_fields.py` | Batch add fields to an AITable data table |
|
||||
| `upload_attachment.py` | Upload attachment to AITable attachment field |
|
||||
| `todo_batch_create.py` | Batch create todos from JSON (with priority, due date, executors) |
|
||||
| `todo_daily_summary.py` | Summarize today/this week's incomplete todos |
|
||||
| `todo_overdue_check.py` | Scan overdue todos and output overdue list |
|
||||
| `contact_dept_members.py` | Search department by name and list all members |
|
||||
| `attendance_my_record.py` | View my attendance records for today/this week/specific date |
|
||||
| `attendance_team_shift.py` | Query team shift schedules and attendance statistics |
|
||||
| `report_inbox_today.py` | View today's received reports with details |
|
||||
|
||||
</details>
|
||||
|
||||
**ISV Integration**: Author your own Agent Skills and orchestrate them with dws skills for cross-product workflows: **ISV Skill → dws Skill → DingTalk Open Platform API (enforced auth + full audit)**.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -253,7 +302,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --fields invocati
|
||||
```bash
|
||||
dws schema # list all products and tools
|
||||
dws schema aitable.query_records # view parameter schema
|
||||
dws schema aitable.query_records --jq '.tool.input_schema.required' # view required fields
|
||||
dws schema aitable.query_records --jq '.tool.required' # view required fields
|
||||
dws schema --jq '.products[].id' # extract all product IDs
|
||||
```
|
||||
|
||||
@@ -280,21 +329,22 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
|
||||
## Key Services
|
||||
|
||||
| Service | Command | Description |
|
||||
|---------|---------|-------------|
|
||||
| Contact | `contact` | Users / departments |
|
||||
| Chat | `chat` | Group management / members / bot messaging / webhook |
|
||||
| Calendar | `calendar` | Events / meeting rooms / free-busy |
|
||||
| Todo | `todo` | Task management |
|
||||
| Approval | `oa` | Processes / forms / instances |
|
||||
| Attendance | `attendance` | Clock-in / shifts / statistics |
|
||||
| Ding | `ding` | DING messages / send / recall |
|
||||
| Report | `report` | Reports / templates / statistics |
|
||||
| AITable | `aitable` | AI table operations |
|
||||
| Workbench | `workbench` | App query |
|
||||
| DevDoc | `devdoc` | Open platform docs search |
|
||||
| Service | Command | Tools | Subcommands | Description |
|
||||
|---------|---------|:-----:|-------------|-------------|
|
||||
| Contact | `contact` | 8 | `user` `dept` | Search users by name/mobile, batch query, departments, current user profile |
|
||||
| Chat | `chat` | 14 | `message` `group` `bot` `search` | Group CRUD, member management, message history, topic replies, send as user |
|
||||
| Bot | `chat bot` | 9 | — | Robot creation, group/single messaging, webhook, message recall |
|
||||
| Calendar | `calendar` | 13 | `event` `room` `participant` `busy` | Events CRUD, meeting room booking, free-busy query, participant management |
|
||||
| Todo | `todo` | 6 | `task` | Create, list, update, done, get detail, delete |
|
||||
| Approval | `oa` | 9 | `approval` | Approve/reject/revoke, pending tasks, initiated instances, process list |
|
||||
| Attendance | `attendance` | 4 | `record` `shift` `summary` `rules` | Clock-in records, shift schedules, attendance summary, group rules |
|
||||
| Ding | `ding` | 3 | `message` | Send/recall DING messages |
|
||||
| Report | `report` | 7 | `create` `list` `detail` `template` `stats` `sent` | Create reports, sent/received list, templates, statistics |
|
||||
| AITable | `aitable` | 27 | `base` `table` `record` `field` `attachment` `template` | Full CRUD for bases/tables/records/fields, views, import/export, templates |
|
||||
| Workbench | `workbench` | 2 | `app` | Batch query app details |
|
||||
| DevDoc | `devdoc` | 2 | `article` | Search platform docs and error codes |
|
||||
|
||||
Run `dws --help` for the full list, or `dws <service> --help` for subcommands.
|
||||
> 104 tools across 12 products. Run `dws --help` for the full list, or `dws <service> --help` for subcommands.
|
||||
|
||||
<details>
|
||||
<summary>Coming soon</summary>
|
||||
|
||||
+70
-20
@@ -9,7 +9,7 @@
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Go-1.25+-green?logo=go&logoColor=white" alt="Go 1.25+">
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue" alt="License Apache-2.0"></a>
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases"><img src="https://img.shields.io/badge/release-v1.1.0-red" alt="v1.1.0"></a>
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases"><img src="https://img.shields.io/badge/release-v1.0.4-red" alt="v1.0.4"></a>
|
||||
<a href="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/actions/workflows/ci.yml"><img src="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<img src=".github/badges/coverage.svg" alt="Coverage">
|
||||
</p>
|
||||
@@ -139,7 +139,23 @@ export DWS_CLIENT_SECRET=<your-app-secret>
|
||||
dws auth login
|
||||
```
|
||||
|
||||
> CLI 参数优先于环境变量。凭证用于钉钉 OAuth 设备流认证。
|
||||
<details>
|
||||
<summary><strong>凭证配置优先级</strong></summary>
|
||||
|
||||
`client-id` 和 `client-secret` 支持多种配置方式,按以下优先级生效(从高到低):
|
||||
|
||||
| 优先级 | 配置方式 | 说明 |
|
||||
|------|----------|------|
|
||||
| 1 | CLI 参数 / 持久化配置 | `--client-id` / `--client-secret` 命令行参数;首次登录成功后自动保存,`client-secret` 存储在系统 Keychain 中 |
|
||||
| 2 | 环境变量 | `DWS_CLIENT_ID` / `DWS_CLIENT_SECRET` |
|
||||
| 3 | 默认值 | 代码中的预设值(仅开发时使用) |
|
||||
|
||||
**推荐用法**:
|
||||
- **首次登录**:使用 `--client-id` 和 `--client-secret` 参数,登录成功后凭证会自动安全存储
|
||||
- **后续使用**:直接运行 `dws` 命令,Token 自动刷新时会从 Keychain 读取已保存的凭证
|
||||
- **CI/CD 环境**:建议使用环境变量配置
|
||||
|
||||
</details>
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -183,7 +199,7 @@ Agent 无需预置所有命令知识,通过 `dws schema` 动态发现可用能
|
||||
dws schema --jq '.products[] | {id, tool_count: (.tools | length)}'
|
||||
|
||||
# 第二步:查看目标工具的参数结构
|
||||
dws schema aitable.query_records --jq '.tool.input_schema'
|
||||
dws schema aitable.query_records --jq '.tool.parameters'
|
||||
|
||||
# 第三步:构造正确的调用
|
||||
dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
|
||||
@@ -191,7 +207,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
|
||||
|
||||
### Agent Skills
|
||||
|
||||
仓库为每个钉钉产品提供 Agent Skill(`SKILL.md`),安装后 Claude Code / Cursor 等工具可直接使用钉钉能力:
|
||||
仓库内置完整的 Agent Skill 体系(`skills/`),安装后 Claude Code / Cursor 等 AI 工具可通过自然语言直接操作钉钉:
|
||||
|
||||
```bash
|
||||
# 安装 skills 到当前项目
|
||||
@@ -200,7 +216,40 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace
|
||||
|
||||
> `install.sh` 安装到 `$HOME/.agents/skills/dws`(全局);`install-skills.sh` 安装到 `./.agents/skills/dws`(当前项目)。
|
||||
|
||||
编写您自己的 Agent Skill,与 dws 内置 Skill 搭配构建跨产品工作流:**ISV Skill → dws Skill → 钉钉开放平台 API(强制鉴权 + 全链路审计)**。
|
||||
**包含内容:**
|
||||
|
||||
| 组件 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 主 Skill | `SKILL.md` | 意图路由、决策树、安全规则、错误处理 |
|
||||
| 产品参考 | `references/products/*.md` | 各产品命令详细参考(aitable、chat、calendar 等) |
|
||||
| 意图指南 | `references/intent-guide.md` | 易混淆场景消歧(如 report vs todo) |
|
||||
| 全局参考 | `references/global-reference.md` | 认证、输出格式、全局 flag |
|
||||
| 错误码 | `references/error-codes.md` | 错误码 + 调试流程 |
|
||||
| Recovery 指南 | `references/recovery-guide.md` | `RECOVERY_EVENT_ID` 处理 |
|
||||
| 现成脚本 | `scripts/*.py` | 13 个批量操作脚本(见下方) |
|
||||
|
||||
<details>
|
||||
<summary><strong>现成脚本</strong> — 13 个 Python 脚本,覆盖常见多步工作流</summary>
|
||||
|
||||
| 脚本 | 说明 |
|
||||
|------|------|
|
||||
| `calendar_schedule_meeting.py` | 一键创建日程 + 添加参与者 + 搜索并预定空闲会议室 |
|
||||
| `calendar_free_slot_finder.py` | 查询多人共同空闲时段,推荐最佳会议时间 |
|
||||
| `calendar_today_agenda.py` | 查看今天/明天/本周的日程安排 |
|
||||
| `import_records.py` | 从 CSV/JSON 批量导入记录到 AI 表格 |
|
||||
| `bulk_add_fields.py` | 批量添加字段到 AI 表格数据表 |
|
||||
| `upload_attachment.py` | 上传附件到 AI 表格 attachment 字段 |
|
||||
| `todo_batch_create.py` | 从 JSON 文件批量创建待办(含优先级、截止时间、执行者) |
|
||||
| `todo_daily_summary.py` | 汇总今天/本周未完成的待办 |
|
||||
| `todo_overdue_check.py` | 扫描已过截止时间但未完成的待办,输出逾期清单 |
|
||||
| `contact_dept_members.py` | 按部门名称搜索并列出所有成员 |
|
||||
| `attendance_my_record.py` | 查看我今天/本周/指定日期的考勤记录 |
|
||||
| `attendance_team_shift.py` | 查询团队成员本周排班和出勤统计 |
|
||||
| `report_inbox_today.py` | 查看今天收到的日志列表及详情 |
|
||||
|
||||
</details>
|
||||
|
||||
**ISV 集成**:编写您自己的 Agent Skill,与 dws 内置 Skill 搭配构建跨产品工作流:**ISV Skill → dws Skill → 钉钉开放平台 API(强制鉴权 + 全链路审计)**。
|
||||
|
||||
## 功能特性
|
||||
|
||||
@@ -253,7 +302,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --fields invocati
|
||||
```bash
|
||||
dws schema # 列出所有产品和工具
|
||||
dws schema aitable.query_records # 查看参数 Schema
|
||||
dws schema aitable.query_records --jq '.tool.input_schema.required' # 查看必填字段
|
||||
dws schema aitable.query_records --jq '.tool.required' # 查看必填字段
|
||||
dws schema --jq '.products[].id' # 提取所有产品 ID
|
||||
```
|
||||
|
||||
@@ -280,21 +329,22 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
|
||||
## 核心服务
|
||||
|
||||
| 服务 | 命令 | 描述 |
|
||||
|---------|---------|-------------|
|
||||
| 通讯录 | `contact` | 用户 / 部门 |
|
||||
| 群聊 | `chat` | 群管理 / 群成员 / 机器人消息 / Webhook |
|
||||
| 日历 | `calendar` | 日程 / 会议室 / 闲忙 |
|
||||
| 待办 | `todo` | 任务管理 |
|
||||
| 审批 | `oa` | 流程 / 表单 / 实例 |
|
||||
| 考勤 | `attendance` | 打卡 / 排班 / 统计 |
|
||||
| DING | `ding` | DING 消息 / 发送 / 撤回 |
|
||||
| 日志 | `report` | 日志 / 模版 / 统计 |
|
||||
| 智能表格 | `aitable` | AI 表格操作 |
|
||||
| 工作台 | `workbench` | 应用查询 |
|
||||
| 开发者文档 | `devdoc` | 开放平台文档搜索 |
|
||||
| 服务 | 命令 | 工具数 | 子命令 | 描述 |
|
||||
|------|------|:------:|--------|------|
|
||||
| 通讯录 | `contact` | 8 | `user` `dept` | 按姓名/手机号搜索、批量查询、部门树、当前用户信息 |
|
||||
| 群聊 | `chat` | 14 | `message` `group` `bot` `search` | 群增删改查、成员管理、消息拉取、话题回复、以用户身份发消息 |
|
||||
| 机器人 | `chat bot` | 9 | — | 机器人创建、群聊/单聊消息、Webhook、消息撤回 |
|
||||
| 日历 | `calendar` | 13 | `event` `room` `participant` `busy` | 日程增删改查、会议室预订、闲忙查询、参与者管理 |
|
||||
| 待办 | `todo` | 6 | `task` | 创建、列表、修改、完成、详情、删除 |
|
||||
| 审批 | `oa` | 9 | `approval` | 同意/拒绝/撤销、待我审批、我发起的、流程列表 |
|
||||
| 考勤 | `attendance` | 4 | `record` `shift` `summary` `rules` | 打卡记录、排班查询、考勤摘要、考勤组规则 |
|
||||
| DING | `ding` | 3 | `message` | 发送/撤回 DING 消息 |
|
||||
| 日志 | `report` | 7 | `create` `list` `detail` `template` `stats` `sent` | 创建日志、收发列表、模版、统计 |
|
||||
| 智能表格 | `aitable` | 27 | `base` `table` `record` `field` `attachment` `template` | 多维表/数据表/记录/字段全量 CRUD、视图、导入导出、模板 |
|
||||
| 工作台 | `workbench` | 2 | `app` | 批量查询应用详情 |
|
||||
| 开发者文档 | `devdoc` | 2 | `article` | 搜索开放平台文档与错误码 |
|
||||
|
||||
运行 `dws --help` 查看完整列表,或 `dws <service> --help` 查看子命令。
|
||||
> 12 个产品,104 个工具。运行 `dws --help` 查看完整列表,或 `dws <service> --help` 查看子命令。
|
||||
|
||||
<details>
|
||||
<summary>即将推出</summary>
|
||||
|
||||
@@ -188,8 +188,11 @@ func newAuthLogoutCommand() *cobra.Command {
|
||||
if err := authpkg.DeleteTokenData(configDir); err != nil {
|
||||
return apperrors.NewInternal(fmt.Sprintf("failed to clear token data: %v", err))
|
||||
}
|
||||
// Clean up app credentials (app.json + keychain secret)
|
||||
_ = authpkg.DeleteAppConfig(configDir)
|
||||
_ = os.Remove(filepath.Join(configDir, "mcp_url"))
|
||||
_ = os.Remove(filepath.Join(configDir, "token"))
|
||||
_ = os.Remove(filepath.Join(configDir, "token.json"))
|
||||
clearCompatCache()
|
||||
w := cmd.OutOrStdout()
|
||||
fmt.Fprintln(w, "[OK] 已清除所有认证信息")
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
)
|
||||
|
||||
const (
|
||||
// appConfigFile is the filename for storing app credentials.
|
||||
appConfigFile = "app.json"
|
||||
)
|
||||
|
||||
// AppConfig represents the application credentials configuration.
|
||||
// This is stored in ~/.dws/app.json with the client secret securely stored in keychain.
|
||||
type AppConfig struct {
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret SecretInput `json:"clientSecret"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Cached app config for performance (avoid repeated file reads).
|
||||
var (
|
||||
cachedAppConfig *AppConfig
|
||||
cachedAppConfigOnce sync.Once
|
||||
cachedAppConfigMu sync.RWMutex
|
||||
)
|
||||
|
||||
// GetAppConfigPath returns the path to the app config file.
|
||||
func GetAppConfigPath(configDir string) string {
|
||||
return filepath.Join(configDir, appConfigFile)
|
||||
}
|
||||
|
||||
// LoadAppConfig loads the app configuration from disk.
|
||||
// Returns nil, nil if the config file does not exist.
|
||||
func LoadAppConfig(configDir string) (*AppConfig, error) {
|
||||
path := GetAppConfigPath(configDir)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading app config: %w", err)
|
||||
}
|
||||
|
||||
var config AppConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("parsing app config: %w", err)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SaveAppConfig saves the app configuration to disk.
|
||||
// If the client secret is a plain string, it will be stored in keychain
|
||||
// and the config file will contain a reference to it.
|
||||
func SaveAppConfig(configDir string, config *AppConfig) error {
|
||||
// Store plain secret in keychain, convert to reference
|
||||
if config.ClientSecret.IsPlain() && config.ClientID != "" {
|
||||
storedRef, err := StoreSecret(config.ClientID, config.ClientSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("storing client secret: %w", err)
|
||||
}
|
||||
config.ClientSecret = storedRef
|
||||
}
|
||||
|
||||
// Update timestamps
|
||||
if config.CreatedAt.IsZero() {
|
||||
config.CreatedAt = time.Now()
|
||||
}
|
||||
config.UpdatedAt = time.Now()
|
||||
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling app config: %w", err)
|
||||
}
|
||||
|
||||
path := GetAppConfigPath(configDir)
|
||||
if err := helpers.AtomicWriteJSON(path, append(data, '\n')); err != nil {
|
||||
return fmt.Errorf("writing app config: %w", err)
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cachedAppConfigMu.Lock()
|
||||
cachedAppConfig = config
|
||||
cachedAppConfigMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAppConfig removes the app configuration and associated keychain secrets.
|
||||
func DeleteAppConfig(configDir string) error {
|
||||
// Load existing config to clean up keychain
|
||||
existing, _ := LoadAppConfig(configDir)
|
||||
if existing != nil {
|
||||
RemoveSecretStore(existing.ClientSecret)
|
||||
}
|
||||
|
||||
// Remove config file
|
||||
path := GetAppConfigPath(configDir)
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("removing app config: %w", err)
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
cachedAppConfigMu.Lock()
|
||||
cachedAppConfig = nil
|
||||
cachedAppConfigMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCachedAppConfig returns the cached app configuration.
|
||||
// It loads from disk on first call and caches the result.
|
||||
// Returns nil if no configuration exists or loading fails.
|
||||
func GetCachedAppConfig(configDir string) *AppConfig {
|
||||
cachedAppConfigOnce.Do(func() {
|
||||
cfg, err := LoadAppConfig(configDir)
|
||||
if err == nil && cfg != nil {
|
||||
cachedAppConfigMu.Lock()
|
||||
cachedAppConfig = cfg
|
||||
cachedAppConfigMu.Unlock()
|
||||
}
|
||||
})
|
||||
|
||||
cachedAppConfigMu.RLock()
|
||||
defer cachedAppConfigMu.RUnlock()
|
||||
return cachedAppConfig
|
||||
}
|
||||
|
||||
// ReloadAppConfig forces a reload of the app configuration from disk.
|
||||
// This should be called after SaveAppConfig to ensure the cache is updated.
|
||||
func ReloadAppConfig(configDir string) (*AppConfig, error) {
|
||||
cfg, err := LoadAppConfig(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cachedAppConfigMu.Lock()
|
||||
cachedAppConfig = cfg
|
||||
cachedAppConfigMu.Unlock()
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// HasAppConfig returns true if an app configuration file exists.
|
||||
func HasAppConfig(configDir string) bool {
|
||||
path := GetAppConfigPath(configDir)
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ResolveAppCredentials resolves the client ID and secret from the app config.
|
||||
// Returns empty strings if the config doesn't exist or resolution fails.
|
||||
func ResolveAppCredentials(configDir string) (clientID, clientSecret string) {
|
||||
cfg := GetCachedAppConfig(configDir)
|
||||
if cfg == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
clientID = cfg.ClientID
|
||||
secret, err := ResolveSecret(cfg.ClientSecret)
|
||||
if err == nil {
|
||||
clientSecret = secret
|
||||
}
|
||||
return clientID, clientSecret
|
||||
}
|
||||
@@ -149,9 +149,16 @@ func (p *DeviceFlowProvider) loginOnce(ctx context.Context, attempt int) (*Token
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.T("换取 token 失败"), err)
|
||||
}
|
||||
|
||||
// Save token data with associated client ID for refresh
|
||||
tokenData.ClientID = p.clientID
|
||||
if err := SaveTokenData(p.configDir, tokenData); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.T("保存 token 失败"), err)
|
||||
}
|
||||
|
||||
// Persist app credentials if using custom client credentials
|
||||
oauthProvider.persistAppConfigIfNeeded()
|
||||
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ package auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -84,8 +85,9 @@ func SetClientSecret(secret string) {
|
||||
|
||||
// ClientID returns the OAuth client ID with priority:
|
||||
// 1. Runtime override (CLI flag --client-id)
|
||||
// 2. Environment variable (DWS_CLIENT_ID)
|
||||
// 3. Default hardcoded value
|
||||
// 2. Persisted app config (from previous login)
|
||||
// 3. Environment variable (DWS_CLIENT_ID)
|
||||
// 4. Default hardcoded value
|
||||
func ClientID() string {
|
||||
clientMu.RLock()
|
||||
override := runtimeClientID
|
||||
@@ -93,6 +95,10 @@ func ClientID() string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
// Try loading from persisted app config
|
||||
if id, _ := ResolveAppCredentials(getDefaultConfigDir()); id != "" {
|
||||
return id
|
||||
}
|
||||
if v := os.Getenv("DWS_CLIENT_ID"); v != "" {
|
||||
return v
|
||||
}
|
||||
@@ -101,8 +107,9 @@ func ClientID() string {
|
||||
|
||||
// ClientSecret returns the OAuth client secret with priority:
|
||||
// 1. Runtime override (CLI flag --client-secret)
|
||||
// 2. Environment variable (DWS_CLIENT_SECRET)
|
||||
// 3. Default hardcoded value
|
||||
// 2. Persisted app config (from previous login, stored in keychain)
|
||||
// 3. Environment variable (DWS_CLIENT_SECRET)
|
||||
// 4. Default hardcoded value
|
||||
func ClientSecret() string {
|
||||
clientMu.RLock()
|
||||
override := runtimeClientSecret
|
||||
@@ -110,8 +117,38 @@ func ClientSecret() string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
// Try loading from persisted app config (secret is in keychain)
|
||||
if _, secret := ResolveAppCredentials(getDefaultConfigDir()); secret != "" {
|
||||
return secret
|
||||
}
|
||||
if v := os.Getenv("DWS_CLIENT_SECRET"); v != "" {
|
||||
return v
|
||||
}
|
||||
return DefaultClientSecret
|
||||
}
|
||||
|
||||
// getRuntimeCredentials returns the runtime-override credentials if set.
|
||||
// Returns empty strings if no runtime overrides were provided.
|
||||
func getRuntimeCredentials() (clientID, clientSecret string) {
|
||||
clientMu.RLock()
|
||||
defer clientMu.RUnlock()
|
||||
return runtimeClientID, runtimeClientSecret
|
||||
}
|
||||
|
||||
// getEnvClientID returns the environment variable client ID if set.
|
||||
func getEnvClientID() string {
|
||||
return os.Getenv("DWS_CLIENT_ID")
|
||||
}
|
||||
|
||||
// getDefaultConfigDir returns the default configuration directory.
|
||||
// Priority: DWS_CONFIG_DIR env var > ~/.dws
|
||||
func getDefaultConfigDir() string {
|
||||
if envDir := os.Getenv("DWS_CONFIG_DIR"); envDir != "" {
|
||||
return envDir
|
||||
}
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ".dws"
|
||||
}
|
||||
return filepath.Join(homeDir, ".dws")
|
||||
}
|
||||
|
||||
@@ -176,9 +176,16 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.T("换取 token 失败"), err)
|
||||
}
|
||||
|
||||
// Save token data with associated client ID for refresh
|
||||
tokenData.ClientID = p.clientID
|
||||
if err := SaveTokenData(p.configDir, tokenData); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.T("保存 token 失败"), err)
|
||||
}
|
||||
|
||||
// Persist app credentials if using custom client credentials
|
||||
p.persistAppConfigIfNeeded()
|
||||
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
@@ -290,3 +297,30 @@ func (p *OAuthProvider) Logout() error {
|
||||
func (p *OAuthProvider) Status() (*TokenData, error) {
|
||||
return LoadTokenData(p.configDir)
|
||||
}
|
||||
|
||||
// persistAppConfigIfNeeded saves app credentials if custom ones were used.
|
||||
// This ensures the client secret is available for future token refreshes.
|
||||
func (p *OAuthProvider) persistAppConfigIfNeeded() {
|
||||
// Check if custom credentials were provided via runtime flags
|
||||
clientID, clientSecret := getRuntimeCredentials()
|
||||
if clientID == "" || clientSecret == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Only persist if they differ from environment/default values
|
||||
envID := getEnvClientID()
|
||||
if clientID == envID || clientID == DefaultClientID {
|
||||
return
|
||||
}
|
||||
|
||||
// Save app config with secret stored in keychain
|
||||
config := &AppConfig{
|
||||
ClientID: clientID,
|
||||
ClientSecret: PlainSecret(clientSecret),
|
||||
}
|
||||
if err := SaveAppConfig(p.configDir, config); err != nil {
|
||||
if p.logger != nil {
|
||||
p.logger.Warn("failed to persist app credentials", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain"
|
||||
)
|
||||
|
||||
const (
|
||||
// secretKeyPrefix is the keychain account prefix for app secrets.
|
||||
secretKeyPrefix = "appsecret:"
|
||||
)
|
||||
|
||||
// SecretRef references a secret stored externally.
|
||||
type SecretRef struct {
|
||||
Source string `json:"source"` // "keychain" | "file"
|
||||
ID string `json:"id"` // keychain key or file path
|
||||
}
|
||||
|
||||
// SecretInput represents a secret value: either a plain string or a SecretRef object.
|
||||
type SecretInput struct {
|
||||
Plain string // non-empty for plain string values
|
||||
Ref *SecretRef // non-nil for SecretRef values
|
||||
}
|
||||
|
||||
// PlainSecret creates a SecretInput from a plain string.
|
||||
func PlainSecret(s string) SecretInput {
|
||||
return SecretInput{Plain: s}
|
||||
}
|
||||
|
||||
// IsZero returns true if the SecretInput has no value.
|
||||
func (s SecretInput) IsZero() bool {
|
||||
return s.Plain == "" && s.Ref == nil
|
||||
}
|
||||
|
||||
// IsSecretRef returns true if this is a SecretRef object.
|
||||
func (s SecretInput) IsSecretRef() bool {
|
||||
return s.Ref != nil
|
||||
}
|
||||
|
||||
// IsPlain returns true if this is a plain text string (not a SecretRef).
|
||||
func (s SecretInput) IsPlain() bool {
|
||||
return s.Ref == nil && s.Plain != ""
|
||||
}
|
||||
|
||||
// MarshalJSON serializes SecretInput: plain string → JSON string, SecretRef → JSON object.
|
||||
func (s SecretInput) MarshalJSON() ([]byte, error) {
|
||||
if s.Ref != nil {
|
||||
return json.Marshal(s.Ref)
|
||||
}
|
||||
return json.Marshal(s.Plain)
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes SecretInput from either a JSON string or a SecretRef object.
|
||||
func (s *SecretInput) UnmarshalJSON(data []byte) error {
|
||||
// Try string first
|
||||
var plain string
|
||||
if err := json.Unmarshal(data, &plain); err == nil {
|
||||
s.Plain = plain
|
||||
s.Ref = nil
|
||||
return nil
|
||||
}
|
||||
// Try SecretRef object
|
||||
var ref SecretRef
|
||||
if err := json.Unmarshal(data, &ref); err == nil && isValidSource(ref.Source) && ref.ID != "" {
|
||||
s.Ref = &ref
|
||||
s.Plain = ""
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("clientSecret must be a string or {source, id} object")
|
||||
}
|
||||
|
||||
// ValidSecretSources is the set of recognized SecretRef sources.
|
||||
var ValidSecretSources = map[string]bool{
|
||||
"file": true, "keychain": true,
|
||||
}
|
||||
|
||||
func isValidSource(source string) bool {
|
||||
return ValidSecretSources[source]
|
||||
}
|
||||
|
||||
// secretAccountKey generates the keychain account key for an app's secret.
|
||||
func secretAccountKey(clientID string) string {
|
||||
return secretKeyPrefix + clientID
|
||||
}
|
||||
|
||||
// ResolveSecret resolves a SecretInput to a plain string.
|
||||
// SecretRef objects are resolved by source (file / keychain).
|
||||
func ResolveSecret(input SecretInput) (string, error) {
|
||||
if input.Ref == nil {
|
||||
return input.Plain, nil
|
||||
}
|
||||
switch input.Ref.Source {
|
||||
case "file":
|
||||
data, err := os.ReadFile(input.Ref.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read secret file %s: %w", input.Ref.ID, err)
|
||||
}
|
||||
return strings.TrimSpace(string(data)), nil
|
||||
case "keychain":
|
||||
val, err := keychain.Get(keychain.Service, input.Ref.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get secret from keychain: %w", err)
|
||||
}
|
||||
return val, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown secret source: %s", input.Ref.Source)
|
||||
}
|
||||
}
|
||||
|
||||
// StoreSecret stores a plain text secret in keychain and returns a SecretRef.
|
||||
// If the input is already a SecretRef, it is returned as-is.
|
||||
// Returns error if keychain is unavailable.
|
||||
func StoreSecret(clientID string, input SecretInput) (SecretInput, error) {
|
||||
if !input.IsPlain() {
|
||||
return input, nil // SecretRef → keep as-is
|
||||
}
|
||||
key := secretAccountKey(clientID)
|
||||
if err := keychain.Set(keychain.Service, key, input.Plain); err != nil {
|
||||
return SecretInput{}, fmt.Errorf("keychain unavailable: %w\nhint: use file reference in config to bypass keychain", err)
|
||||
}
|
||||
return SecretInput{Ref: &SecretRef{Source: "keychain", ID: key}}, nil
|
||||
}
|
||||
|
||||
// RemoveSecretStore cleans up keychain entries when an app is removed.
|
||||
// Errors are intentionally ignored — cleanup is best-effort.
|
||||
func RemoveSecretStore(input SecretInput) {
|
||||
if input.IsSecretRef() && input.Ref.Source == "keychain" {
|
||||
_ = keychain.Remove(keychain.Service, input.Ref.ID)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ type TokenData struct {
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
CorpName string `json:"corp_name,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"` // Associated app client ID for refresh
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ cli_version: ">=1.1.0"
|
||||
## 意图判断决策树
|
||||
|
||||
用户提到"表格/多维表/AI表格/记录/数据" → `aitable`
|
||||
用户提到"审批/请假/报销/出差/加班" → `approval`
|
||||
用户提到"审批/请假/报销/出差/加班" → `oa`
|
||||
用户提到"考勤/打卡/排班" → `attendance`
|
||||
用户提到"日程/日历/会议室/约会" → `calendar`
|
||||
用户提到"群聊/建群/群成员/群管理/机器人发消息/Webhook/机器人群发/机器人单聊/通知" → `chat`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## 认证
|
||||
|
||||
```bash
|
||||
# 首次: 扫码登录 (浏览器自动打开)
|
||||
# 首次: OAuth 设备流登录 (钉钉扫码授权)
|
||||
dws auth login
|
||||
|
||||
# 查看状态
|
||||
@@ -11,6 +11,9 @@ dws auth status
|
||||
|
||||
# 退出
|
||||
dws auth logout
|
||||
|
||||
# 重置本地凭证 (Token 解密失败时使用)
|
||||
dws auth reset
|
||||
```
|
||||
|
||||
登录后自动管理 token 刷新,日常使用无需重复登录。
|
||||
@@ -28,11 +31,13 @@ dws auth logout
|
||||
### Headless 环境 (CI/CD)
|
||||
|
||||
```bash
|
||||
# 桌面: 导出凭证
|
||||
dws auth import credentials.json
|
||||
# 通过环境变量配置认证(无需交互式登录)
|
||||
export DWS_CLIENT_ID=<your-app-key>
|
||||
export DWS_CLIENT_SECRET=<your-app-secret>
|
||||
dws auth login
|
||||
|
||||
# 服务器: 导入凭证
|
||||
dws auth import credentials.json
|
||||
# 或使用 --device 设备流登录(远程服务器/Docker)
|
||||
dws auth login --device
|
||||
```
|
||||
refresh_token 单设备独占,远程刷新后源设备凭证失效。
|
||||
|
||||
@@ -59,12 +64,13 @@ dws recovery finalize --event-id <event_id> --outcome recovered|failed|handoff -
|
||||
| 标志 | 短名 | 说明 | 默认 |
|
||||
|------|:---:|------|------|
|
||||
| `--format` | `-f` | 输出格式: json / table / raw | json |
|
||||
| `--jq` | | jq 表达式过滤输出 (如: `.items[] \| .name`) | 无 |
|
||||
| `--fields` | | 筛选输出字段 (逗号分隔, 如: name,id,status) | 无 |
|
||||
| `--verbose` | `-v` | 详细日志 | false |
|
||||
| `--debug` | | 调试日志 | false |
|
||||
| `--yes` | `-y` | 跳过确认提示 | false |
|
||||
| `--dry-run` | | 预览操作不执行 | false |
|
||||
| `--timeout` | | HTTP 超时 (秒) | 30 |
|
||||
| `--token` | | API Token (覆盖配置) | 无 |
|
||||
| `--mock` | | Mock 数据 (开发用) | false |
|
||||
| `--client-id` | | 覆盖 OAuth Client ID | 无 |
|
||||
| `--client-secret` | | 覆盖 OAuth Client Secret | 无 |
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
| "帮我建一个明天下午的日程" | 日历日程 | `calendar` | — | 日历日程管理(可含参与者/会议室)|
|
||||
| "帮我看看收到的日报" | 日志收件箱 | `report` | `todo` | 钉钉日志系统(日报/周报),不是待办 |
|
||||
| "帮我创建一个待办提醒" | 个人待办 | `todo` | `report` | 个人任务提醒,不是日志汇报 |
|
||||
| "帮我提交请假审批" | 发起审批 | `approval` | — | 审批流程,不是待办或日志 |
|
||||
| "帮我提交请假审批" | 发起审批 | `oa` | — | 审批流程,不是待办或日志 |
|
||||
| "帮我建一个项目群" | 创建群聊 | `chat group create` | — | 群聊管理,不是日历日程 |
|
||||
| "把张三拉进群" | 添加群成员 | `chat group members add` | — | 先查 userId,再添加 |
|
||||
| "让机器人在群里发个通知" | 机器人群发 | `chat message send-by-bot` | `chat message send-by-webhook` | 企业内部机器人发消息,需 robotCode |
|
||||
@@ -91,7 +91,7 @@
|
||||
|
||||
```bash
|
||||
# 1. 搜索同事 userId
|
||||
dws contact user search --query "张三" --format json
|
||||
dws contact user search --keyword "张三" --format json
|
||||
|
||||
# 2. 创建日程
|
||||
dws calendar event create --title "会议" \
|
||||
@@ -107,7 +107,7 @@ dws calendar participant add --event <EVENT_ID> --users <USER_ID> --format json
|
||||
|
||||
```bash
|
||||
# 1. 搜索同事 userId
|
||||
dws contact user search --query "张三" --format json
|
||||
dws contact user search --keyword "张三" --format json
|
||||
|
||||
# 2. 创建待办
|
||||
dws todo task create --title "任务内容" --executors <USER_ID> --format json
|
||||
|
||||
@@ -236,13 +236,13 @@ Usage:
|
||||
Example:
|
||||
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>
|
||||
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --record-ids rec1,rec2
|
||||
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --query "关键词" --limit 50
|
||||
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --keyword "关键词" --limit 50
|
||||
Flags:
|
||||
--base-id string Base ID (必填)
|
||||
--cursor string 分页游标,首次不传
|
||||
--field-ids string 返回字段 ID 列表,逗号分隔,单次最多 100 个
|
||||
--filters string 结构化过滤条件 JSON
|
||||
--query string 全文关键词搜索
|
||||
--keyword string 全文关键词搜索
|
||||
--limit int 单次最大记录数,默认 100,最大 100
|
||||
--record-ids string 指定记录 ID 列表,逗号分隔,单次最多 100 个
|
||||
--sort string 排序条件 JSON 数组
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|-------|------|
|
||||
| `search` | 搜索群聊 |
|
||||
| `group create` | 创建群 |
|
||||
| `group members` | 查看群成员列表 |
|
||||
| `group members list` | 查看群成员列表 |
|
||||
| `group members add` | 添加群成员 |
|
||||
| `group members remove` | 移除群成员(⚠️ 危险操作) |
|
||||
| `group members add-bot` | 添加机器人到群 |
|
||||
@@ -48,13 +48,13 @@ Flags:
|
||||
|
||||
---
|
||||
|
||||
## group members — 查看群成员列表
|
||||
## group members list — 查看群成员列表
|
||||
|
||||
```
|
||||
Usage:
|
||||
dws chat group members [flags]
|
||||
dws chat group members list [flags]
|
||||
Example:
|
||||
dws chat group members --id <openConversationId> --format json
|
||||
dws chat group members list --id <openConversationId> --format json
|
||||
Flags:
|
||||
--id string 群会话 ID (必填)
|
||||
--cursor string 分页游标
|
||||
@@ -226,7 +226,7 @@ Flags:
|
||||
|
||||
- 用户说"搜索一个群" → `search`
|
||||
- 用户说"帮我建个群" → `group create`
|
||||
- 用户说"看看群里有谁" → `group members`
|
||||
- 用户说"看看群里有谁" → `group members list`
|
||||
- 用户说"把张三拉进群" → 先 `contact user search` 获取 userId,再 `group members add`
|
||||
- 用户说"把张三移出群" → 先 `contact user search` 获取 userId,再 `group members remove`(⚠️ 需确认)
|
||||
- 用户说"改一下群名" → `group rename`
|
||||
@@ -245,7 +245,7 @@ Flags:
|
||||
# ── 工作流: 建群并添加机器人 ──
|
||||
|
||||
# 1. 搜索同事 userId
|
||||
dws contact user search --query "张三" --format json
|
||||
dws contact user search --keyword "张三" --format json
|
||||
|
||||
# 2. 创建群
|
||||
dws chat group create --name "项目群" --users <userId1>,<userId2> --format json
|
||||
|
||||
@@ -17,9 +17,9 @@ Example:
|
||||
Usage:
|
||||
dws contact user search [flags]
|
||||
Example:
|
||||
dws contact user search --query "张三"
|
||||
dws contact user search --keyword "张三"
|
||||
Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
--keyword string 搜索关键词 (必填)
|
||||
```
|
||||
|
||||
#### 按手机号搜索用户
|
||||
@@ -49,9 +49,9 @@ Flags:
|
||||
Usage:
|
||||
dws contact dept search [flags]
|
||||
Example:
|
||||
dws contact dept search --query "技术部"
|
||||
dws contact dept search --keyword "技术部"
|
||||
Flags:
|
||||
--query string 搜索关键词 (必填)
|
||||
--keyword string 搜索关键词 (必填)
|
||||
```
|
||||
|
||||
#### 查看部门成员
|
||||
@@ -79,10 +79,10 @@ Flags:
|
||||
dws contact user get-self --format json
|
||||
|
||||
# 2. 按名字搜索同事 — 提取 userId
|
||||
dws contact user search --query "张三" --format json
|
||||
dws contact user search --keyword "张三" --format json
|
||||
|
||||
# 3. 查看部门结构 — 提取 deptId
|
||||
dws contact dept search --query "技术部" --format json
|
||||
dws contact dept search --keyword "技术部" --format json
|
||||
|
||||
# 4. 查看部门成员
|
||||
dws contact dept list-members --ids <deptId> --format json
|
||||
@@ -94,7 +94,7 @@ dws contact dept list-members --ids <deptId> --format json
|
||||
|------|------|------|
|
||||
| `user get-self/search` | `userId` | 其他产品中的 --users/--executor 参数 |
|
||||
| `user get-self/search` | `orgAuthEmail` | mail message send 的 --to/--cc (跨产品) |
|
||||
| `dept search/list-children` | `deptId` | dept list-children/list-members 的 --id/--ids |
|
||||
| `dept search` | `deptId` | dept list-members 的 --ids |
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -105,4 +105,4 @@ dws contact dept list-members --ids <deptId> --format json
|
||||
|
||||
| 脚本 | 场景 | 用法 |
|
||||
|------|------|------|
|
||||
| [contact_dept_members.py](../../scripts/contact_dept_members.py) | 按部门名称搜索并列出所有成员 | `python contact_dept_members.py --query "技术部"` |
|
||||
| [contact_dept_members.py](../../scripts/contact_dept_members.py) | 按部门名称搜索并列出所有成员 | `python contact_dept_members.py --keyword "技术部"` |
|
||||
|
||||
@@ -35,7 +35,7 @@ Flags:
|
||||
--template-id string 日志模版 ID (必填),从 template list 返回中取
|
||||
--contents string 日志内容 JSON 数组 (必填),每项须含 key/sort/content/contentType/type
|
||||
--dd-from string 创建来源标识 (默认 dws)
|
||||
--to-chat 是否发送到日志接收人单聊 (默认 false,传本 flag 则为 true)
|
||||
--to-chat string 是否发送到日志接收人单聊 (传 "true" 发送)
|
||||
--to-user-ids string 接收人 userId,逗号分隔 (可选)
|
||||
```
|
||||
|
||||
|
||||
@@ -18,32 +18,90 @@ Flags:
|
||||
--size string 每页数量 (默认 10)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## approval — 审批
|
||||
|
||||
### 查询审批表单
|
||||
### 搜索错误码
|
||||
```
|
||||
Usage:
|
||||
dws approval list-forms [flags]
|
||||
dws devdoc article search-error [flags]
|
||||
Example:
|
||||
dws approval list-forms --format json
|
||||
dws devdoc article search-error --keyword "403" --format json
|
||||
Flags:
|
||||
--keyword string 错误码或关键词 (必填)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## oa — 审批
|
||||
|
||||
### 查询可见审批流程
|
||||
```
|
||||
Usage:
|
||||
dws oa approval list-forms [flags]
|
||||
Example:
|
||||
dws oa approval list-forms --format json
|
||||
```
|
||||
|
||||
### 查询审批实例详情
|
||||
```
|
||||
Usage:
|
||||
dws approval detail --instance-id <ID> [flags]
|
||||
dws oa approval detail --instance-id <ID> [flags]
|
||||
Example:
|
||||
dws approval detail --instance-id <ID> --format json
|
||||
dws oa approval detail --instance-id <ID> --format json
|
||||
```
|
||||
|
||||
### 查询审批任务
|
||||
### 查询审批记录
|
||||
```
|
||||
Usage:
|
||||
dws approval tasks --instance-id <ID> [flags]
|
||||
dws oa approval records --instance-id <ID> [flags]
|
||||
Example:
|
||||
dws approval tasks --instance-id <ID> --format json
|
||||
dws oa approval records --instance-id <ID> --format json
|
||||
```
|
||||
|
||||
### 查询待我审批的任务
|
||||
```
|
||||
Usage:
|
||||
dws oa approval tasks [flags]
|
||||
Example:
|
||||
dws oa approval tasks --format json
|
||||
```
|
||||
|
||||
### 查询待我处理的审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval pending [flags]
|
||||
Example:
|
||||
dws oa approval pending --format json
|
||||
```
|
||||
|
||||
### 查询我发起的审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval initiated [flags]
|
||||
Example:
|
||||
dws oa approval initiated --format json
|
||||
```
|
||||
|
||||
### 同意审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval approve --instance-id <ID> --task-id <TASK_ID> [flags]
|
||||
Example:
|
||||
dws oa approval approve --instance-id <ID> --task-id <TASK_ID> --format json
|
||||
```
|
||||
|
||||
### 拒绝审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval reject --instance-id <ID> --task-id <TASK_ID> [flags]
|
||||
Example:
|
||||
dws oa approval reject --instance-id <ID> --task-id <TASK_ID> --remark "不符合要求" --format json
|
||||
```
|
||||
|
||||
### 撤销审批
|
||||
```
|
||||
Usage:
|
||||
dws oa approval revoke --instance-id <ID> [flags]
|
||||
Example:
|
||||
dws oa approval revoke --instance-id <ID> --format json
|
||||
```
|
||||
|
||||
---
|
||||
@@ -51,12 +109,20 @@ Example:
|
||||
## 意图判断
|
||||
|
||||
- 用户说"开发文档/API 文档/接口文档" → `devdoc article search`
|
||||
- 用户说"审批/请假/报销/出差" → `approval`
|
||||
- 用户说"API 报错/错误码" → `devdoc article search-error`
|
||||
- 用户说"审批/请假/报销/出差" → `oa approval`
|
||||
- 用户说"同意审批/批准" → `oa approval approve`
|
||||
- 用户说"拒绝审批/驳回" → `oa approval reject`
|
||||
- 用户说"撤销审批/撤回" → `oa approval revoke`
|
||||
- 用户说"待我审批/我要审批的" → `oa approval pending` 或 `oa approval tasks`
|
||||
- 用户说"我发起的审批" → `oa approval initiated`
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `devdoc article search` | 文档链接 | 直接展示给用户 |
|
||||
| `approval list-forms` | processCode | `approval detail` / `approval tasks` 等 |
|
||||
| `approval tasks` | taskId | `approval approve` / `approval reject` |
|
||||
| `oa approval list-forms` | processCode | detail / records 等 |
|
||||
| `oa approval tasks` | taskId, instanceId | approve / reject |
|
||||
| `oa approval pending` | instanceId | detail / approve / reject |
|
||||
| `oa approval initiated` | instanceId | detail / revoke |
|
||||
|
||||
@@ -8,12 +8,11 @@ Usage:
|
||||
dws todo task create [flags]
|
||||
Example:
|
||||
dws todo task create --title "修复线上Bug" --executors <USER_ID_1>,<USER_ID_2> --priority 40
|
||||
dws todo task create --title "每日站会" --executors <USER_ID> --due "2026-03-20T10:00:00+08:00" --recurrence "DTSTART:20260320T020000Z\nRRULE:FREQ=DAILY;INTERVAL=1"
|
||||
dws todo task create --title "提交报告" --executors <USER_ID> --due "2026-03-20T10:00:00+08:00"
|
||||
Flags:
|
||||
--due string 截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)
|
||||
--executors string 执行者 userId 列表 (必填)
|
||||
--priority string 优先级: 10低/20普通/30较高/40紧急
|
||||
--recurrence string 循环待办 (需先设置 --due); 仅支持按天循环,格式见下方说明
|
||||
--title string 待办标题 (必填)
|
||||
```
|
||||
|
||||
@@ -81,7 +80,6 @@ Flags:
|
||||
## 意图判断
|
||||
|
||||
用户说"加个待办/记一下/TODO" → `task create`
|
||||
用户说"每天重复/循环待办/按天重复" → `task create`(需 `--due` + `--recurrence`)
|
||||
用户说"看看待办/我有啥要做" → `task list`
|
||||
用户说"改个待办/修改待办标题/改优先级" → `task update`
|
||||
用户说"做完了/完成待办/标记完成" → `task done`
|
||||
@@ -97,11 +95,6 @@ Flags:
|
||||
dws todo task create --title "修复线上Bug" --executors userId1,userId2 \
|
||||
--priority 40 --due "2026-03-10T18:00:00+08:00" --format json
|
||||
|
||||
# 1b. 创建按天循环的待办(必须先有 --due;recurrence 与 MCP create_personal_todo 一致)
|
||||
dws todo task create --title "每日站会" --executors userId1 \
|
||||
--due "2026-03-20T10:00:00+08:00" \
|
||||
--recurrence "DTSTART:20260320T020000Z\nRRULE:FREQ=DAILY;INTERVAL=1" --format json
|
||||
|
||||
# 2. 查看未完成待办
|
||||
dws todo task list --page 1 --size 20 --status false --format json
|
||||
|
||||
@@ -129,7 +122,6 @@ dws todo task delete --task-id <taskId> --yes --format json
|
||||
|
||||
- 优先级值: 10=低, 20=普通, 30=较高, 40=紧急
|
||||
- `--due` 截止时间使用 ISO-8601 格式(如 2026-03-10T18:00:00+08:00)
|
||||
- `--recurrence`:仅在与 `--due` 同时设置时有效;当前仅支持按天循环。字符串内需含换行,示例:`DTSTART:20260320T020000Z\nRRULE:FREQ=DAILY;INTERVAL=1`(DTSTART 表示首次截止时间,需与业务约定一致)
|
||||
- `task list` 的 `--status` 对应 MCP `get_user_todos_in_current_org` 的 `todoStatus` 参数
|
||||
- todo 是个人待办管理产品
|
||||
- `task update` 可同时修改标题/优先级/截止时间/完成状态
|
||||
|
||||
Reference in New Issue
Block a user