feat: raw API update README.
This commit is contained in:
1 parent
592749c986
commit
e06b1d68ff
7 files changed
+67
-70
No files matched your search
@@ -289,8 +289,8 @@ dws auth login --client-id <APP_KEY> --client-secret <APP_SECRET>
|
||||
|
||||
# === api.dingtalk.com ===
|
||||
|
||||
# Get current user info
|
||||
dws api GET /v1.0/contact/users/me
|
||||
# List all enterprise apps
|
||||
dws api GET /v1.0/microApp/allApps
|
||||
|
||||
# Search users (POST + JSON body)
|
||||
dws api POST /v1.0/contact/users/search \
|
||||
@@ -301,16 +301,16 @@ dws api POST /v1.0/contact/users/search \
|
||||
# Get user details (use --base-url to specify domain)
|
||||
dws api POST /topapi/v2/user/get \
|
||||
--base-url https://oapi.dingtalk.com \
|
||||
--data '{"userid":"manager123"}'
|
||||
--data '{"userid":"<USER_ID>"}'
|
||||
|
||||
# Or use the full URL directly
|
||||
dws api POST https://oapi.dingtalk.com/topapi/v2/user/get \
|
||||
--data '{"userid":"manager123"}'
|
||||
--data '{"userid":"<USER_ID>"}'
|
||||
|
||||
# === General ===
|
||||
dws api GET /v1.0/attendance/groups --page-all # auto-paginate
|
||||
dws api GET /v1.0/contact/users/me --dry-run # preview request
|
||||
dws api GET /v1.0/contact/users/me --jq '.nick' # jq filtering
|
||||
dws api GET /v1.0/microApp/allApps --page-all # auto-paginate
|
||||
dws api GET /v1.0/microApp/allApps --dry-run # preview request
|
||||
dws api GET /v1.0/microApp/allApps --jq '.agentId' # jq filtering
|
||||
```
|
||||
|
||||
| Feature | Details |
|
||||
@@ -318,7 +318,7 @@ dws api GET /v1.0/contact/users/me --jq '.nick' # jq filtering
|
||||
| Dual-form auto-detection | Automatically selects api.dingtalk.com (header auth) or oapi.dingtalk.com (query-param auth) based on URL |
|
||||
| Automatic token management | App-level accessToken is fetched on first call, cached while valid, auto-refreshed on expiry |
|
||||
| Domain allowlist | Only `api.dingtalk.com` and `oapi.dingtalk.com` permitted — prevents token leakage |
|
||||
| Auto-pagination | `--page-all` iterates all pages; `--page-limit` caps the maximum |
|
||||
| Auto-pagination | `--page-all` iterates all pages. `--page-limit` caps the maximum (default 10, set to 0 for unlimited, hard cap at 500 to prevent infinite loops) |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+9
-9
@@ -281,7 +281,7 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace
|
||||
|
||||
`dws api` 让你直接调用任意钉钉 OpenAPI,无需 SDK,Token 自动获取和刷新。
|
||||
|
||||
> **前置条件**:必须使用自有应用凭证登录(见[自建应用模式](#开始使用))。通过 MCP 默认凭证登录的加密 token 不支持 raw API 调用。
|
||||
> **前置条件**:必须使用自有应用凭证登录(见[自建应用模式](#开始使用))。通过 MCP 默认凭证登录 不支持 raw API 调用。
|
||||
|
||||
```bash
|
||||
# 登录(仅首次)
|
||||
@@ -289,8 +289,8 @@ dws auth login --client-id <APP_KEY> --client-secret <APP_SECRET>
|
||||
|
||||
# === api.dingtalk.com ===
|
||||
|
||||
# 获取当前用户信息
|
||||
dws api GET /v1.0/contact/users/me
|
||||
# 获取企业所有应用列表
|
||||
dws api GET /v1.0/microApp/allApps
|
||||
|
||||
# 搜索用户 (POST + JSON body)
|
||||
dws api POST /v1.0/contact/users/search \
|
||||
@@ -301,16 +301,16 @@ dws api POST /v1.0/contact/users/search \
|
||||
# 获取用户详情(使用 --base-url 指定域名)
|
||||
dws api POST /topapi/v2/user/get \
|
||||
--base-url https://oapi.dingtalk.com \
|
||||
--data '{"userid":"manager123"}'
|
||||
--data '{"userid":"<USER_ID>"}'
|
||||
|
||||
# 也可以直接使用完整 URL
|
||||
dws api POST https://oapi.dingtalk.com/topapi/v2/user/get \
|
||||
--data '{"userid":"manager123"}'
|
||||
--data '{"userid":"<USER_ID>"}'
|
||||
|
||||
# === 通用功能 ===
|
||||
dws api GET /v1.0/attendance/groups --page-all # 自动翻页
|
||||
dws api GET /v1.0/contact/users/me --dry-run # 预览请求
|
||||
dws api GET /v1.0/contact/users/me --jq '.nick' # jq 过滤
|
||||
dws api GET /v1.0/microApp/allApps --page-all # 自动翻页
|
||||
dws api GET /v1.0/microApp/allApps --dry-run # 预览请求
|
||||
dws api GET /v1.0/microApp/allApps --jq '.agentId' # jq 过滤
|
||||
```
|
||||
|
||||
| 特性 | 说明 |
|
||||
@@ -318,7 +318,7 @@ dws api GET /v1.0/contact/users/me --jq '.nick' # jq 过滤
|
||||
| 双形态自动识别 | 根据 URL 自动选择 api.dingtalk.com(Header 认证)或 oapi.dingtalk.com(Query 参数认证) |
|
||||
| Token 自动管理 | 首次调用自动获取应用级 accessToken,有效期内缓存,过期自动刷新 |
|
||||
| 域名白名单 | 仅允许 `api.dingtalk.com` 和 `oapi.dingtalk.com`,防止 Token 泄露 |
|
||||
| 自动分页 | `--page-all` 自动遍历所有分页,`--page-limit` 控制上限 |
|
||||
| 自动分页 | `--page-all` 自动遍历所有分页。`--page-limit` 控制翻页上限(默认 10,设为 0 不限制,硬上限 500 防止死循环) |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -45,13 +45,13 @@ func newAPICommand(flags *GlobalFlags) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "api <METHOD> <PATH> [flags]",
|
||||
Short: "调用钉钉 OpenAPI (Raw HTTP)",
|
||||
Long: `直接调用钉钉 OpenAPI,支持新版和旧版两种 API 形态。
|
||||
Long: `直接调用钉钉 OpenAPI,支持 api.dingtalk.com 和 oapi.dingtalk.com 两个域名。
|
||||
|
||||
新版 API (api.dingtalk.com):
|
||||
api.dingtalk.com:
|
||||
Token 通过 HTTP Header (x-acs-dingtalk-access-token) 传递。
|
||||
路径格式: /v1.0/xxx 或 /v2.0/xxx
|
||||
|
||||
旧版 API (oapi.dingtalk.com):
|
||||
oapi.dingtalk.com:
|
||||
Token 通过 URL 查询参数 (access_token) 传递。
|
||||
路径格式: /topapi/v2/xxx 或完整 URL https://oapi.dingtalk.com/topapi/...
|
||||
|
||||
@@ -59,7 +59,7 @@ func newAPICommand(flags *GlobalFlags) *cobra.Command {
|
||||
通过 MCP 默认凭证登录获取的加密 token 不支持 raw API 调用。
|
||||
|
||||
示例:
|
||||
# === 新版 API (api.dingtalk.com) ===
|
||||
# === api.dingtalk.com ===
|
||||
|
||||
# 获取当前用户信息
|
||||
dws api GET /v1.0/contact/users/me
|
||||
@@ -72,9 +72,9 @@ func newAPICommand(flags *GlobalFlags) *cobra.Command {
|
||||
dws api POST /v1.0/calendar/users/me/calendars/primary/events \
|
||||
--data '{"summary":"Team Meeting","start":{"dateTime":"2026-01-01T10:00:00+08:00"}}'
|
||||
|
||||
# === 旧版 API (oapi.dingtalk.com) ===
|
||||
# === oapi.dingtalk.com ===
|
||||
|
||||
# 获取用户详情 (旧版,使用 --base-url)
|
||||
# 获取用户详情 (使用 --base-url)
|
||||
dws api POST /topapi/v2/user/get \
|
||||
--base-url https://oapi.dingtalk.com \
|
||||
--data '{"userid":"manager123"}'
|
||||
|
||||
@@ -15,9 +15,6 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/authretry"
|
||||
)
|
||||
|
||||
// authRetryingKey marks a context that has already attempted one
|
||||
@@ -41,12 +38,12 @@ func IsAuthRetrying(ctx context.Context) bool {
|
||||
// withAuthRetrying returns a child context flagged as "already retried once"
|
||||
// so the runner does not enter an infinite refresh loop if the second attempt
|
||||
// also returns AuthRefreshRequired.
|
||||
func withAuthRetrying(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithValue(ctx, authRetryingKey, true)
|
||||
}
|
||||
// func withAuthRetrying(ctx context.Context) context.Context {
|
||||
// if ctx == nil {
|
||||
// ctx = context.Background()
|
||||
// }
|
||||
// return context.WithValue(ctx, authRetryingKey, true)
|
||||
// }
|
||||
|
||||
// handleAuthRefreshRequired performs a one-shot ForceRefresh using the active
|
||||
// configDir and re-runs the invocation through the supplied runner. It must
|
||||
@@ -63,21 +60,21 @@ func withAuthRetrying(ctx context.Context) context.Context {
|
||||
// re-runs the invocation with withAuthRetrying applied so a second
|
||||
// refresh request from the overlay degrades gracefully to "show the
|
||||
// original error".
|
||||
func handleAuthRefreshRequired(
|
||||
ctx context.Context,
|
||||
r executor.Runner,
|
||||
invocation executor.Invocation,
|
||||
refresh *authretry.AuthRefreshRequired,
|
||||
) (executor.Result, error) {
|
||||
if refresh == nil {
|
||||
return executor.Result{}, nil
|
||||
}
|
||||
if IsAuthRetrying(ctx) {
|
||||
return executor.Result{}, refresh.Cause
|
||||
}
|
||||
if _, err := ForceRefreshAccessToken(ctx, defaultConfigDir()); err != nil {
|
||||
return executor.Result{}, refresh.Cause
|
||||
}
|
||||
ResetRuntimeTokenCache()
|
||||
return r.Run(withAuthRetrying(ctx), invocation)
|
||||
}
|
||||
// func handleAuthRefreshRequired(
|
||||
// ctx context.Context,
|
||||
// r executor.Runner,
|
||||
// invocation executor.Invocation,
|
||||
// refresh *authretry.AuthRefreshRequired,
|
||||
// ) (executor.Result, error) {
|
||||
// if refresh == nil {
|
||||
// return executor.Result{}, nil
|
||||
// }
|
||||
// if IsAuthRetrying(ctx) {
|
||||
// return executor.Result{}, refresh.Cause
|
||||
// }
|
||||
// if _, err := ForceRefreshAccessToken(ctx, defaultConfigDir()); err != nil {
|
||||
// return executor.Result{}, refresh.Cause
|
||||
// }
|
||||
// ResetRuntimeTokenCache()
|
||||
// return r.Run(withAuthRetrying(ctx), invocation)
|
||||
// }
|
||||
@@ -430,9 +430,9 @@ func printPerfReportSummary(w io.Writer, report *PerfReport) {
|
||||
fmt.Fprintf(w, " %-25s %dms (框架开销 %dms)\n", "总耗时", report.TotalMs, report.OverheadMs)
|
||||
}
|
||||
|
||||
func formatLocalTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Local().Format("2006-01-02 15:04")
|
||||
}
|
||||
// func formatLocalTime(t time.Time) string {
|
||||
// if t.IsZero() {
|
||||
// return ""
|
||||
// }
|
||||
// return t.Local().Format("2006-01-02 15:04")
|
||||
// }
|
||||
@@ -297,9 +297,9 @@ func getRuntimeCredentials() (clientID, clientSecret string) {
|
||||
}
|
||||
|
||||
// getEnvClientID returns the environment variable client ID if set.
|
||||
func getEnvClientID() string {
|
||||
return os.Getenv("DWS_CLIENT_ID")
|
||||
}
|
||||
// func getEnvClientID() string {
|
||||
// return os.Getenv("DWS_CLIENT_ID")
|
||||
// }
|
||||
|
||||
// getDefaultConfigDir returns the default configuration directory.
|
||||
// Priority: DWS_CONFIG_DIR env var > ~/.dws
|
||||
|
||||
@@ -573,15 +573,15 @@ func TestSyncSkills(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
|
||||
}
|
||||
// func contains(s, substr string) bool {
|
||||
// return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
|
||||
// }
|
||||
|
||||
func containsSubstring(s, sub string) bool {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// func containsSubstring(s, sub string) bool {
|
||||
// for i := 0; i+len(sub) <= len(s); i++ {
|
||||
// if s[i:i+len(sub)] == sub {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
Reference in new issue
Block a user