Commit Graph
212 Commits
Author SHA1 Message Date
修雨 df1e33442b fix(skill): embed skill sources in the binary and cover qoder dirs in upgrade refresh
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.
2026-06-12 17:57:48 +08:00
修雨 a7ca1e1a5b fix(connect): pass user-level Claude settings env through to the claudecode channel
The claudecode channel spawns claude with --setting-sources project to
keep the bot persona neutral, but that also drops the user-level
settings.json env block where third-party provider tools (cc-switch
etc.) store ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN. claude then
fell back to the official login and replied "Not logged in - Please
run /login" for anyone on a relayed model.

Re-expose that env block as process environment for the spawned CLI
(both one-shot and streaming paths) via the channel envFn hook.
Variables already exported by the operator are not overridden, and
user hooks/plugins still stay out of replies.

Fixes PeterGuy326#10.
2026-06-12 16:14:50 +08:00
修雨 211a06c05a fix(devapp): resolve op-app MCP endpoint from gateway base URL
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.
2026-06-12 14:54:05 +08:00
修雨 1c90edb92a fix(connect): drain buffered app-server frames before honoring read EOF
The codex app-server read loop reports EOF before closing the message
channel, so when the process exits right after its final frame, both
channels are ready and select picks one at random — runTurn could drop
an already-received turn/completed and fail with "stream ended before
turn/completed: EOF" (the CI flake on this PR and #8). Drain buffered
frames first; reproduced at ~7%/300 runs before, 0/300 after.
2026-06-12 09:42:51 +08:00
修雨 36b89a04b1 Merge remote-tracking branch 'wxianfeng/feat/dws-devapp' into HEAD
# Conflicts:
#	internal/helpers/connect_stream.go
2026-06-12 09:18:10 +08:00
quanming ae5ef70bb8 fix: harden codex app-server client concurrency 2026-06-11 19:27:50 +08:00
修雨 c151756168 feat(connect): Q&A-bot hardening for the stream bridge
Five operational gaps that bite when a connector-backed bot answers
questions in real groups:

- Single-instance lock: one connector per robot per machine (pid file,
  stale-lock takeover). Duplicate Stream connections on one clientId get
  messages load-balanced between them, so the bot answers intermittently
  (verified live).
- Access policy: --allowed-users / --allowed-groups allowlists plus a
  per-sender rate limit (--user-rate-limit, default 20/min). Every
  message is an LLM call; without this anyone who can reach the bot can
  spend on its behalf.
- Per-conversation FIFO queue: same-chat messages run in arrival order
  (follow-ups need the previous turn's --resume session; parallel CLIs
  racing one session corrupt it); different chats stay parallel.
- Picture messages: resolve the callback downloadCode via robot
  messageFiles/download to a local temp file and hand the path to the
  agent CLI — error screenshots are the top Q&A inbound and were
  previously dropped silently. The claudecode persona now permits
  reading attached files.
- --knowledge-dir: lightweight local retrieval (markdown chunking,
  latin-token + CJK-bigram scoring) prepends top-k matching chunks to
  each question while the agent keeps running from the clean scratch
  dir, so replies stay inside DingTalk's AI-assistant response window.

All knobs have env mirrors (DWS_ALLOWED_USERS / DWS_ALLOWED_GROUPS /
DWS_USER_RATE_LIMIT / DWS_KNOWLEDGE_DIR) for service-style runs.

go build ./... clean; go test ./internal/helpers -race passes with new
tests for lock/gate/queue/knowledge/media. test/cli_compat, test/scripts
and test/unit fail identically on the unmodified base (pre-existing
fixture issues).
2026-06-11 12:18:25 +08:00
quanming 3dcb40c634 feat: add codex app-server robot bridge 2026-06-11 11:10:25 +08:00
修雨 4b8acc8e73 docs(connect): fix stale CardTemplate comment; document AI-assistant reply window in hermes channel guidance
- ConnectOptions.CardTemplate said an empty value falls back to the public
  openclaw template, but the code (newAICardClient/hasTemplate) treats empty
  as cards-off, and the explicit "public" alias exists precisely for that
  opt-in. Align the comment with the actual contract.
- hermes official-channel steps now warn about the trap verified live: a
  devapp robot renders replies through the AI-assistant response window, so
  a slow agent's plain-text reply gets the Done reaction (HTTP 200) but never
  shows up, leaving the loading card stuck. Point users to enabling AI cards
  on the hermes side.
