Commit Graph
94 Commits
Author SHA1 Message Date
Dennis 9b44eeb5b0 Merge origin/main into feature/shortcut 2026-07-15 17:38:58 +08:00
玉澜 0a7b6d8406 Merge upstream/main into feat/schema-on-main
Bring in #555 audit export/tail/verify public leaves for schema completeness.
2026-07-14 16:36:01 +08:00
修雨 27c3449036 fix(audit): drain forwards on error exit, fail CSV on corrupt JSONL
Address second-round review on PR #555:

- Move CloseAuditSink into the unconditional Execute defer so async remote
  forwards are drained on BOTH success and failure paths. Cobra skips
  PersistentPostRunE when RunE returns an error, which previously dropped
  in-flight forwards for failed commands. Make CloseAuditSink idempotent via
  sync.Once so the success-path hook and the defer can both call it.
- CSV export now returns a "文件:行号" error on malformed JSONL instead of
  silently skipping the line and exiting 0.
- Add regressions: TestCloseAuditSinkDrainsOnErrorPath (error-path drain),
  TestExportCSVFailsOnMalformedJSON (corrupt JSONL visible), and
  TestAuditIdentityReresolvesOnProfileSwitch (per-profile Actor via an
  injectable token loader seam).
2026-07-14 14:22:13 +08:00
修雨 d0787ce8ee fix(audit): stateless hash chain, waitable forwarder, profile actor, observability
Address PR #555 review:
- chain: derive prev_hash from file tail under cross-process flock, drop the
  global .chain sidecar so per-day files stay independently verifiable and
  concurrent dws processes cannot fork the chain
- forward: track async forwards with WaitGroup and add bounded Close(ctx) so
  in-flight deliveries are not dropped on process exit
- actor: resolve Actor from the active runtime profile (profile-keyed cache)
- observability: BuildSink returns init errors; write/forward failures reported
  to file log and to stderr when DWS_AUDIT_DEBUG is set
- cli: reject `audit tail --lines` < 1; check CSV writer/flush errors
- wire CloseAuditSink into PersistentPostRunE
- add regression tests for cross-date/cross-process chain, forwarder
  wait/timeout, init-failure, tail validation, CSV export
