Two distribution defects left installed skills stale (issue PeterGuy326#8):
1. skill setup resolved its source from cwd / exe-adjacent checkouts, so
an upgraded binary happily re-installed whatever stale checkout it was
run from, and agents kept routing 'create robot' into the chat dead end.
The skills/mono and skills/multi trees are now embedded (go:embed) and
setup defaults to that copy, materialized into ~/.dws/skills/<mode> —
upgrading the binary upgrades what setup installs. --source and
DWS_SKILL_SOURCE stay as explicit overrides, and an override that has
no skill root errors out instead of silently falling back.
2. dws upgrade's skill refresh (knownSkillDirs) skipped ~/.qoder and
~/.qoderwork, so Qoder/QoderWork hosts never got refreshed routing.
Added both to the dir table and its seven mirrors (npm install.js,
install.sh/ps1, install-skills.sh, homebrew template, package test,
verify-package-managers.sh).
setup-side detection and --target qoder/qoderwork already work on this
branch; this closes the remaining upgrade-side and source-side gaps.
The devapp endpoint was hardcoded to the pre gateway, so robot
create/list hit the pre environment for everyone. Derive it from the
configured MCP gateway base URL instead: production by default, pre
when ~/.dws/mcp_url points at the pre gateway.
Also bump quickstart doc to v1.0.51-dws-devapp and add a FAQ entry for
the production-side developer-identity check.
Ported from PR #407: `robot connect` supports the qoder/qoderwork channels,
but skill setup neither probed ~/.qoder / ~/.qoderwork in its all-targets
checklist nor knew a qoderwork install path, so Qoder/QoderWork hosts missed
the skill routing. Consistency is enforced by TestAgentSkillPathsCoversSetupHomes.
- printExecutionError: in JSON mode emit the structured error to stdout
(not stderr) so machine consumers parsing stdout get a parseable result;
update the 3 root_execute tests to assert the new contract.
- requireYesForDelete prompt: when stdin is not a TTY (piped/scripted/JSON
pipelines), do not show an interactive confirm that emits non-JSON and
blocks on stdin; return a structured validation error requiring --yes.
Extract the server-list endpoint path into a single discoveryAPIPath constant and move it from /cli/discovery/apis to /cli/discovery/apis/bamboo so future version bumps touch one place.
Only the path changes: the MCP base host stays on production https://mcp.dingtalk.com and the auth / skill / doctor endpoints are untouched. The edition DiscoveryURL hook (full-URL FetchServersFromURL) is unaffected. All mock/test fixtures are updated to the new path and a CHANGELOG 1.0.34 entry is added.
The conference-local stdio plugin runs an Initialize/ListTools handshake
during command-tree construction. When the DingTalk desktop client isn't
running it returns "本地服务未就绪", which was logged at Warn and printed
to stderr on every invocation.
Because discovery happens in NewRootCommandWithEngine — before
PersistentPreRunE applies --debug/--verbose via configureLogLevel — this
Warn showed regardless of flags, polluting output and misleading callers
(and LLMs) into treating it as the cause of an unrelated command error
(e.g. an auth error or PARAM_ERROR from a different server).
This is an expected, benign outcome for an optional local plugin:
commands that ship toolOverrides still register up-front via
registerStdioServerFromOverlay, so availability is unaffected. Downgrade
both discovery handshake failures to Debug so normal output stays clean.
* fix(drive): drop client-side Content-Type fallback on presigned OSS PUT
The drive helper used to set req.Header["Content-Type"] = fallbackMIME
whenever the prepare_upload response returned an empty headers map.
DingTalk drive uses OSS presigned URLs whose StringToSign is computed
against an empty Content-Type at signing time, so any client-side
Content-Type makes the signature OSS recomputes at PUT time differ
from the server's presignature → HTTP 403 SignatureDoesNotMatch.
This broke every `dws drive upload` for any file whose mime detects
to a non-empty value (image/png, application/pdf, etc.), which is
basically everything in practice.
Reproduction (against DingTalk drive):
dws drive upload --file any.png
→ [1/3] 获取上传凭证 any.png (X 字节, image/png)...
→ [2/3] 上传文件到 OSS...
→ OSS 上传失败 HTTP 403: SignatureDoesNotMatch
Fix: trust the server's headers map as authoritative. Empty map means
"no client-side headers needed" — do not infer and add Content-Type.
Removes the hasContentType / fallbackMIME path entirely; httpPutDriveFile
signature loses the fallbackMIME parameter.
Manual verification:
curl -X PUT -H "Content-Type:" --data-binary @file <same-presigned-url>
→ HTTP 200 (proves the only difference was the client-side Content-Type)
Note: aitable.go's upload-file helper deliberately keeps its
Set("Content-Type", mimeType) call because its OSS endpoint uses a
different signing mode (server includes the client-declared mime in
its signature computation, verified by running aitable upload-file
across 12 file types — all succeed). The two helpers must not be
unified without re-validating both endpoints.
Tests:
- TestHttpPutDriveFile_NoContentTypeWhenServerHeadersEmpty: guards
that an empty server headers map results in no Content-Type on PUT.
- TestHttpPutDriveFile_PassthroughServerHeaders: guards that server
headers (Content-Type, x-oss-*) are forwarded verbatim.
* feat(aitable): unhide attachment upload-file + clarify prepare-only command
AI agents that discover commands only via --help (e.g. Lobster, generic
LLM agents) currently hit a dead end when trying to upload an attachment
to an AITable:
- `dws aitable attachment` exposes only `upload` (prepare-only),
which returns uploadUrl + fileToken but does not actually upload.
- The real one-shot command `attachment upload-file` (which performs
prepare + HTTP PUT + return fileToken automatically) is marked
Hidden:true, so it is invisible in --help output.
Agents that don't read skills/references/products/aitable.md therefore
get stuck after step 1 — they call `upload`, receive an uploadUrl they
have no idea how to consume, attempt various wrong things (e.g. write
the uploadUrl into the record's attachment field as if it were a token),
fail, and fall back to "please use the UI to upload" messages, which
makes dws look broken even though the capability is fully implemented.
This is the same UX gap that lark-cli avoids with its highly visible
`base +record-upload-attachment` command — discoverability via --help
is the difference between "works for any agent" and "only works if the
agent reads our skill docs".
Changes:
- newAITableUploadFileCommand: remove Hidden:true so the command
appears in `dws aitable attachment --help`. Tighten Short to
explicitly mention the 3 steps it bundles. Long now also calls
out the prepare-only sibling and recommends this command as the
default for AI agents.
- newAITableAttachmentUploadCommand (prepare-only): add a Long
description that explicitly states this command is only step 1
of a 3-step flow, lists what an agent must do after (HTTP PUT,
then write fileToken into record attachment field with the exact
[{"fileToken":"ft_xxx"}] shape), and points to upload-file as
the recommended one-shot alternative. Updated Short to flag that
no file is uploaded by this command.
- aitable_upload_file_test: add TestAITableUploadFileCommandIsDiscoverable
to guard against re-introducing Hidden:true. The test includes a
rationale comment explaining the agent-discoverability gap.
The drive Content-Type fix in the preceding commit and this aitable
discoverability fix together restore end-to-end attachment upload
functionality for both human operators and AI agents that only read
--help.
* feat(root): show 'dws upgrade' guidance in dws --help when commands are missing
AI agents (and users) reading `dws --help` see only the discovered MCP
service list and utility command list, with no hint about what to do if
none of the listed commands fit their task. The natural failure mode is
to give up or hack around — but in many cases the right action is just
`dws upgrade`, because new capabilities and bugfixes ship continuously
and a missing command is usually a stale binary issue.
Two changes restore visibility of the root command's Long description so
this guidance can be surfaced:
- internal/app/root.go: set root.Long to a one-paragraph hint —
"if you hit a missing command, an error, or cannot complete the
task, try `dws upgrade` first; both the DingTalk OpenAPI surface
and dws CLI evolve continuously."
- internal/app/root_help.go: the custom SetHelpFunc that renders the
root help (renderRootHelp) replaces cobra's default template, which
had been silently dropping root.Long from --help output. Restore
rendering by appending root.Long (when non-empty) after the command
list, separated by a blank line. This matches cobra's default
behavior for the Long field while preserving the custom services /
utilities sections renderRootHelp introduced.
- internal/app/visibility_test.go: new TestRenderRootHelpIncludesLong
guards against re-introducing the regression. Uses a sentinel Long
string and asserts renderRootHelp output contains it verbatim. If a
future change rewrites the help renderer without preserving Long
rendering, this test fails immediately and reminds the author the
upgrade hint must stay visible.
Verified locally:
$ ./dws --help | tail -3
Use "dws <service> --help" for more information about a discovered MCP
service or "dws <command> --help" for utility commands.
提示: 如果遇到能力缺失、命令报错、新功能未注册、或无法完成任务,
请先用 'dws upgrade' 升级到最新版本后再试. 钉钉 OpenAPI 和 dws CLI
持续迭代, 新能力和 bugfix 会先在新版本上线.
GoReleaser cross-compiles darwin/arm64 binaries on ubuntu-latest with no
codesign step. macOS 11+ on Apple Silicon requires at least an ad-hoc
signature; unsigned arm64 binaries are SIGKILL'd by amfid on first exec,
which surfaces as `signal: killed` and aborts `dws upgrade` at the
"解压并验证" step.
Two layers of fix:
1. Release-side: post-goreleaser.sh now unpacks each dws-darwin-*.tar.gz,
applies an ad-hoc signature (codesign locally, rcodesign in CI),
deterministically repacks, and rewrites the matching line in
checksums.txt. release.yml installs rcodesign 0.27.0 before
GoReleaser runs.
2. Client-side self-heal: validateNewBinary detects `signal: killed` on
darwin and retries once after running `codesign --force --sign -` and
clearing com.apple.quarantine. Future releases stay functional even
if the signing step is ever skipped.
Verified end-to-end: stripped a real dws binary → exec exits 137 →
validateNewBinary recovers → final binary shows Signature=adhoc and runs.
* fix(auth): partition app.json filename by edition to isolate credentials
Two dws binaries sharing the same config directory (typically ~/.dws or
DWS_CONFIG_DIR) previously read and wrote a single app.json. Editions
that pin AuthClientID via hooks still go through the open-core
post-login persistence path, which records a bare ClientID without a
paired ClientSecret. The sibling edition reading the same path would
then adopt that foreign clientID via ResolveAppCredentials.
Mirror the strategy already used by the cache loader
(pkg/config.EditionPartition): GetAppConfigPath returns a filename
suffixed with the active edition name. Open-source keeps "app.json" for
backwards compatibility; sibling editions land on "app-<edition>.json".
LoadAppConfig / SaveAppConfig / HasAppConfig / DeleteAppConfig all
route through GetAppConfigPath, so this single change physically
isolates credential files end-to-end without any read-time heuristics.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(auth): address app config partition review
* fix(auth): clean legacy sibling app config
* test(auth): cover legacy app config cleanup guards
* fix(auth): close app config review follow-ups
---------
Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(app): prevent command field from overwriting existing endpoint
In AppendDynamicServer, when a plugin declares command != id, the command
endpoint is written unconditionally, overwriting any previously registered entry.
Fix: use first-writer-wins - only write if the key is not yet present.
id registration and dynamicProducts remain unconditional (unaffected).
* test(app): add regression test for command endpoint first-writer-wins guard
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* 1.0.19 changelog
* stick opt
* fix(cli): utf-8 safe sticky suffix guard + changelog (#272)
SuffixLooksLikeValue used to read suffix[0] (a single byte) for both the
uuid format branch and the fallback "is the first rune a letter?" check.
For multi-byte UTF-8 leading runes — common in dws because value text is
often Chinese — this picked up only the first byte (0xE0..0xF4 lead),
which is not a letter and not a hex digit, so the function silently
returned true and let glued tokens like --name<CJK> get split into
--name <CJK>... This switches both branches to utf8.DecodeRuneInString
and adds a utf8.RuneError guard so invalid UTF-8 input is rejected too.
Also locks down the new behaviour in CHANGELOG ## [Unreleased]:
- Changed: schema-aware sticky flag splitting
- Added: available_flags field on unknown-flag errors
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(output): add -f ndjson (implemented) and scaffold -f csv (#252)
`larksuite/cli` exposes --format ndjson / csv; dws only had json/table/raw/
pretty. This adds both as recognised global formats:
- ndjson: fully implemented (internal/output/ndjson.go) — top-level arrays
and well-known list wrappers ({items|results|data|records|...}) emit one
compact JSON document per line; anything else degrades to a single line.
Streaming-friendly counterpart to `-f json`.
- csv: scaffolded (internal/output/csv.go) — writeCSV currently returns a
clear "not implemented (#252)" error rather than silently degrading to
JSON. A TODO block spells out the planned implementation (reuse the table
renderer's extractRowsFromMap/rowsFromSlice flattening + encoding/csv).
Wired through Write() and normalizeFormat(); --format help string now lists
ndjson (csv to be added when writeCSV lands). Tests cover ndjson rendering
(array / wrapped-list / scalar) and pin the csv not-implemented contract.
Skeleton PR for #252 — ndjson is shippable as-is; csv is left for a
follow-up commit on this branch.
* feat(output): implement -f csv (#252)
Completes the CSV half of the format work. writeCSV mirrors the shape
decisions `-f table` already makes (reuses normalizePayload /
unwrapPrimaryObject / extractRowsFromMap / rowsFromSlice / formatValue) so
columns and value flattening are consistent between the two formats:
- a list of objects (bare or wrapped under items/results/data/records/...) →
header row + one row per element; union of keys sorted; missing values are
empty cells; nested objects/arrays render as compact JSON in the cell;
sibling metadata of the list (total, hasMore, ...) is dropped.
- a single object → two-column key,value CSV (keys sorted).
- a non-uniform list / scalar → single-column `value` CSV.
- empty / nil → empty document.
encoding/csv.Writer handles RFC-4180 quoting (commas, quotes, newlines);
cells go through formatValue (also strips terminal control sequences, same
as the table renderer). `--fields` projection composes for free since
WriteFiltered applies SelectFields before Write.
--format help now lists csv; FormatCSV doc comment dropped the WIP marker;
the not-implemented test is replaced with real coverage (list with
comma/CJK/nested-array-as-JSON, wrapped-list-with-metadata, single object,
scalar) plus a --fields composition test.
* feat(output): broadcast list metadata as trailing columns in -f csv (#252)
Per review preference: instead of dropping the list's sibling metadata
(total, hasMore, ...) when rendering {records:[...], total:N} as CSV, append
each meta key as a trailing column repeated on every row, so a CSV consumer
never silently loses it (CSV has no "footer table" the way the table
renderer does). Meta keys colliding with a data column are skipped; an empty
list still emits the header (data + meta) plus one row of empty data cells
carrying the meta values. New broadcastMeta helper; doc comment + tests
updated (incl. an empty-list-with-metadata case).
* feat(output): recognise real DingTalk envelope keys in -f csv/ndjson/table
之前 -f csv 和 -f ndjson 的 list 检测白名单只认 items/results/data/list/
records/tools/servers/products,但真实钉钉响应用的是 result(单数直接数组
或一层包裹)/documents/emailAccounts/todoCards/events/messages,导致大
部分 list 命令的 csv 输出退化成 key,value 二列、ndjson 退化成整包一行。
修复点:
1. preferredListKeys 扩展加上真实 envelope key(result/documents/
emailAccounts/todoCards/events/messages),并把它升级为 csv/table/
ndjson 共享的"单一事实源"——filter.go 的 findDataList 不再维护自己
的本地副本,直接复用这个列表。
2. extractRowsFromMap 改为委托 findDataList,自动获得"一层深度"的
wrapper 支持({result: {todoCards: [...]}} 这种 envelope 现在能识
别)。meta 合并:outer + inner 双层 sibling 拉通,outer 同名 key
优先(避免 inner 把外层 success/total 等覆盖掉)。
3. writeTableish 和 writeCSV 调整 unwrapPrimaryObject 和
extractRowsFromMap 的优先级——先试 list 检测,没命中再走 unwrap,
避免 {result: {todoCards: [...]}} 被 unwrap 剥掉外层后直接走
key,value 分支。
4. findDataList 允许"空数组+preferred key" 命中,保留原来"空 list +
metadata 仍渲染为表格 + meta 广播一行"的行为。
新增 TestTabularDetectsRealDingTalkEnvelopes:四个真实 envelope 形态
(contact user search 的 result 直接数组、doc search 的 documents 顶层、
mail mailbox list 的 emailAccounts、todo task list 的 result.todoCards
一层深度)验证 ndjson 行数和 csv 表头都符合预期。
真接口回归(已登录态跑过):
- dws contact user search -f csv → name/userId 列正常出表
- dws todo task list -f ndjson → 20 行一条任务,可 jq -r .subject 直接管
- dws doc search -f csv → 10 行 + nextPageToken 等 meta 广播尾列
- dws schema -f csv → 无回归
Closes part of #252 follow-up.
* docs(auth): correct login help to reflect actual default + SSH guidance (#226)
The --help long description for `dws auth login` claimed the default mode was
"OAuth 设备流 (默认)", but the actual default starts a 127.0.0.1 loopback
listener (oauth_provider.go:131-136) and only switches to device flow when
--device is passed. SSH-into-headless-Linux users following the docs hit a
dead end because the local browser cannot reach 127.0.0.1 on the remote box.
Rewrite the description so each method is named after its real flag:
- OAuth Loopback 流 (默认)
- OAuth 设备流 (--device)
- 直接提供 Token (--token)
Add an explicit warning callout and a `--device` example for SSH/headless
environments. Also realign two flagErrorWithSuggestions strings in root.go
that propagated the same misconception ("默认使用设备流" → loopback default,
SSH 用户加 --device).
No behavioural change.
* docs(auth): drop emoji from login help warning
The three custom http.Transport instances built by the CLI
(`internal/transport/client.go` MCP transport, `internal/apiclient/client.go`
DingTalk OpenAPI client, `internal/app/legacy.go` IPv4-forcing registry
client) all set DialContext / TLSClientConfig / timeouts but omit the Proxy
field. Per Go's net/http contract, a non-nil Transport without an explicit
Proxy means "no proxy" — env vars are silently ignored, breaking sandboxed
or air-gapped deployments that route outbound through HTTP_PROXY /
HTTPS_PROXY.
Set Proxy: http.ProxyFromEnvironment on all three. Adds a regression test
per package that pointer-compares the Transport's Proxy func against
http.ProxyFromEnvironment (avoids flakiness from Go's envProxyOnce
memoisation when running alongside tests that read proxy env early).
When two MCP servers register tools with the same name (e.g. both drive
and doc have 'create_folder'), the tool-level endpoint map uses
last-writer-wins, causing invocations to route to the wrong server.
Fix: swap Priority 1 (product-level) and Priority 2 (tool-level) in
directRuntimeEndpoint so that when the caller already knows the
productID, the product endpoint is authoritative. Tool-level lookup
remains as a fallback for cases where productID is empty.
This fixes 'dws drive mkdir' and 'dws drive download' being silently
routed to the doc MCP server instead of the drive MCP server.
Closes#219
Co-authored-by: 猷诺 <bicheng.ybc@alibaba-inc.com>
* feat(pat): A-core minimum viable loop for host-owned PAT
Enables third-party agents to integrate PAT via the smallest possible
contract surface. The flow a host needs to ship end-to-end:
1. Export DINGTALK_DWS_AGENTCODE in the spawned shell.
2. Run any `dws ...` business command.
3. On PAT hit, CLI exits with code 4 and writes a single-line stderr
JSON following docs/pat/contract.md §2.
4. Host parses the JSON and invokes `dws pat chmod <scope>...
--agentCode ... --grant-type ...` to grant.
5. Host replays the original command.
This PR intentionally ships ONLY the chmod path plus the host-owned
switch; the async flow (apply / status / scopes + authRequestId
registry + PAT_SCOPE_AUTH_REQUIRED active-request branch) lands in a
follow-up stacked PR (A-ext).
Scope of A-core:
- Host-owned PAT trigger: auth.HostOwnsPATFlow() keyed exclusively on
DINGTALK_DWS_AGENTCODE (DINGTALK_AGENT / DWS_CHANNEL / claw-type do
NOT participate).
- claw-type: hard-wired to "openClaw" in pkg/edition/default.go
MergeHeaders hook, matching historical main behavior and decoupled
from DINGTALK_AGENT.
- dws pat chmod: factory-built (newChmodCommand) so the PAT subcommand
tree has no shared package-level caller.
- stderr JSON classifier: internal/errors/pat.go covers PAT_NO_PERMISSION
/ PAT_LOW_RISK / PAT_MEDIUM_RISK / PAT_HIGH_RISK / PAT_SCOPE_*
selectors and fills data.hostControl for host consumption.
- Env contract: DINGTALK_SESSION_ID / DINGTALK_TRACE_ID /
DINGTALK_MESSAGE_ID carry HTTP trace headers (Chain B);
DWS_SESSION_ID (+ REWIND_SESSION_ID alias) is the lone fallback for
`dws pat chmod --session-id` (Chain A). The two chains are
independent; aliases do not cross-pollinate.
- Docs: docs/pat/{README,contract,host-integration}.md + refreshed
docs/architecture.md "PAT Architecture" chapter + docs/reference.md
PAT section. error-catalog.md folded into contract.md §6.
- Tests: host-owned signal, stderr contract, classifier, chmod factory,
retry / poll loop for the CLI-owned chmod path.
Compatibility: no breaking changes on main. All non-PAT commands are
untouched.
Follow-up PRs (in order):
- codex/pat-ext : dws pat apply / status / scopes + AsyncRegistry.
- codex/pat-oss-refactor (aka PR B) : oauth_helpers / secure_store
refactors split out per code review.
Made-with: Cursor
* fix(pat): unify host-owned stderr contract
* revert: drop PAT docs and changelog from pat core PR
* fix(pat): pin behavior auth endpoint
* fix(pat): recognize legacy tool miss
* fix(pat): fallback on gateway diagnostics
* fix(pat): treat authorization uri as opaque
* fix(pat): accept result envelope in device-flow polling
* feat(pat): add browser policy and poll compatibility
* fix(pat): keep CLI and host PAT contracts separate
* test(pat): isolate opaque uri retry env
* fix(pat): harden PAT result routing
* fix(pat): harden host-owned flow and runtime fallback
* fix(pat): preserve chmod failure on empty grant result
* refactor(pat): drop dead doc anchors, dedupe stderr injection helpers
Address two reviewer findings on top of the PAT-core series.
Fix:
- chmod: resolveSessionIDFromEnv silently selects DWS_SESSION_ID; drop
the slog.Warn that emitted both raw session ids into stderr /
~/.dws/logs.
- Remove every docs/pat/contract.md / error-catalog.md /
host-integration.md anchor from comments, help, and tests; those
files were never added by this branch. Comments are now self-contained
or point to docs/reference.md where applicable.
Refactor:
- internal/errors/pat.go: extract lookupCodeIn so getPATErrorCode and
getDWSGatewayErrorCode share one traversal; have ClassifyPatAuthCheck
delegate to getPATErrorCode instead of repeating the enum walk.
- internal/errors/pat.go + internal/app/pat_auth_retry.go: extract
ApplyHostMutations as the single injection point for data.hostControl
+ data.openBrowser; cleanPATJSON and enrichPATErrorForHostControl now
share it so the two stderr-JSON write paths cannot drift.
- internal/app/pat_auth_retry.go: drop buildPATScopeHostJSON and
buildHostControlState dead 1-line wrappers.
- internal/pat/chmod.go: drop unused patApply/Status/Scopes constants;
derive legacyToolArgs from toolArgs by clone + scopes->scope rename so
the two payloads stay in lock-step on every other field.
Net 15 files, +176 / -193. Verification:
env -u DINGTALK_DWS_AGENTCODE go test \
./internal/errors ./internal/auth ./internal/pat ./test/unit
env -u DINGTALK_DWS_AGENTCODE go test ./internal/app \
-run 'Test(IsPat|ExtractPat|PrintPat|PollPat|HandlePat|RetryWithPat|EnrichPAT|BuildPAT|DirectRuntime|ResolveIdentityHeaders)'
env -u DINGTALK_DWS_AGENTCODE go test -race \
./internal/pat ./internal/errors ./test/unit
env -u DINGTALK_DWS_AGENTCODE go test -race ./internal/app \
-run 'Test(HandlePat|RetryWithPat|EnrichPAT|BuildPAT|ResolveIdentityHeaders|DirectRuntime)'
go vet ./... && go build ./...
All green.
Made-with: Cursor
---------
Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
* fix(chat,cmdutil): restore group @-mentions and members list subcommand
Two CLI regressions surfaced after PR #170 promoted hardcoded helpers over
the discovery envelope:
1. issue #177 — `dws chat message send --group ... --at-users ...` failed
with `unknown flag: --at-users` on v1.0.16. The hardcoded helper that
replaced the v1.0.15 envelope leaf only declared --group / --user /
--open-dingtalk-id / --text / --title and silently dropped the envelope's
--at-users / --at-all / --at-mobiles flags, so any group @-mention call
bounced at cobra's flag parser before ever reaching the MCP tool.
Add the three flags back to `newChatMessageSendCommand` and forward
atUserIds / isAtAll / atMobiles to `send_message_as_user` in the
`--group` branch; reject the flags loudly outside `--group` to avoid
silently swallowing user intent in single-chat mode (single-chat tools
have no @-mention semantics).
2. issue #164 — `dws chat group members --id <openConversationId>` returned
`unknown flag: --id`. The helper had `members` as a "group with bare
RunE + --id/--cursor flags" while the envelope publishes `members` as a
leaf for `get_group_members`; the merge layer's shape-mismatch branch
treats envelope as authority and silently drops the entire helper
subtree (list / add / remove / add-bot all become unreachable).
Restructure helper-side `members` into a pure group container and
promote list to its own explicit subcommand:
dws chat group members list --id <openconversation_id>
And extend `pkg/cmdutil/MergeHardcodedLeaves` so the existing
OverridePriority annotation also applies to the leaf↔group shape
mismatch — when the helper group carries strictly higher priority it
replaces the dynamic leaf, mirroring the leaf↔leaf override semantics
that PR #170 already established. Without this the helper's `list`
subcommand stays unreachable and the user-visible bug persists.
Tests:
- `internal/helpers/chat_test.go`: forwards-at-mentions (group + each
flag variant), rejects-at-mentions-outside-group (single-chat
validation), members-list-subcommand (structure + execution).
- `internal/app/legacy_test.go`: pickCommands integration test that
reproduces the #164 shape mismatch end-to-end and asserts the helper
subtree survives.
- `pkg/cmdutil/leaf_merge_test.go`: positive + boundary (equal priority)
cases for the new leaf↔group promotion path.
Verified end-to-end against existing chat helper tests, app pickCommands
tests, cmdutil merge tests, cobracmd priority tests, compat dynamic
command tests — all green.
Skill docs (`skills/references/products/chat.md`) updated to teach the
new `members list` path with a migration note, and to surface the
restored `--at-mobiles` flag alongside `--at-all` / `--at-users`.
Closes#177Closes#164
* fix(chat): resolve merge conflict in command Long text
Combine --title-required wording from #174 with at-mention
documentation introduced in this PR. CI build was failing on
unresolved <<<<<<< / >>>>>>> markers left in chat.go.
pickCommands previously dropped an entire helper subtree whenever a
top-level product name collided with the dynamic overlay. The intent was
to keep the discovery envelope as runtime authority for leaves it
declared, but the side effect was that every helper-only sibling
disappeared too — commands like `chat message send-by-bot`,
`chat message recall-by-bot`, `chat message send-by-webhook`, and
`chat group members add-bot` had silently vanished from the open
edition since the pickCommands introduction, making PR #161's
send-by-bot routing fix unreachable for open-source users.
Switch to cmdutil.MergeHardcodedLeaves for same-named products: the
dynamic side still wins every leaf conflict, and helper-only subtrees
are grafted into the dynamic tree. Envelopes remain authoritative for
leaves they declare; helpers once again fill the gaps they don't.
Verified on a freshly built open-edition binary:
dws chat message send-by-bot --robot-code ... --users ... --text ...
returns a real processQueryKey and routes through canonical_product=bot
via helper_override, matching the v1.0.15 baseline.
* feat(compat): merge same-name subcommands under shared parent
When multiple server entries attach to the same parent and their cli.command
collides with a subcommand already in the parent tree, the incoming subcommand's
children are merged recursively into the existing one instead of producing a
duplicate sibling. Leaf-name collisions resolve first-wins.
Fixes the duplicate `group` / `message` rows in `dws chat --help` that surfaced
when bot capabilities were distributed across chat.group.members and
chat.message subtrees.
* feat(ir): carry FlagOverlay and ToolAnnotations into canonical catalog
FlagOverlay mirrors the per-parameter CLI overlay (alias, transform,
transformArgs, env default, default value, hidden) sourced from
market.CLIToolOverride.Flags.
ToolAnnotations mirrors MCP 2025+ tool annotations with nullable hints
(destructive / read-only / idempotent / open-world) so absence means
"unknown" rather than "false".
BuildCatalog now propagates both through the canonical layer, with tests
covering group metadata and flag-overlay passthrough.
* feat(output): add -f pretty for schema-aware colored output
dws schema now supports -f pretty alongside json / raw. Pretty mode
partitions output by product / tool / parameter / enum and applies ANSI
color so humans can browse the catalog without piping through jq.
json / raw behaviour is unchanged.
docs/reference.md gains a Schema Introspection section documenting
dws schema and the new pretty format.
* feat(skills): refresh 13-product references; add devdoc / drive / oa
SKILL.md cli_version bumped to >=1.0.15; product index reordered to match
the v1.0.15 command surface.
New references:
- devdoc.md: Open Platform documentation search
- drive.md: DingTalk drive (promoted out of Coming soon)
- oa.md: OA approval end-to-end flow
Major rewrites for aitable (dashboard / chart share, import/export,
attachment flow), chat (bot capabilities merged into chat.message and
chat.group.members), calendar (event suggest / attachments), doc (comment
subtree, file create, upload/download flow), and minutes (list
mine/shared/all unified, record subcommands split).
Minor alignment touch-ups for contact / report / todo flag names.
* docs(release): sync v1.0.15 notes — 159 commands across 13 products
README Key Services fully refreshed: chat 23, calendar 14, aitable 41,
doc 21, minutes 19; drive promoted out of Coming soon; workbench and
standalone bot rows removed; Quick Start expanded to 7 examples spanning
doc / minutes / drive; Coming soon trimmed to mail / conference / aiapp /
live / wiki.
Adds docs/command-index.md, an auto-generated English listing of all 159
runtime commands with description and when-to-use guidance aimed at AI
agents. Replaces the ad-hoc command-index.pre.* / command-index.full.*
snapshots used during development.
CHANGELOG 1.0.15 records the compat subcommand-merging feature, the new
command index, and a flag-naming cleanup across chat / calendar / drive /
minutes / contact / devdoc.
PR #126 removed the privileged managed-plugin mechanism but kept
LoadManaged, IsManaged, and ~/.dws/plugins/managed/ fallbacks so that
plugins installed by pre-#126 builds would keep loading. The window
for that migration was ~4 days (2026-04-15 → 2026-04-19), and the
original install path mostly failed anyway (issue #124 / GitHub Pages
HTML response), so there is no real user base to preserve.
Scope of deletion:
- plugin.Loader.LoadManaged, scanDir's isManaged param, loadPlugin's
isManaged param. LoadAll now combines user + dev only.
- Plugin.IsManaged field. Converter/root.go no longer branch on it;
stdio and HTTP descriptors both report source="plugin".
- ~/.dws/plugins/managed/ branches in ListInstalled, SetEnabled,
RemovePlugin.
- config.PluginManagedDir constant.
- TestLoaderLoadManaged, TestRemoveLegacyManagedPlugin, and the
"legacy managed" table case in TestRemovePluginPurgesSettings.
Kept on purpose:
- Manifest.Type public schema field ("managed"|"user") — it appears
in existing plugin.json files and is only validated, never
behaviorally consumed.
Users with an orphaned ~/.dws/plugins/managed/ directory can
`rm -rf ~/.dws/plugins/managed/` — the CLI no longer reads it.
Wire plugin module + help command + OAuth client-id/secret flags through
the existing i18n catalog so --help is consistent Chinese under zh locale
and English under en locale, instead of mixing the two.
- internal/app/plugin_cmd.go: wrap 16 Short strings with i18n.T
- internal/app/flags.go: wrap --client-id/--client-secret descriptions
- internal/app/root_help.go: override cobra default help command with a
localized one so "dws help --help" and the utility-commands listing
share the same catalog
- internal/i18n/locales/{en,zh}.json: add 20 new catalog entries
Verified under DWS_LANG=zh (all Chinese), DWS_LANG=en (original English
preserved), and LANG-based auto-routing. go test ./internal/app/... pass.
Co-authored-by: 修雨 <huyizhou.hyz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat: PAT scope error visualization and auto-retry with authorization polling
1. PAT error result visualization (non-JSON)
- Human-readable error output matching lark-cli style
- Shows error type, message, hint, and authorization command
- JSON output also available via --format json
2. Auto-retry with polling after authorization wall
- Detects missing_scope / insufficient_scope / permission errors
- Polls every 5s for token update after user completes auth
- 10 minute timeout before giving up
- Automatically retries the original command after successful auth
Changes:
- internal/app/pat_auth_retry.go: Core PAT error handling and retry logic
- internal/app/pat_auth_retry_test.go: 11 unit tests
- internal/app/runner.go: Integration with executeInvocation error path
- internal/auth/device_flow.go: Add SetScope() method
* feat: migrate PAT (Permission Authorization) infrastructure to open-source core
Move PAT permission management code from dws-wukong overlay to the
open-source dingtalk-workspace-cli core. This enables PAT handling
in the public distribution while keeping dws-wukong fully compatible
through the edition.Hooks mechanism.
Changes:
- pkg/edition: add 6 new Hooks fields (AuthClientID, AuthClientFromMCP,
SaveToken, LoadToken, DeleteToken, ClassifyToolResult) for overlay
extensions to inject custom auth/token/error behaviour
- internal/errors/pat: new PATError type with ExitCode()=4 and
RawStderr() for embedded-mode passthrough, plus ClassifyToolResultContent
and ClassifyMCPResponseText classification functions
- internal/pat: new command group (pat chmod) with tool result handling
- internal/app/root: register PAT commands before RegisterExtraCommands hook
- internal/app/runner: integrate ClassifyToolResult hook in the
callResult.IsError branch so overlays can intercept PAT/gateway errors
before generic handling
The dws-wukong overlay remains unchanged — its existing pat/ package and
RegisterExtraCommands hook continue to work. Through the go.mod replace
directive and deduplicateCommands mechanism, both codebases coexist
without conflict.
* feat(pat): add PAT auth check with device flow polling and auto-retry
- Add ClassifyPatAuthCheck / AsPatAuthCheckError in pat.go for
AGENT_CODE_NOT_EXISTS error detection
- Add handlePatAuthCheck in pat_auth_retry.go: inject clientId as
x-robot-uid header, poll device flow endpoint, auto-retry on APPROVED
- Integrate PAT check into runner.go executeInvocation pipeline
(edition hook + open-source fallback)
- Add patAuthRequiredCodes map for extensible auth-required code matching
- Remove test_parse.go (temporary test script)
* fix(pat): update poll path to /cli/oauth/device/poll and add x-user-access-token header
- Change DevicePollPath from /api/dingtalk-workspace-cli/oauth/device/poll
to /cli/oauth/device/poll (aligning with backend endpoint convention)
- Add x-user-access-token header to poll requests (loaded from stored token)
* feat(auth): PAT device flow improvements and flowId empty guard
- pat_auth_retry: fix JSON field parsing (result -> data) using DevicePollResponse
- pat_auth_retry: fix elapsed time calculation based on start timestamp
- device_flow: add flowId empty guard - skip polling and show auth URL for manual handling
- device_flow: unify terminalBaseURL to GetMCPBaseURL for polling endpoint
- device_flow: remove unused PollFlowApproval dead code
- runner: integrate DWS_CLIENT_ID env and app.json persistence
- oauth_provider: add x-robot-uid header removal on logout
- client: add request/response logging for transport debugging
* feat(env): switch all service discovery URLs from pre-release to production
- endpoints: pre-login/pre-api/pre-mcp/pre-open-dev -> login/api/mcp/open-dev
- loader: DefaultMarketBaseURL -> mcp.dingtalk.com
- registry: defaultBaseURL -> mcp.dingtalk.com
- constants: DefaultTerminalBaseURL -> open-dev.dingtalk.com
* fix: remove residual merge conflict markers and duplicate Hooks fields in edition.go; unify developer-settings URL via config helper in device_flow.go
* chore: remove lark-cli reference from PrintPatAuthError comment
* test: add comprehensive unit tests for PAT auth modules
- Create internal/errors/pat_test.go (38 tests): covers ClassifyToolResultContent,
ClassifyMCPResponseText, ClassifyPatAuthCheck, AsPatAuthCheckError, cleanPATJSON,
stripClassFields, getDWSGatewayErrorCode, isNotLoggedInError, isBusinessError,
IsPATError, IsPATNoPermissionCode, suggestForBusinessErrorText
- Supplement pat_auth_retry_test.go (+7 tests): IsPatRetrying context checks,
pollPatDeviceFlow edge cases (server error fallback, SSO redirect skip),
extractPatScopeError nil/identity extraction
* feat(pat): exchange authCode for fresh token after PAT APPROVED
Previously pollPatDeviceFlow discarded the authCode from the poll
response, so handlePatAuthCheck retried with the stale token after
APPROVED. This mirrors device_flow.go loginOnce which calls
exchangeCode → SaveTokenData.
Changes:
- pollPatDeviceFlow: return (status, authCode, error) instead of
(status, error); extract Data.AuthCode on APPROVED
- handlePatAuthCheck APPROVED path: call ExchangeCodeForToken +
SaveTokenData before ResetRuntimeTokenCache, with graceful
fallback on exchange failure
- Update all 6 poll test cases for new 3-return signature; add
authCode assertions for APPROVED/REJECTED/EXPIRED/CANCELLED/
ServerError scenarios
* fix(pat): CR round-2 must-fix items
- scopeValueRegex: support multi-segment scopes (mail:a.b:send)
Updated regex to ([a-zA-Z][a-zA-Z0-9_.]*(?::[a-zA-Z][a-zA-Z0-9_.]*)+)
- handlePatAuthCheck: add 3 integration tests (APPROVED/REJECTED/EmptyFlowID)
Covers the 132-line main orchestrator with mock runner + httptest server
- Extract ParseDeviceFlowStatus + status constants to auth package
Eliminates string literal duplication across device_flow.go and pat_auth_retry.go
* fix(pat): use direct OAuth path when clientSecret is provided
When PAT error response includes both clientId and clientSecret,
use SetClientID() (direct DingTalk API mode) instead of
SetClientIDFromMCP() (MCP proxy mode). The MCP proxy does not
hold the secret for the PAT-assigned app, causing HTTP 400
'invalidParameter.idOrSecret.notFound' on token exchange.
Now: clientSecret present → direct mode; absent → MCP proxy mode.
---------
Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>