2026-06-10 23:54:15 +08:00
修雨 f013536aac Merge upstream feat/dws-devapp: adopt upstream's hardcoded-endpoint priority
Conflict resolutions:
- internal/app/direct_runtime.go: take upstream (532fcb4) — the maintainer
  stabilized the endpoint tests by documenting the hardcoded devapp endpoint
  as authoritative (tests renamed *DoesNotOverrideHardcoded), superseding our
  demote-to-fallback fix for the same failing tests.
- skills/mono devapp.md: union — keep our reply-card/card-template/CLI
  preflight bullets and upstream's Codex DWS_AGENT_CMD guidance.
2026-06-10 21:51:25 +08:00
修雨 f9abc79ecc feat(devapp): cards opt-in via template; default replies are plain text + reactions
Mirror hermes' semantics: without a configured card template, replies go as
plain text/markdown with the Thinking/Done reaction chips (which need no
template and always render); cards activate only when --card-template /
DWS_CARD_TEMPLATE is set ('public' opts into the openclaw shared template
explicitly). This removes the silent-failure trap where every card API call
succeeds but the client cannot render a template it is not authorized for —
the previous default left users staring at 内容加载失败 instead of falling
back to text.
2026-06-10 21:43:42 +08:00
修雨 99893e473a feat(devapp): CLI dependency preflight in robot connect dry-run
The channel CLIs (claude/codebuddy/qodercli/...) may be missing on the
user's machine. Runtime already preflights at connect (auto-installing
package-manager CLIs, erroring with a hint otherwise), but agents had no
machine-readable way to check BEFORE starting the connector. dry-run now
reports a cli block ({required, installed, path, autoInstall, installHint})
per channel — desktop-app channels and openclaw/hermes report their
onboarding hint. Skill docs gain the agent preflight workflow: check
dry-run first; npm channels may auto-install; desktop-app channels must
have the app installed by the user before connecting.
2026-06-10 21:32:25 +08:00
修雨 51b3316187 feat(devapp): configurable AI-card template via --card-template / DWS_CARD_TEMPLATE
Card templates are app-scoped — the hermes docs prescribe grabbing the
template ID from YOUR app's AI Card settings in the developer console, and
our live A/B confirmed another app's template renders 内容加载失败. Expose
the template as a flag (default: the openclaw public template, best-effort)
so operators can register an app-owned template for reliable, branded
rendering. Skill docs updated with the developer-console path.
2026-06-10 21:18:45 +08:00
InternZzp 532fcb4874 fix: stabilize direct runtime endpoint tests 2026-06-10 21:07:03 +08:00
修雨 28d556c5e9 fix(devapp): demote the built-in devapp endpoint to a fallback; gofmt
The hardcoded devapp endpoint was checked BEFORE dynamic registration and
edition static/supplement lookups, short-circuiting every configurable
source — the TestDirectRuntimeEndpoint_Devapp*WithoutRegistry tests encode
the intended priority (built-in = last resort so operators can repoint the
product without a rebuild) and have been failing on the branch. Move the
built-in check after Priority 4; env override > dynamic > PAT > edition >
built-in. Also gofmt two files the Lint job flagged.
2026-06-10 21:02:22 +08:00
修雨 c106de8361 feat(devapp): AI-card replies with thinking/done states and live streaming for robot connect
Bring the official-channel reply experience (hermes/openclaw) to every
stream-bridge channel:

- "🤔Thinking" reaction chip on the user's message while the agent runs,
  swapped to "🥳Done" when the reply lands — the hermes text-emotion contract
  (POST /v1.0/robot/emotion/reply|recall, emotionId 2659900).
- AI-card reply using the openclaw connector's public template + payload
  contract (msgContent + flowStatus state machine). Card templates are
  app-scoped: hermes' own template renders "内容加载失败" for any other app —
  confirmed by live A/B on the same robot.
- Live streaming into the card where the channel CLI supports it (verified
  per CLI): claude/codebuddy stream-json text deltas (thinking deltas
  filtered), qodercli stream-json per-turn snapshots; codex/gemini/opencode
  stay one-shot. Frames are full-content and throttled at 800ms per card.