2026-07-14 11:56:09 +08:00
修雨 32c1d772de fix(root): expose audit command in help and reserve it from plugins 2026-07-14 10:30:05 +08:00
修雨 39b3003e2f Merge branch 'main' into feat/audit-log-v2 2026-07-14 10:21:15 +08:00
玉澜 6d97bf7204 Merge upstream/main into feat/schema-on-main
Complete the registry-first Schema delivery invariants, bind the event command surface, and preserve the event subprocess contract from main.
2026-07-14 01:25:05 +08:00
玉澜 f6c2ce655d refactor(schema): unify registry-first delivery 2026-07-13 19:49:40 +08:00
Dennis fb4cf70c93 Merge remote-tracking branch 'origin/main' into feature/shortcut 2026-07-13 16:05:41 +08:00
玉澜 561525a18b feat(schema): generate stable agent command catalog 2026-07-11 10:28:10 +08:00
aa6abc5ed6 feat(event): add personal event subscriptions (#589)
* dws event

* fix consume fail

* test: add stream ticket injection probe

* feat: add portal ticket stream mode

* user event

* fix: allow portal ticket normal without app secret

* event

* user event

* eventType filter

* refactor(event): 优化IPC端点路径处理和改进相关测试

- 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑
- 新增event包实现Unix socket路径长度限制及长路径fallback机制
- 添加endpoint_test.go覆盖路径短长及唯一性的单元测试
- 修改busctl模块使用统一的IPC端点获取方法,避免重复实现
- transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误
- 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度
- 去除多个文件中无用的runtime导入,简化代码依赖

* opt

* event skill

* default value

* install script event

* fix: remove subscribe id event fanout filter

* fix(personal): 修正指定发送人消息描述错误

* more im event

* filter subId

* fix: align personal event schema with stream payload

* fix: avoid duplicate app helper name

* feat: simplify personal event schemas

* feat: simplify event schema output

* docs: refine dingtalk event skill references

* feat: align personal event consume flags

* fix event stop and status visibility

* hide app event public entrypoints

* hide incomplete personal sender event

* remove external event reference comments

* chore(event): prepare official release

* fix(event): harden personal stream lifecycle

---------

Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
2026-07-10 17:54:43 +08:00
DennisandClaude Opus 4.8 b9f2733821 feat(app): assemble and wire shortcut commands into the CLI
builtin blank-imports every service + smart package so their registrations run,
exposes Commands(), and provides the zero-side-effect coverage suite
(TestAllShortcutsAssemble / TestAllToolLiteralsAreReal / TestNoDuplicateCommands
/ TestAllHaveIntent). legacy loads user YAML shortcuts then merges built-in
shortcut leaves into the helper command tree; root wraps the tool caller with the
usage recorder and registers dws shortcut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 10:04:10 +08:00
修雨 5ac5fcbf16 Merge remote-tracking branch 'origin/main' into feat/audit-log-v2
# Conflicts:
#	internal/helpers/devapp_connect.go
2026-07-08 13:59:54 +08:00
修雨 a32d7985e6 refactor: switch to static endpoint delivery runtime 2026-07-07 19:07:03 +08:00
修雨 4a717bd92f feat(audit): implement user operation audit log
- Add internal/audit package: Event struct, FileSink, date rotation, L1 hash chain, HTTP forwarding, 3-tier redaction
- Integrate with runner: emit audit event in executeInvocation defer
- Add dws audit tail/export/verify command group
- Register DWS_AUDIT* env vars in configmeta, enabled by default
2026-07-06 11:55:40 +08:00
e32fa1535c feat(auth): support multi-profile login (#500)
* feat(auth): support multi-profile login

* fix(auth): complete multi-org profile acceptance

* feat(auth): 完成多组织 profile 验收

* docs(auth): 补充多组织 Ralph 验收材料

* feat(auth): 支持 auth switch TUI 切换 profile

* feat(auth): logout 默认清理所有组织

* feat(auth): login 默认新增组织授权

* feat(profile): 使用 profile switch 切换组织

* docs(ralph): 更新 profile switch 验收材料

* fix(profile): 展示全部可切换组织

* feat(profile): support multi-org switch tui

* chore(install): add branch source installer

* fix(profile): keep global profile out of tool params

* feat(profile): support csv multi-profile runtime

* ci: add multi-profile e2e workflow

* ci: run multi-profile e2e on all branches

* docs: document multi-profile e2e ci gate

* docs: remove multi-profile test cases from pr

* ci: harden multi-profile e2e gates

* fix(auth): serialize profiles.json RMW and harden multi-profile persistence

Wrap all profiles.json read-modify-write paths (profile switch/use/remove,
status marking, token save, logout) in the existing dual-layer lock via a new
withProfilesLock helper. Split each writer into a public (locking) entry point
plus a lock-free *Locked variant so the non-reentrant lock is never re-acquired;
the refresh path (oauth_helpers) and the load-path legacy migration now call the
lock-free saver to avoid self-deadlock.

Also: write profiles.json and the token marker via per-write random temp names
(uuid) to stop concurrent writers from corrupting a fixed .tmp file; quarantine
an unparseable profiles.json and rebuild an empty config so the CLI can
self-heal instead of locking out auth reset/logout; make DeleteAllTokenData
proceed even if profiles.json cannot be read; and stop SyncLegacyTokenMirror
from deleting the legacy mirror on a transient keychain read error.

* fix(auth): do not fall back to a different org's legacy token slot

When no explicit --profile is given, LoadTokenDataForProfile resolves the
current/primary profile and reads its per-corp keychain slot. If that slot
read failed, the code silently fell through to the legacy single token slot,
which after any drift between the legacy mirror and the current profile could
belong to a different organization. The command would then run as the wrong
org with no indication to the user.

Reproduction (conceptual):
  - profiles.json currentProfile = corpA
  - corpA's keychain slot is unreadable, legacy single slot still holds corpB
  - any read command (no --profile) silently used corpB's token

Fix: when a profile is resolved but its slot read fails and no --profile was
given, only fall back to the legacy single slot when its CorpID matches the
resolved profile (same org); otherwise return the original error instead of
acting as a different organization. The no-profile legacy path (pre-migration
installs with no resolved profile) is unchanged.

Tests:
  - Covered by the existing internal/auth suite under go test -race; the
    same-org fallback preserves the legacy-mirror case while the cross-org
    case now surfaces the read error.

* feat(skill): document multi-org profile usage and always ship dws-shared

The skills had no guidance on the multi-profile capability, so an agent would
treat the CLI as single-org: when a lookup missed in the current org it would
give up or ask the user instead of searching other logged-in orgs. The multi
skill set also referenced a `dws-shared` prerequisite that was never actually
installed, and the only multi-org hints lived inline in three product skills.

This adds, in source only:
- A "multi-org / profile" section in the mono SKILL.md (concept, commands,
  cross-org rule, aggregation, safety guardrails) plus a decision-tree entry,
  trigger conditions, and a corrected logout danger-table row (logout removes
  all orgs by default; removing the primary silently re-elects a new primary,
  confirm before removing the primary).
- A standalone skills/multi/dingtalk-profile skill mirroring the same content.
- A new skills/multi/dws-shared skill that carries auth, global flags and the
  multi-org rule, so every product skill's PREREQUISITE resolves and all
  read/search skills inherit the cross-org behavior without per-skill edits.
- Cross-org fallback notes on dingtalk-aisearch / chat / contact.

To guarantee the prerequisite actually ships, multi-mode install now force-
includes dws-shared even when --skill / --exclude narrows the set (no-op when
the source has no dws-shared, preserving older layouts).

Tests:
  - internal/app: TestP1SharedAlwaysIncludedWithSkillFilter installs with
    `-s aitable` and asserts dws-shared still lands in the destination;
    TestP1SharedNoopWhenAbsent guards the older-layout no-op.
  - go test -race ./internal/auth/... ./internal/app/... passes.

---------

Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
Co-authored-by: qinze <audanye@gmail.com>
2026-06-29 18:27:25 +08:00
修雨 49637d982e feat(compat): hide phantom override commands from --help (tool-existence guard)
Override leaves whose backing MCP tool isn't actually deployed rendered in
`dws <svc> --help` but failed at invocation with "tool not found" (43 phantom
commands across 391 overrides; attendance declared 38, only 4 deployed).

BuildDynamicCommands now takes an existingTools oracle (CLI slug -> live tool
set from the tools/ cache). A leaf whose tool is missing from its resolved
server's set is marked Hidden; groups left childless collapse. Safety rails:
acts only when a server's tool set is KNOWN and non-empty (cold cache / overlay
/ plugin paths pass nil and no-op, never blanking the tree); serverOverride
leaves resolve against the target server; pipeline leaves are never hidden.

Adds scripts/dev/check-phantom-overrides.py as a publish-time gate, and
phantom_guard_test.go covering hide / cold-cache-keep / serverOverride / pipeline
/ empty-group-collapse.
2026-06-29 10:53:03 +08:00
修雨 4c5f1faeb1 feat: align open CLI with dws-wukong via cedar discovery version code (#509)
Switches the discovery version code bamboo -> cedar and aligns the open edition CLI with dws-wukong across communication (calendar book/acl/attendee, minutes tag, mail folder/template/contact, chat file upload, todo add-attachment, attendance transforms) and structured-office (aitable advperm/view/section/workflow/record, sheet/drive/wiki/doc) domains. Includes output-envelope parity, parse_bool/attendance_class_check_time transforms, --calendar-id support, CHANGELOG 1.0.43 and README command-index refresh. cedar config validated on pre and prod endpoints.
2026-06-26 21:49:38 +08:00
修雨 9545c1dde5 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	.github/badges/coverage.svg
2026-06-25 14:12:58 +08:00
shangguanxuan.sgx 760c40dd71 Merge upstream main 2026-06-25 11:10:25 +08:00
修雨 f522a9c2c2 Merge upstream/main into feat/dws-devapp
Sync with DingTalk-Real-AI/main (1.0.35–1.0.40):
- 1.0.35–1.0.39: chat @-mentions, sheet export, doc strip unsafe chars,
  PAT batch grants, devdoc RAG, agent_code detection, AI-tag opt-in
- 1.0.40 (#486): China mirror via Gitee + npmmirror
- (#488) embed skills/ into binary
- (#464) strip duplicate H1 on JSONML doc-create

# Conflicts:
#	.github/workflows/release.yml
#	CHANGELOG.md
#	README.md
#	README_zh.md
#	docs/command-index.md
#	internal/app/runner.go
#	internal/app/runner_test.go
#	internal/auth/channel.go
#	internal/helpers/devdoc.go
#	internal/helpers/devdoc_test.go
#	internal/pat/chmod.go
#	internal/pat/chmod_test.go
#	internal/pat/pat.go
#	internal/transport/client.go
#	scripts/install-skills.sh
#	scripts/install.ps1
#	scripts/install.sh
#	skills/mono/references/products/devdoc.md
#	skills/mono/references/products/simple.md
#	skills/multi/dingtalk-devdoc/references/devdoc.md
#	skills/multi/dingtalk-skill/references/skill.md
#	test/cli_compat/devdoc_test.go
#	test/cli_compat/helpers_test.go
#	test/skill_tests.md
#	test/skill_tests_results.md
#	test/unit/pat_host_owned_signal_test.go
2026-06-24 20:15:53 +08:00
shangguanxuan.sgx 6067906c55 fix(devdoc): fallback empty rag article search 2026-06-18 13:08:34 +08:00
qinzeandClaude Opus 4.8 70d66daa5f feat(dev): schema 覆盖 dev doc + dingtalk-dev skill 精简
- dws schema 支持 dev.doc.search:fetcher 按 source 取(op-app 钉死 pre/
  devdoc 走运行时解析),给 doc search 叶子加 mcp-tool/mcp-source 注解
- dev doc search flag 对齐 MCP 参数名(--keyword/--size 为主,旧名隐藏兼容)
- dingtalk-dev SKILL.md:顶部加 MUST DO(每次先 --help/schema 查),核心规则
  瘦身、去重、去加粗/箭头符号
- references 精简:删参数表/flag 列表(schema/--help 已实时提供),只留状态机/
  覆盖语义/链路/gotcha;event 补"订阅前需先建联"业务规则;新增 recipes.md
  端到端链路;典型任务收成指针

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:56:11 +08:00
qinzeandClaude Opus 4.8 6244222adf feat(dev): schema 实时拉取 MCP + flag 全面对齐 MCP 参数名
- 新增 dws schema dev.* 实时从 op-app tools/list 拉取参数(gws 扁平格式),
  description/type/required/default 全部 verbatim 来自服务端,零本地合成
- flag 全部改为 kebab(MCP 参数名),去掉别名,schema 展示与 CLI flag 完全一致,
  修掉 schema 显示假参数名的问题
- CLI 同步新 MCP 契约(工具名复数化、参数名 rename)
- dingtalk-dev skill 文档同步新 flag 名

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:43:19 +08:00
shangguanxuan.sgx 11dfd4ccf2 feat: add dws recommended batch auth flow 2026-06-16 14:09:15 +08:00
修雨 60ac0b409d fix(cli): guard canonical mcp tree against poisoned-cache flag collisions (#454)
* fix(cli): guard canonical mcp tree against poisoned-cache flag collisions

The canonical 'dws mcp' tree is built from cached catalog data before the
legacy command build and before Cobra dispatches anything, so a pflag
panic there (a tool schema property named after the reserved --params
flag, as cached during the 1.0.32 incident) aborted every invocation --
including 'dws cache refresh' and 'dws upgrade' -- and sat outside all
three poisoned-cache guards (#447/#449/#452).

Two layers, mirroring the existing guards:
- applyFlagSpecs now skips reserved (--json/--params), duplicate and
  alias-colliding flag names and sanitizes shorthands instead of letting
  pflag panic; the skipped property stays reachable through the reserved
  JSON payload flags (same degradation semantics as #449).
- newMCPCommand wraps the build in the #452 recover -> quarantine ->
  retry-once -> degrade-to-stub sequence, so even an unforeseen panic
  class no longer locks the CLI out.

* docs: amend 1.0.36 changelog for the re-cut with the canonical tree guard (#454)
2026-06-11 09:16:36 +08:00
修雨 3282957958 Revert "fix(cli): JSON errors to stdout; no interactive confirm on piped stdin (#413)" (#415)
This reverts commit f826375556.
2026-06-08 17:53:16 +08:00
修雨 ffefc53861 fix(cli): JSON errors to stdout; no interactive confirm on piped stdin (#413)
- 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.
2026-06-08 17:53:16 +08:00
修雨 5fbf12fe50 Revert "fix(cli): JSON errors to stdout; no interactive confirm on piped stdin (#413)" (#415)
This reverts commit f826375556.
2026-06-05 12:10:17 +08:00
修雨 f826375556 fix(cli): JSON errors to stdout; no interactive confirm on piped stdin (#413)
- 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.
2026-06-05 11:22:22 +08:00
539d10f80f feat: 合并 pre-mcp-discovery 分支能力到 main(服务发现保持生产) (#391)
将 test/pre-mcp-discovery 分支上多位同学的能力改动整体合入 main,
服务发现端点保持生产 https://mcp.dingtalk.com,未携带分支上的预发改动。

主要内容:
- 修雨域(chat / report / todo / contact / mail)对齐 wukong 基线 (#355)
- DWS 内容生产能力增强 (#387)、文档导出与 OA 对齐 (#362 #388 #390)
- envelope 注册 cli.Aliases 为 cobra aliases、命令结构与 JSON 解析优化
- skills 多包同步 wukong 对齐内容(attendance/calendar/minutes/oa/sheet 等)
- product/oa 进一步对齐 wukong

排除项:未携带 566b4e1(test: point service discovery to pre-mcp.dingtalk.com);
以下 4 个常量保持生产:skill_command.go / auth/endpoints.go / cli/loader.go / market/registry.go。

Co-authored-by: john <32427341+wqyenjoy@users.noreply.github.com>
Co-authored-by: wxianfeng <wang.fl1429@gmail.com>
2026-06-02 21:04:12 +08:00
修雨 4ebc8d0d38 fix(plugin): silence stdio discovery handshake failures (Warn → Debug) (#380)
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.
2026-06-01 17:11:52 +08:00
Ari 45cb237f74 fix(drive): unblock OSS upload + improve AI-agent discoverability (#347)
* 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 会先在新版本上线.
2026-05-24 15:55:08 +08:00
6707e56f9c feat(cli): schema-aware sticky flag splitting and structured unknown-flag recovery (#272)
* 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>
2026-05-13 10:56:02 +08:00
修雨 f78cc5c846 docs(auth): correct login help to reflect actual default + SSH guidance (#226) (#238)
* 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
2026-05-09 09:25:57 +08:00
fantiu 4c44a2b632 Merge branch 'main' into feat-rawapi 2026-04-27 20:03:40 +08:00
xianfeng wangandgithub-actions[bot] acc5dc8a3c feat(plugins): register stdio plugin commands from CLI overlay before discovery (#179)
* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* plugin in help

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-27 16:20:35 +08:00
fantiu 46192d68ba feat(api):client logic fixed. 2026-04-24 17:04:09 +08:00
fantiu eeb1782f7a feat(api): add raw DingTalk OpenAPI command with dual-version token support 2026-04-24 16:40:13 +08:00
xianfeng wang bbca507792 feat(discovery): MCP discovery service abstraction with positional params, examples, defaults, and schema v3 extensions (#156)
* discovery

* discovery

* wk discovery

* wk discovery

* wk discovery

* wk discovery

* wk discovery

* wk discovery

* discovery example and default

* position param

* Fix test

* dev auto login

* fix test

* fix conflict
2026-04-24 14:08:17 +08:00
修雨 4f915e4e2c refactor(plugin): drop managed/ compat layer left by PR #126 (#135)
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.
2026-04-22 14:56:11 +08:00
ybcstudyand猷诺 a5bede3a19 fix: -f json模式下错误JSON从stdout改为输出到stderr (#133)
修复背景:
CI测试脚本通过检查stderr中是否包含错误关键词来判断命令是否失败。
但-f json模式下,printExecutionError将错误JSON输出到stdout,
导致stderr为空,测试断言失败。

改动:
- root.go:150: apperrors.PrintJSON(stdout, err) → apperrors.PrintJSON(stderr, err)
- root_execute_test.go: 3处断言方向同步调整(stdout↔stderr)

影响范围:
仅影响-f json模式下的错误输出流向,正常输出不受影响。
符合Unix惯例:错误信息输出到stderr,正常结果输出到stdout。

Co-authored-by: 猷诺 <bicheng.ybc@alibaba-inc.com>
2026-04-20 19:38:39 +08:00
shangguanxuan633-labandshangguanxuan.sgx 725577103d feat: PAT scope error visualization and auto-retry with authorization polling (#113)
* 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>
2026-04-19 19:12:34 +08:00
修雨 f762117d4e Merge pull request #126 from DingTalk-Real-AI/refactor/remove-managed-plugin-privilege
refactor(plugin): 移除 default/managed plugin 特权机制
2026-04-19 14:56:59 +08:00
修雨 59e51c348a perf(plugin): share cache.Store across discovery + relax cold timeouts
Address reviewer feedback on #125:

- Hoist a single *cache.Store above the discovery fan-out so all HTTP and
  stdio goroutines share one instance rather than each spawning its own in
  registerHTTPServer/registerStdioServer. Atomic tmp+rename per cache key
  keeps concurrent writers on distinct keys collision-free; global runtime
  registries (AppendDynamicServer, RegisterStdioClient) already guard
  themselves with a mutex. Added a comment pointing at those invariants.

- Relax cold-path timeouts to survive healthy cross-region endpoints and
  Python/Node interpreter warm-up: HTTP 500ms → 1s (plain), 700ms → 1.5s
  (auth), stdio 1s → 2s. A new DWS_PLUGIN_COLD_TIMEOUT env var overrides
  all three with a single duration, registered in configmeta so it shows
  up in `dws config --help`.

- Add internal/app/plugin_discovery_concurrency_test.go covering the
  thread-safety claims end-to-end: 16 parallel SaveTools with distinct
  keys, 32 parallel AppendDynamicServer, 32 parallel RegisterStdioClient,
  plus TestResolvePluginColdTimeouts exercising defaults, a valid
  override, an unparseable value, and a non-positive value. All pass with
  go test -race.

Verified: go build ./... clean; targeted -race tests PASS in 1.5s;
./internal/{app,cli,cache,plugin}/... all green.
2026-04-19 14:41:40 +08:00
修雨 33ae780103 refactor(plugin): remove default/managed plugin privileged mechanism
Drop the hardcoded default-managed-plugin bootstrap that auto-fetched
DingTalk-Real-AI/* plugins on CLI startup. All third-party plugins are
now installed equally via `dws plugin install` — no privileged workspace.

Motivation (issue #124): first-run startup printed
  WARN failed to fetch remote info for default plugin
       DingTalk-Real-AI/conference: invalid character '<' looking for
       beginning of value
because the GitHub Pages registry returned HTML for the missing plugin.
Beyond the error, the whole "built-in plugin" concept contradicts the
CLI's lightweight design: plugin installation planning belongs to
agent-authored skills, not to the CLI binary.

Changes:
- Delete internal/plugin/updater.go (EnsureManaged, CheckAndUpdate,
  checkRemoteVersion, downloadAndInstall, promptUpdate, zip extraction
  — the entire managed-plugin update pipeline).
- Delete internal/plugin/updater_test.go.
- Remove DefaultManagedPlugins, OfficialPluginWorkspace, and
  PluginUpdateCheckInterval from pkg/config/constants.go.
- Remove the EnsureManaged/CheckAndUpdate bootstrap block from
  internal/app/root.go; only legacy LoadManaged() kept for back-compat
  so plugins already installed under ~/.dws/plugins/managed/ still load.
- internal/plugin/loader.go: InstallFromGit always installs under
  PluginUserDir; RemovePlugin allows removal of legacy managed-dir
  plugins instead of refusing.
- internal/app/plugin_cmd.go: drop `--type managed` flag and related
  gating from `plugin install`; disable/remove commands no longer
  differentiate managed vs user.
- Update plugin_test.go accordingly (replace TestRemoveManagedPluginBlocked
  with TestRemoveLegacyManagedPlugin, drop TestPromptUpdate).

Backward compatibility: `PluginManagedDir = "managed"` constant retained
so plugins already on disk from older CLI versions still load and are
removable. No migration required.

Verification:
- go build ./cmd                 → success
- go test ./internal/plugin/...  → PASS
- go test ./internal/app/...     → PASS (436s, matches main baseline)
- Smoke: `/tmp/dws-new plugin list` no longer prints the "Pulling
  built-in plugin" or "failed to fetch remote info" WARN.
- Pre-existing failures on main (unrelated): test/cli_compat,
  test/integration/extensions (requires auth login),
  test/scripts (requires DWS_PACKAGE_VERSION or git tag),
  test/unit TestOpenSourceTreeOmitsEmbeddedHostMarkers.

Closes #124
2026-04-19 11:18:56 +08:00
修雨 daf56514f7 perf(plugin): parallelize all plugin discovery and tighten cold timeouts
Follow-up to cache-first registration (c95ec04). The cold-cache wall
clock is now bounded by the slowest individual plugin, not the sum:

- Fan out HTTP and stdio discovery together in goroutines rather than
  running the stdio loop serially after HTTP.
- HTTP cold budget: 4s → 700ms (auth) / 500ms (plain). Honest dial
  timeouts on unreachable endpoints fail fast; healthy third-party
  endpoints respond well under the window. The outcome is still saved
  as a negative cache, so a miss this run costs the next run ~0ms.
- Stdio cold budget: 4s → 1s. A local subprocess handshake is milliseconds.

With three user plugins (one pointing at TEST-NET-1, permanently
unreachable), cold `dws --help` drops from ~3.3s to ~0.8s and warm stays
at ~80ms. The savings compound linearly with plugin count.
2026-04-18 14:22:53 +08:00
修雨 8bcbceb971 perf(plugin): serve plugin MCP tool list from disk cache on startup
Follow-up to the 4s startup cap (df01f36). Plugin command registration
now reads the tools snapshot directly from the cache store on the hot
path, and only falls through to an Initialize+ListTools RPC when no
snapshot exists.

- registerHTTPServer / registerStdioServer try cache.LoadTools first;
  on hit they build Cobra commands synchronously with zero network I/O.
- Cold cache falls back to the existing synchronous discovery (already
  bounded at 2-4s) and persists the outcome — including empty tool
  lists — as a negative cache so the next invocation is fast regardless
  of endpoint health.
- Cache entries are namespaced under "plugin:<name>:<server>" so they
  are distinct from Market cache entries in `dws cache status`, and
  refresh on-demand via `dws cache clean` / `dws cache refresh`; the
  existing 7d ToolsTTL otherwise expires entries naturally.

Warm-cache `dws --help` with three user plugins (one unreachable) now
returns in ~80ms versus 3.7s with synchronous discovery, a >40x win
when endpoints are offline.
2026-04-18 00:47:20 +08:00
修雨 df01f36442 fix(transport): cap plugin MCP startup at ~4s when endpoints are unreachable
Issue #119: an unreachable third-party MCP plugin (e.g. blocked/firewalled
endpoint) blocks `dws --help` for ~10s on every CLI invocation, because
plugin discovery happens eagerly during command-tree construction.

Root cause was a stack of timeouts that multiplied under transport failure:

* `transport.Client.Initialize` loops three supported protocol versions on
  every error — including dial timeouts and HTTP 5xx — even though those
  failure modes are protocol-version-independent. Three loops × the dial
  budget = 3× the worst-case startup cost.
* Default `DialContext.Timeout` was 10s, so a single dial against an
  unroutable address (e.g. TEST-NET-1) burned the full plugin context.
* `registerHTTPServer` granted plugins with `AuthHeaders` a 10s outer
  context (intended for slow third-party services), and `registerStdioServer`
  granted every stdio plugin 10s. Either single misbehaving plugin therefore
  stalled the entire CLI.
* Registry-side `defaultDiscoveryTimeout` (10s) and `perServerDiscoveryTimeout`
  (5s) had the same shape on the LoadCatalog path.

Changes:

* `transport.Client.Initialize`: short-circuit when the underlying
  `*CallError.Stage` is anything other than `CallStageJSONRPC`. Protocol
  version negotiation is the only justification for retrying with another
  version; transport/HTTP failures fail identically and should surrender.
* `transport.defaultTransport`: `DialContext.Timeout` 10s → 3s.
* `app.registerHTTPServer`: AuthHeaders timeout 10s → 4s.
* `app.registerStdioServer`: outer ctx timeout 10s → 4s.
* `cli.defaultDiscoveryTimeout`: 10s → 4s.
* `discovery.Service`: rename `perServerDiscoveryTimeout` 5s → 2s
  (`defaultPerServerDiscoveryTimeout`); add `Service.PerServerTimeout`
  override field for tests/callers needing a tighter or looser bound.

Tests:

* `TestInitializeShortCircuitsOnHTTPError` — asserts only ONE protocol
  version is attempted on HTTP 5xx.
* `TestInitializeShortCircuitsOnDialFailure` — asserts Initialize returns
  in <2s on a refused-connection address.
* Existing `TestInitializeNegotiatesProtocolVersion` continues to pass —
  JSON-RPC-stage errors still trigger version fallback.

End-to-end measurement with a blackhole plugin
(endpoint=192.0.2.1, AuthHeaders set):

  | Variant       | median `dws --help` |
  | ------------- | ------------------- |
  | main          | 10.2s               |
  | PR #121 alone | 9.1s                |
  | this branch   | 3.72s (-63%)        |

The `loader.go` and `discovery/service.go` timeout reductions overlap with
PR #121 by @utafrali; see PR description for attribution.

Refs: #119
Supersedes: #121
2026-04-17 17:45:35 +08:00
扶疏 94ad422a9f style: fix code formatting 2026-04-17 12:19:27 +08:00