Before, /new and /clear were identical: both just dropped the local
conversationId->sessionId mapping. The agent-side session was never
disposed (opencode sessions leaked), and the two commands had no real
difference beyond their ack text.
Align them to each channel's real capability:
- /new (resetSession): drop the local mapping only; the old agent session
is left intact and stays resumable where the agent supports it.
- /clear (new sessionClearer): actively dispose the current session via the
agent's real delete primitive. opencode implements it with
DELETE /session/:id (idempotent on 404). Channels whose agent exposes no
delete in the mode DWS drives it (Codex app-server, Qoder stream, Claude
exec) fall back to resetSession, so /clear behaves like /new there.
Tests pin both directions: /clear issues exactly one DELETE and drops the
mapping; /new (resetSession) issues zero DELETEs so the old session stays
resumable. Doc updated to describe the per-channel behavior.
go build ./... ok; go vet ok; go test ./internal/helpers ok.
The fork's dev branch had the pre-release URL (pre-mcp.dingtalk.com)
hardcoded as the default MCP base, which would leak prepub config into
any release build. Revert to the upstream production default.
🤖 Generated with [Qoder][https://qoder.com]
Rewrite mirror-to-gitee.yml to push main + tags to the Gitee mirror using
HTTPS + GITEE_TOKEN (reusing the existing secret) instead of hub-mirror-action
+ an SSH key. Gated on GITEE_TOKEN; needs GITEE_USER + GITEE_REPO secrets.
Keeps Gitee code/raw in sync automatically on every push to main + tags, so
no more manual git push after each release.
Probe GitHub Releases on startup; if it is unreachable (typical in mainland
China), automatically switch to the Gitee mirror — so a plain
`curl … | sh` (or .ps1 / install-skills.sh) works everywhere with no
DWS_GITEE_REPO env var. Explicit DWS_GITEE_REPO still wins; DWS_NO_FALLBACK=1
disables the probe; local source-checkout installs skip it.
The shared opencode http.Client had a hardcoded 30s Timeout used for every
request including POST /session/{id}/message. Long agent turns (e.g. a web
research report) take longer than 30s, so the client aborted mid-flight with
"context deadline exceeded (Client.Timeout exceeded while awaiting headers)",
even though the per-turn budget (DWS_AGENT_TIMEOUT_MS, default 300s) was far
larger. Retrying hit the same wall.
Drop the client-level deadline (rely on the per-request ctx that doJSON already
wires via http.NewRequestWithContext) and bound only the /global/health probe
with a short 10s timeout so startup detection stays snappy.
Tests: pin Timeout==0 on the default client, and assert a slow reply succeeds
within the turn budget but is cut when the turn ctx is shorter than the reply.
PR #448 stripped a leading H1 matching --name on the markdown path of
`dws doc create`, but the JSONML path (--content-format jsonml) was never
covered. Rich documents — tables, callouts, styled blocks — go through
create_document + update_document(jsonml=...), which writes the body
verbatim, so a leading h1 whose text equals the document name renders the
title twice (the "two headings" effect the doc platform shows because it
already renders --name as the page title).
Add stripLeadingDuplicateTitleJSONML: parse the marshaled JSONML body,
skip an optional ["root", {}, ...] wrapper, and drop the first node when
it is an h1 whose concatenated leaf text equals --name (trimmed,
case-insensitive). Any parse failure or non-match leaves the body
untouched, so a valid write is never blocked. A stderr note mirrors the
markdown path so agents learn the convention.
- CHANGELOG: 1.0.40 (China mirror via Gitee + npmmirror, #486)
- mirror-to-gitee.yml: gate on GITEE_PRIVATE_KEY so it skips cleanly when the
SSH key is unset (code mirror handled by Gitee-side pull-mirror) instead of
failing the run.
go:embed the skills/ tree (mono + multi) into the binary and make
`dws skill setup` install from the embedded bundle by default. Upgrading
the binary now refreshes the installed skill, instead of silently reusing
a stale copy probed from the current working directory. An explicit
--source / DWS_SKILL_SOURCE still overrides for development.
Replants the change from #441 onto main: #441 was stacked on the
abandoned feat/chat-bot-provisioning-openclaw base (PRs #407/#397 closed),
which is why its CI was red. The embedded-bundle test asserted a
connect.md doc that only existed on that dead base; retargeted it to a
reference doc that exists on main.
Add scripts/release/sync-to-oss.sh and wire it into release.yml. After
the GitHub release, artifacts (binaries, checksums, dws-skills.zip) plus
the install scripts are pushed to an OSS bucket in the
<base>/<version>/<file> layout the install.sh DWS_RELEASE_BASE switch
expects, with a latest.txt pointer.
Gated on OSS_* repo secrets: skips cleanly when unset, so forks without
the secrets keep releasing to GitHub only.
When `dev connect` runs with no credentials (no --robot-client-id/secret, no
--unified-app-id) in a real terminal, it now guides the user instead of failing:
- Ask "new app or existing app?".
- Existing: read a unified-app-id (reuses the credentials-get path) or an
explicit clientId/clientSecret pair.
- New: read the app name, robot display name and description, submit the async
robot-create task, then poll for the result to obtain credentials. This has a
real side effect (it provisions a real robot app).
connect_onboarding.go keeps the flow channel-agnostic and io-injected so the
whole decision tree is unit-tested with a mock runner and scripted stdin (no
network, no real provisioning). Non-interactive invocations — scripts, daemons,
pipes, and --dry-run — are unchanged: they still require an explicit credential
flag, so nothing that worked before starts prompting.
Tests cover: existing/unified, existing/raw-creds, new-app submit+poll (asserts
the submit_robot_create_task / query_robot_create_result calls and params),
invalid choice, and a missing required field (no side-effecting call).
Note: the real provisioning path shares the same unverified credential field
names as devAppFetchCredentials (clientId/appKey, clientSecret/appSecret — see
its TODO(verify)); the live create→credentials flow is to be verified on the
pre-prod gateway with the -dws-devapp prerelease before relying on it.
Parameterize the hardcoded GitHub Releases domain in install.sh,
install.ps1 and install-skills.sh so domestic users can point asset
downloads (binary tarball, checksums, skills zip) and latest-version
resolution at a China-accessible mirror (e.g. OSS+CDN / dingtalk domain).
Defaults stay on GitHub — fully backward compatible.
opencode persists its own sessions but, unlike the Claude family, will not let
the caller mint the session id up front: `opencode run` creates a session on the
first turn and only reports its id in the `--format json` event stream, then
continues it with `--session <id>`. So the connector now CAPTURES the id from
the output (like codex's thread id) instead of minting a UUID:
- connect_opencode.go: a dedicated opencodeForwarder that runs
`opencode run --pure --format json`, accumulates reply text from `text`
events, captures the top-level sessionID, and on the first turn persists the
convID->sessionID map to <config>/connect/<clientId>/opencode-sessions.json
(best-effort, restored on startup). The next message — and a connector
restart — replays it with `--session <id>`. Implements streamingForwarder and
sessionResetter, so /new and /clear work and streaming cards stream.
- forwarderForChannel routes the opencode channel to it.
- connectAgentOptionsPayload reports opencode memory as
"per-conversation-captured"; --agent-memory help now lists codex/opencode too.
Verified end to end against the real opencode CLI: turn 1 captures the session
id, turn 2 with --session recalls a fact from turn 1. Unit tests cover capture +
resume (stub bin), store persistence across restart, and reset.
gemini also exposes --session-id/--resume but could not be verified end to end
here (no GEMINI_API_KEY) and its --resume id semantics are unconfirmed, so it is
left for a follow-up rather than wired blind. qoder's qodercli still has only
--resume (no addressable id) and cannot do per-conversation memory.
Two connector improvements for the linked-bot chat experience, modelled on
codex / OpenClaw connect:
1. Built-in slash commands. A user can type /new (aliases /start, /reset) to
open a fresh session or /clear to wipe the current one, instead of the
message being forwarded to the agent. The command set is fixed (never
dynamically extended) so behaviour stays predictable. Matching requires the
whole message to be the bare command, so a normal question that merely starts
with a slash is forwarded untouched. Costs no agent turn / tokens.
- connect_command.go: channel-agnostic parser + action table (+ tests).
- sessionResetter optional interface; execForwarder (Claude family) and the
codex app-server forwarder both implement it. The main loop intercepts a
command right after the owner-decision interceptor and acks via webhook.
2. Persist codex per-conversation context. codexThreadSessions previously kept
the convID->threadID map in memory only, so a codex connector restart lost
every conversation's context — unlike the Claude-family convSessions store
which already persisted. The thread identity is now the authoritative,
mutex-guarded map persisted to <config>/connect/<clientId>/codex-threads.json
(best-effort, atomic write), restored on startup. An empty clientId keeps it
in memory only, preserving the original behaviour. This also removes a data
race on threadID by moving it off the per-conversation state into the
sessions store.
去掉"以开放平台文档为准/dev doc search",改成"可订阅事件码通过 event list 查询
(返回 events[] 列出 eventCode/eventName/subscribed)"——event list 本就列出所有可订阅事件,更准。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unauthenticated curl is limited to 60 req/h, and once exhausted the API
returns 403. The previous `curl -fsSL ... | grep tag_name` swallowed the
403 silently, leaving DEVAPP_VERSION empty and surfacing a misleading
"No release found" error even though the fork has plenty of releases.
- Prefer `gh api` when available (authenticated, 5 000 req/h).
- Fall back to curl while capturing the HTTP status; on 403/429 fail
with an actionable message instead of pretending no release exists.
The opencode channel forwarded to `opencode run` without isolating the
operator's interactive environment. claudecode already neutralises via
`--setting-sources project --strict-mcp-config`; opencode had no equivalent.
When the operator runs a plugin suite that swaps the global default agent at
runtime (e.g. OhMyOpenCode's rotating Greek-codename agents), headless
`opencode run` crashes resolving a default agent that no longer exists
("default agent ... not found"), and the crash trace is sent back to DingTalk
as the bot reply. Verified by reproducing in a clean /tmp dir: plain
`opencode run` crashes, `opencode run --pure` replies normally.
Add --pure to the opencode argvTail so the bot answers in a clean,
plugin-free environment, consistent with the claudecode channel.
Without a dev entry in the product table and intent tree, agents reading the
mono skill routed 'create robot' / 'connect' requests to dws chat (because
'机器人' appeared in the chat product description), then landed on
dws chat bot search — wrong path.
Changes:
- skills/mono/SKILL.md: add dev row to product table; annotate chat row to
scope it to IM-only; add dev intent-tree rule covering 创建机器人/建联/
接入 agent/opencode/…; prepend dev-vs-chat disambiguation rule
- skills/mono/references/products/dev.md: new file — full command reference
for dws dev app (lifecycle/robot build-号/credentials/permission/event/
version) and dws dev connect (建联 flags, channel list, dry-run cli check,
codex gotcha), sourced from dingtalk-dev multi-skill references
写死 DingTalk-Real-AI 导致 fork CI 想往官方仓库发 release 被 403 拒绝。
改用 CI 自带的 GITHUB_REPOSITORY_OWNER, fork CI 发到 fork、官方 CI 发到官方。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>