- --reply-card flag (default on, env DWS_REPLY_CARD=0) with full fallback
  chain: any card failure → plain text/markdown webhook reply; stuck cards
  are best-effort marked failed.
- Deliver responses are checked for business-level failures inside HTTP 200
  ({"result":[{"success":false,...}]} observed live), and every card API
  response body is logged for operability.
- workbuddy persona now forbids tool use: headless codebuddy otherwise
  stalls on permission gates trying to write memory files.
- Skill docs: reply-card flag, version-publish approver-selection workflow
  (check-approval → present approver list to the user → publish --approver).

Known issue under investigation: after a batch of invalid-template test
cards, the reporting device renders all subsequent cards as "内容加载失败"
even though every API call succeeds and identical payloads rendered earlier
the same day — suspected client-side card/template cache poisoning; pending
verification on a second client.
2026-06-10 20:46:38 +08:00
修雨 1b3319603d fix(devapp): surface Stream SDK connection lifecycle on stderr
The SDK's default logger is a doNothingLogger, so the connector printed
nothing on connect/reconnect/read errors — a dead connection was
indistinguishable from a healthy idle one. Wire an ILogger that writes
Info/Warning/Error to stderr (debug frames stay silent). Verified live:
'[stream] connect success, sessionId=...' now appears at startup.
2026-06-10 12:08:15 +08:00
修雨 995d490a76 feat(devapp): give robot connect session memory, model and workdir tuning
Users reported the linked bot feels dumb. Root cause: the forwarder was
hard-pinned to a stateless one-shot haiku run from an empty directory. Expose
the trade-offs as command-set flags instead of hardcoding them:

- --agent-memory (default on): per-conversation session resume. First message
  of a DingTalk conversation mints a UUID and passes `--session-id`,
  follow-ups pass `--resume`, so multi-turn context survives. Only on CLIs
  with addressable sessions (claudecode/codebuddy/workbuddy — verified against
  their --help); qodercli has --resume but no --session-id, so the qoder
  family stays stateless. Broken sessions self-heal: on forward error the
  conversation mapping is dropped and the next message starts fresh.
- --agent-model / DWS_AGENT_MODEL: override the channel CLI's model via each
  spec's model flag (replaces claudecode's built-in haiku pin in place).
- --agent-workdir / DWS_AGENT_WORKDIR: run the agent from a knowledge
  directory for context, instead of the clean temp dir (which stays the
  default to keep cold-start inside DingTalk's reply window).

A DWS_AGENT_CMD override disables flag splicing entirely — that argv is
user-owned and we cannot know which flags it accepts. Dry-run now previews
the effective agent tuning (model/workdir/memory) per channel.
2026-06-10 11:24:54 +08:00
修雨 5a401ddc1b fix(skill): detect ~/.qoder and ~/.qoderwork in skill setup, add qoderwork install path
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.
2026-06-10 10:29:27 +08:00
修雨 f5bdb7118a feat(devapp): add robot connect to fold bot linking into the devapp command set
Bring the channel-aware Stream linking ("建联") from PR #407 into the devapp
command tree as `dws devapp robot connect`, so the full lifecycle
(create app -> create robot -> link to a local agent) lives under one
`dws devapp` domain instead of a separate `dws connect` command.

- Reuse, not duplicate: provisioning ("建号") stays on devapp's existing
  `robot create/submit/result`; this commit ports only the linking half.
- connect_stream.go: the Go-native in-process Stream forwarder + channel
  agent registry, ported verbatim from #407 (package helpers, self-contained).
- devapp_connect.go: trimmed channel routing (resolveConnectChannel /
  launchConnector / buildConnectPlan / ...) plus the new subcommand. The
  standalone `connect` cobra command, `connect start` and `connect bot create`
  are intentionally NOT brought over — the capability is consolidated under
  `dws devapp robot`.
- Credentials: `--robot-client-id` / `--robot-client-secret` for an existing
  robot, or `--unified-app-id` to auto-fetch via devapp's credentials get.
  The flags are named `robot-client-*` (not `client-id`/`client-secret`) to
  avoid shadowing the global OAuth client-override persistent flags.
- Adds the dingtalk-stream-sdk-go v0.9.1 dependency.
- Skill docs (devapp.md, robot.md mono+multi) document `robot connect`.

Known follow-up (marked in code + docs): the get_open_dev_app_credentials
response field names used by `--unified-app-id` auto-fetch are not yet
confirmed against the real gateway; the path degrades safely (empty -> fall
back to explicit flags) and must be verified against pre-prod before relying
on it.
2026-06-10 10:06:31 +08:00
wxianfeng de4a292174 devapp robot version 2026-06-09 15:58:01 +08:00
InternZzp ac825ad293 fix(devapp): use placeholders in security examples 2026-06-09 14:17:00 +08:00
玉澜andClaude Opus 4.6 13d77ee7b7 feat: add credentials get command and hardcode devapp endpoint
- Add `dws devapp credentials get` helper command calling
  `get_open_dev_app_credentials` MCP tool
- Hardcode devapp endpoint to pre-mcp-gw.dingtalk.com/server/op-app
  in direct_runtime.go (same pattern as robotCreate in PR 407)
- Update docs/skills from "Pending" to "Implemented" for credentials
- Remove obsolete showSecret/confirmSecret references
- Expand lifecycle tests to table-driven covering delete/inactive/active

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-09 13:40:33 +08:00
InternZzp 694288cfbc feat(devapp): guard member and security writes 2026-06-09 10:41:19 +08:00
玉澜 9589c23796 feat: complete devapp command routing 2026-06-08 19:07:30 +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
xuanandshangguanxuan.sgx 252c61aff6 fix(pat): pass agent code to chmod batch tools (#414)
Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
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
InternZzp 8cbc56edd2 feat: add devapp helper commands 2026-06-08 17:24:59 +08:00
xuanandshangguanxuan.sgx cb95207d5a fix(pat): keep auth URLs readable in JSON output (#401)
* fix(pat): keep auth URLs readable in JSON output

* fix(pat): narrow URL escaping fix to PAT JSON

---------

Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
2026-06-04 15:57:06 +08:00
修雨 6e3f3cbd24 chore: take aiapp product offline
Remove the entire code surface of the aiapp (AI application: create/query/modify) product:
- Delete internal/helpers/aiapp.go and its test (the init() static command registration)
- Drop aiapp from generator coverage targets and knownRegistryProducts
- Remove aiapp from the operations-analyst persona services in personas.yaml
- Delete skills (mono references/products/aiapp.md + dingtalk-aiapp multi skill + script)
- Update README/README_zh product tables and counts (19->18 products, 20->19 multi skills) and run_skill_tests mapping
- Add a Removed entry to CHANGELOG

The service-discovery side (envelope/discovery.pre.json, Portal-synced and gitignored) removes the aiapp server block separately.
2026-06-03 19:41:05 +08:00
audanye-sudo ce5e919c52 fix(market): version-code discovery path as /cli/discovery/apis/bamboo
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.
2026-06-03 19:41:04 +08:00
修雨 c75ed45c70 fix(keychain): add StorageDir for Windows to unblock v1.0.33 release (#394)
* docs(chat): 资源下载分流改为按 host 判定,补 media 直链可裸 curl(整合 #376)

main 现状的「资源链接形态分流」段补三类增量,supersede #376:
- 判定依据从「链接形态」改为「链接 host,不由扩展名」
- media(down.dingtalk.com/media)点明为公开 CDN,download-media 命令之外裸 curl 也可直下
- 补 .unknown 用 file 判真实类型、图片 AI 描述说明
- 保留 download-media 官方命令路径,删去 #376 误判「无 mediaId 下载命令」一句

* docs(chat): 修正 media 下载分流——host 非唯一、首选 download-media、签名链勿裸 curl

修复 PR 原版对 media 链接的过头断言(评审发现):
- 点明图片 mediaId 链接 host 不唯一:down.dingtalk.com/media(公开)或 *.trans.dingtalk.com 签名+过期链
- 下载主路改为首选 download-media(取最新 URL,不受过期影响);裸 curl 仅限公开直链
- 删去「无需鉴权 / curl 均 HTTP 200」的普适断言(签名链过期后 curl 403)
- 保留 .unknown 用 file 判类型、AI 图片描述、钉盘/alidocs 分流

* fix(keychain): add StorageDir for Windows to unblock cross-platform release

portable_store.go (#357) references keychain.StorageDir on all platforms,
but only keychain_darwin.go / keychain_linux.go defined it — the Windows
backend (DPAPI + registry) lacked StorageDir, so GoReleaser's
windows/amd64 + windows/arm64 cross-compile failed with
'undefined: keychain.StorageDir' and broke the v1.0.33 release.

Add StorageDir to keychain_windows.go: DWS_KEYCHAIN_DIR override →
%LocalAppData%\<service> default, mirroring the linux/darwin logic.
Verified: go build passes for windows/darwin/linux x amd64/arm64.
2026-06-02 23:37:06 +08:00
johnand玉澜 8c4bd71964 fix(doc): align export progress output (#392)
Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
2026-06-02 22:26:03 +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
56a5edecef feat(pat): support batch chmod authorization flows (#389)
* feat(pat): support batch chmod flows

* feat: use server default agentCode for chmod

* feat: surface tool auth metadata

* feat: infer product grant auth metadata

* chore: update coverage badge [skip ci]

* feat(pat): summarize chmod output by default

* chore: update coverage badge [skip ci]

* fix(pat): preserve batch session metadata

* fix(pat): align dry-run session and schema docs

* chore(config): use public example MCP override URL

* fix(pat): align chmod session env handling

---------

Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-02 20:54:55 +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
4e58e45d30 feat(auth): add export/import for portable sandbox credentials (#357)
* feat(auth): add export/import for portable sandbox credentials

Enable migrating encrypted keychain and config between Linux sandboxes so refresh tokens survive beyond the 2-hour access token window.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auth): harden portable export/import per PR review

按 PR 审查意见加固可迁移认证包流程:macOS 默认 Keychain
场景拒绝导出;导入需 --force 覆盖已有登录态;解析 manifest
并在 OS 不匹配时告警;export 参数校验前置;flag 描述中文化;
移除已弃用的 tar.TypeRegA;导出文件后提示删除敏感包。

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auth): require auth-token.enc before portable export

修复 staticcheck S1030(测试改用 exported.Bytes())。
导出前校验 auth-token.enc 存在,避免无登录态时生成
仅含 dek 的无效 bundle;CLI 提示先运行 dws auth login。

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: 洛丘 <wangketing.wkt@antgroup.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 09:19:11 +08:00
修雨 5ce7cb61b0 feat(skill): dws skill setup with mono/multi modes (#332)
新增 `dws skill setup` 交互式安装命令,支持单产品(mono)和多产品(multi,标记为试验版)两种 skill 布局,并与开源 dws v1.0.30 命令对齐。

主要变更:
- 骨架:skills/ 拆为 skills/mono/ 和 skills/multi/(29 个产品 skill),git rename 保留历史
- 命令:新增 `skill setup` cobra 子命令,支持 --mode、--target、--source、--yes,huh 交互
  - multi 模式支持 -s/--skill、-x/--exclude 按产品选择
  - 覆盖全部 14 个 agent home 目录(与 install.sh 一致)
- 发布:install.sh / install.ps1 / post-goreleaser.sh 改读 skills/mono;dws-skills.zip 同时包含 mono 和 multi 内容;~/.dws 本地缓存
- 内容对齐:删除 8 个仅内部使用的产品 skill、dingtalk-a2a;修正 mono/multi 共 15+ 处命令引用以匹配 OSS dws v1.0.30;chat skill 从 dws-wukong 回移更新
- 漂移清理:物理删除 skill 文档中 OSS 未暴露的命令,true-fail 命令转为 caveat;--help diff 100% 覆盖审计
- 测试:新增 Layer A 静态命令调度验证 + Layer B 端到端 mock 调度验证
- 同步 main:drive 9 个命令同步到 mono/multi skill
2026-05-26 09:17:37 +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
修雨 bd711108f9 fix(upgrade): ad-hoc sign darwin binaries to fix amfid SIGKILL on Apple Silicon (#339)
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.
2026-05-21 22:45:22 +08:00
修雨andClaude Opus 4.7 c7d8ddf98d feat: align drive with wukong — helper (upload) + skill docs (doc/sheet dingpan URL) (#335)
* feat(helpers): drive upload 三步合成胶水(list-spaces / delete 由 envelope 接管)

业务背景:开源版 dws drive 仅有来自服务发现 envelope 的 6 个工具,悟空版多
3 个能力 — list-spaces / upload / delete。这 3 个里只有 upload 是真正的客户端
胶水(PUT 文件二进制到 OSS 的中间 HTTP 步),其它两个完全可以通过 envelope
toolOverrides 表达,无需 Go 代码。

按 envelope 优先原则拆分:

1. envelope/pre-discovery.pre.json (单独提供 / 不在本 commit)
   - drive.toolOverrides.list_spaces  →  cliName: list-spaces
   - drive.toolOverrides.delete_document → cliName: delete, serverOverride: doc,
                                             isSensitive: true
2. internal/helpers/drive.go (本 commit)
   - 仅注册 upload 一个 leaf
   - 原因:envelope.PipelineStep 只支持 type:"call"/"download",没有
     type:"upload"。客户端流式 PUT 本地文件到 OSS 签名 URL(含 per-URL
     headers)当前 envelope schema 表达不了,故保留为 helper。

internal/helpers/drive.go (新增 ~290 行)

  - upload — 三步合成:
    1) drive get_upload_info → resourceUrl + uploadId + headers
    2) HTTP PUT 文件二进制 → OSS(沿用 aitable upload-file 模式,
       10min timeout,跟随 per-URL headers)
    3) drive commit_upload → 提交入库
    --dry-run 输出三步 invocation 预览不实际请求。
    parseDriveUploadInfo 兼容 content/result 包裹层与 resourceUrls 数组
    及 flat resourceUrl/uploadUrl fallback,与悟空侧 parseDriveUploadInfo
    等价。
    validateDriveParentID 拒纯数字 dentryId(防混淆 chat 链路 dentryId 与
    drive 链路 dentryUuid)。

注入路径:helpers.RegisterPublic → pickCommands.MergeHardcodedLeaves(dynamic,
helper)(dynamic 赢冲突、helper 填空白)。本 helper root.Use="drive" 不覆盖
dynamic 的 6 个 leaf,仅追加 upload。

dry-run 实测:

  $ dws drive upload --file /etc/hosts --dry-run --format json
  → step_1_get_upload_info + step_2_http_put_oss + step_3_commit_upload 三步预览

测试结果:
  ok  internal/helpers (含全部既有 TestSendByBot* / TestAtomicWrite* /
                       TestValidate* 等 23 个测试)
  ok  test/integration/extensions
  test/cli_compat pre-existing failures 与本改动无关(在干净 origin/main HEAD
  同样 fail,fixture 缺失等)。

未来工作 (follow-up):
  - 等 envelope 文件 commit 到主线后,list-spaces / delete 自动从 envelope
    生成 cobra command,无需此 helper 操心
  - 如果 envelope schema 加入 type:"upload" pipeline 步类型,本 helper 整个
    可以删除,由 envelope pipeline 表达完整三步

关联:
  - 悟空源头实现:dws-wukong/wukong/products/drive.go
    (runDriveUpload / parseDriveUploadInfo / httpPutFile + delete 路由)
  - 配套 skill 文档 PR:#333 (doc 端钉盘 URL 识别)
  - envelope 改动: drive.toolOverrides 增加 list_spaces + delete_document,
    见对应的 envelope PR / commit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skill): doc --node 接受钉盘 document/edit|preview?dentryKey URL + 新增 url-patterns 分流

业务背景:hho 业务侧用户在悟空版上已用钉盘 URL(alidocs.dingtalk.com/document/edit?dentryKey=...)
直接喂给 Agent,但开源版 skill 文档未覆盖这种格式,Agent 不知道整段 URL 应原样传给 --node,
经常误把 dentryKey 当成裸 nodeId 调用导致失败。

本 PR 把悟空 dws-wukong PR #82157526 的 skill 文档增量移植到开源主线,保持 doc/sheet
两端入口的 URL 识别策略对齐悟空:

skills/SKILL.md
- 核心流程新增 Step 0「URL 预检」:含 alidocs URL 必须先读 url-patterns.md 分流,再选择产品
- 详细参考目录加入 url-patterns.md 引用

skills/references/products/doc.md
- dws doc info/read 的 Example 各 +2 行钉盘 URL 示例
- 「URL 识别与 DOC_ID 提取」支持的 URL 格式表新增 document/edit|preview?dentryKey={key} 一行
- 提取规则拆成 3 条,禁止 Agent 自行提取 dentryKey 当裸 nodeId
- 「nodeId 双格式说明」升级为「nodeId 多格式说明」,给出 4 种 --node 输入等价示例

skills/references/url-patterns.md (新建)
- 沉淀 alidocs URL 5 类分流决策:/i/p/ 短链 / /i/nodes/ 节点 / /spreadsheetv2/ 直链 /
  /document/edit|preview?dentryKey 链接 / 其他
- 含 i/nodes/ 类型探测流程(doc info 探 contentType/extension/nodeType 后再选产品)
- 含分享短链 read_url 兜底处理 + 动态渲染失败时的标准答复

skill 命名形态兼容:
- 当前 main 上 skills/ 是扁平结构,改动直接落在 skills/* 路径
- 单点(mono)/ 多点(multi)拆分形态在 feature/skill-setup 分支,待该分支合主线时
  按相同语义同步到 skills/mono/* 和 skills/multi/dingtalk-{doc,sheet}/* 两套

不涉及 Go 代码变更,纯 skill 文档;钉盘 URL 解析逻辑由 MCP 服务端承担。

关联悟空 PR:dws-wukong commit 565c63f8 / 712da968(#82157526)
对应 wukong skill 包:dingtalk-workspace/{SKILL.md,references/products/doc.md,references/url-patterns.md}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:38:00 +08:00
修雨 6be124777f feat: align CLI to wukong IM envelope + schema pipeline / transform / market enhancements (#317)
把 test/pre-mcp-discovery 上累积的稳定改动整理成一个 commit 合入 main,剔除
test-only 的预发环境硬编码与 merge/revert 噪音。

- internal/helpers: 重写 chat.go 对齐 wukong IM envelope,拆分共享的 chat
  命令,删减历史测试桩
- internal/compat: 新增 Pipeline tool override + executor、CLIAliases
  override、json_parse_strict 与 file_read transform;envelope leaf cmd
  Args 从 NoArgs 放宽到 ArbitraryArgs
- internal/cli: stdin 处理增强,覆盖更多 --content / --content-file 路径
- internal/app: direct_runtime / runner 适配嫁接硬编码 helper 到动态命令树
- internal/market: registry 强化 + 单元测试

最终 diff: 12 个文件,+369 / -387。
2026-05-19 15:25:39 +08:00
c6edc84e40 fix(auth): partition app.json filename by edition to isolate credentials (#300)
* 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>
2026-05-17 17:43:20 +08:00
修雨 f497047fff fix(compat): relax envelope leaf cmd Args from NoArgs to ArbitraryArgs (#306)
NewDirectCommand was hard-coding cobra.NoArgs for envelope-generated
leaf commands that have no positional bindings (totalMax == 0). This
is stricter than cobra's own default — legacyArgs (args.go:30-32)
returns nil for any command without subcommands.

The strict behavior surfaced as "unknown command \"<word>\" for
\"dws aisearch person\"" whenever an AI agent passed trailing
positional words after a leaf, e.g.

  dws aisearch person search --keyword "张"
  dws aisearch person user search --keyword "张"

Switching to cobra.ArbitraryArgs restores cobra's natural leaf
behavior: trailing positional args are silently ignored. Existing
positional-binding paths (MinimumNArgs / RangeArgs / MaximumNArgs)
are unchanged.

Verified against dws-wukong/auto-test/cli_to_mcp/testcases on
aiapp / live / aisearch: 50/50 pass (was 48/50; the 2 remaining
failures were the F-class extra-positional-args tolerance cases
this commit fixes).
2026-05-17 17:23:17 +08:00
FuShu-Yang 1c200d883f fix(app): prevent command field from overwriting existing endpoint (#297)
* 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
2026-05-15 11:29:07 +08:00
修雨 ed4673e7d2 fix(chat): require --title for group messages, completing #250's symmetric fix (#294)
`send_message_as_user`'s schema also marks `title` as required, but
`buildChatMessageSendInvocation` only pre-validated it for direct
messages. Sending `dws chat message send --group <cid> --text "1"`
without `--title` therefore reached the API and returned the same
misleading `发群服务窗会话消息失败` business error that #250 fixed for
direct messages, just on the other branch.

Pre-validation now covers both branches: group sends without title
return `--title is required for group messages (--group)`, direct sends
keep the existing `--title is required for direct messages (...)`
message. `Long` help, the `--title` flag description, the first
`Example` line, and `skills/references/products/chat.md` (including the
deeper "drive → chat" workflow example) are realigned to
"群聊与单聊都必填". `internal/helpers/chat_test.go` gains a
`group-without-title` case, and the existing `group` / `positional-text`
success cases are updated to pass `--title`. No API request shape change.
2026-05-14 21:13:28 +08:00
FuShu-Yang 649801e479 fix(transport): suppress noisy WARN on stdio client shutdown (#285)
* fix(transport): suppress noisy WARN on stdio client shutdown

When Stop() kills the subprocess, cmd.Wait() always returns a non-zero
exit code which is expected behavior. Previously this propagated as an
error, causing "failed to stop stdio client ... exit status 1" warnings
on every normal CLI exit for stdio-based plugins.

Now Stop() returns nil when the process was explicitly killed, keeping
the error path only for cases where the process exits on its own with
a non-zero code (e.g. stdin close without kill).

🤖 Generated with [Qoder][https://qoder.com]

* fix(transport): address review feedback — simplify Stop(), fix test assertion

- Remove redundant `killed` flag; inline Kill+Wait+return nil
- Update integration test to positively assert Stop() returns nil after kill

🤖 Generated with [Qoder][https://qoder.com]
2026-05-14 14:45:23 +08:00
修雨 49c5bea4f3 feat(schema): file_read transform + CLIFlagOverride.MapsTo for --content/--content-file (#277 #278 #282 #288) (#291)
* feat(transform): add file_read transform for UTF-8 file / stdin content (#277)

Introduces a new ApplyTransform case "file_read":
- Input: a string flag value treated as a UTF-8 file path
- Special case: the path "-" reads from stdin
- Output: the file contents as a string
- Errors surface as validation errors (exit 2) — non-UTF-8, missing
  file, empty path, and non-string input all reject cleanly

This is a foundational primitive intended to compose with envelope
schema features so a CLI flag like --content-file can carry a path
that ultimately feeds a string-typed MCP parameter with the file's
contents.

## Scope note — MapsTo intentionally NOT included

The original proposal in #277 paired this transform with a new
CLIFlagOverride.MapsTo field that retargets a flag's value to a
different MCP parameter (e.g. --content / --content-file both feed
the upstream `markdown` param). Pre-production end-to-end validation
(see #282) showed that the MCP registry server-side schema does not
currently recognise `mapsTo` and strips it on serialisation. Every
other new envelope field (hidden / required / transform /
mutuallyExclusive / requireOneOf) survives — only mapsTo is dropped.

Shipping MapsTo without server-side support would land dead client
code. The MapsTo field + sourceFlag transform guard were therefore
removed from this PR; they will return in a follow-up PR once #282
(server-side schema acknowledgement of mapsTo) is resolved. The
file_read transform stays here because it composes with multiple
mechanisms beyond MapsTo and is independently testable.

## Tests

Six new cases in internal/compat/transform_test.go cover the
contract: literal file, stdin via "-", missing file, non-UTF-8
input, empty path, non-string input. All pass under both `go test`
and `go test -coverprofile`.

Refs #277, blocked-by #282

* feat(schema): add CLIFlagOverride.MapsTo for sibling-flag routing (#277 #282)

Adds the `MapsTo` field on `CLIFlagOverride` and wires the dispatch loop
in `compat.buildOverrideBindings` so a flag's final value (post-transform
or literal) is routed into a different MCP parameter slot than its own
property name. This lets two sibling CLI flags feed a single upstream
parameter — the canonical case being `--content` (literal) + `--content-file`
(transform: file_read) both mapping to `markdown`.

Tool-level `CLIToolOverride.MutuallyExclusive` (cobra MarkFlagsMutuallyExclusive)
is the right partner for guarding "set one, not both" at parse time; no
new exclusion machinery is added.

Closes the client-side gap previously misattributed to a server-side
mapsTo strip in #282. Once a doc envelope with mapsTo lands in pre-prod,
end-to-end `--content-file` becomes shippable, finishing #277 Step 1b.

Test coverage (internal/compat/dynamic_commands_test.go):
  - MapsTo without transform: literal --content → params[markdown]
  - MapsTo with file_read transform: --content-file path → params[markdown]
  - Sibling flags both mapsTo same target, only one set: clean routing
  - Sibling flags both set: rejected by tool-level MutuallyExclusive (regression)

Backward-compat: empty MapsTo preserves existing params[propertyName] write
semantics for every existing envelope.
2026-05-14 14:44:00 +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