Commit Graph
48 Commits
Author SHA1 Message Date
wxianfeng 4f4ea43549 fix: reconcile primary migration with current main #85349380 2026-08-18 19:25:17 +08:00
wxianfeng 7fbe7593c8 feat: migrate chat primary parameters to #85349380 2026-08-18 19:25:17 +08:00
修雨 9f0966c05a test: stabilize cross-platform coverage CI 2026-07-15 17:07:51 +08:00
修雨 d8ffea02ab test: close remaining coverage gaps 2026-07-15 15:19:25 +08:00
修雨 fafb6f47b9 test: reach complete unit coverage 2026-07-15 12:08:39 +08:00
修雨 8633246eff test: expand unit coverage 2026-07-15 11:41:30 +08:00
修雨 047ac54afe fix(connect): forward complex message payloads (#612)
Remove content-shape and message-type attachment filtering, recover forwarded unknown attachments, and preserve original media across all agent backends.
2026-07-14 22:31:21 +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
SCzheng 43798de088 fix(connect): preserve rich text image attachments (#606) 2026-07-13 22:13:56 +08:00
修雨 390b6115bf fix(connect): harden daemon restart lifecycle (#599) 2026-07-12 23:11:08 +08:00
修雨 5e9a920b76 fix(connect): prevent agent mid-turn blocking
fix(connect): prevent agent mid-turn blocking

Co-authored-by: 修雨 <47820304+PeterGuy326@users.noreply.github.com>
2026-07-09 21:28:35 +08:00
修雨 a32d7985e6 refactor: switch to static endpoint delivery runtime 2026-07-07 19:07:03 +08:00
修雨 b63e1b4469 release: cut 1.0.47 — connector supervision + bot-to-bot @-mention (#548) 2026-07-06 00:46:47 +08:00
修雨 fbfcd69c2b feat(connect): make /new vs /clear use each channel's real session op
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.
2026-06-25 10:43:21 +08:00
quanming c993086d9d feat(connect): keep qoder sessions on stream-json 2026-06-24 20:36:26 +08:00
Aemeathandquanming f218a05ead [codex] use opencode server sessions for OpenCode connect
Co-authored-by: quanming <quanming@code.alibaba-inc.com>
2026-06-24 17:18:58 +08:00
quanming fc5e4d0d8d fix(connect): align codex and qoder sessions 2026-06-24 15:09:50 +08:00
修雨 b096fa06db feat(connect): give opencode per-conversation memory via captured sessions
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.
2026-06-24 10:57:25 +08:00
修雨 107f3eaf6e feat(connect): add /new /clear slash commands and persist codex session context
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.
2026-06-24 10:30:04 +08:00
修雨 c16c4aa59a fix(connect): run opencode with --pure to neutralise operator plugins
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.
2026-06-23 15:07:19 +08:00
修雨 5ed69744cc Merge wxianfeng/feat/dws-devapp into release/dws-devapp
解决 devapp→dev 重构与 connect/数字分身工作的合并冲突,方向:全面对齐
wxianfeng 的 dev 命名空间,保留我方两处实质内容(动态端点 + connect 功能)。

冲突解决要点:
- release.yml:取 wxianfeng 更干净的 npm 守卫(repository_owner + 注释);
  保留我新增的「-dws-devapp 翻成非 prerelease+latest」步骤
- devapp.go / devapp_test.go / 路由文档:取 wxianfeng(新 MCP 工具名
  *_dev_app_*/extension、新增 event 工具、删 robot create、config 合 upsert)
- 端点:保留我方动态 devappMCPEndpoint()(网关 base 推导、生产默认),
  否则会把「建号对所有人打预发」的 bug 倒灌回来;同步修 helper_tool_fetcher.go
  与 direct_runtime_test.go 改用该函数 + wxianfeng 工具名 list_dev_app
- devapp_connect.go:connect 命令对齐 dev connect 命名,保留我方 --agent-cmd
  自研渠道示例(#37),建号引用修正为真实存在的 robot submit
- skills:跟随 wxianfeng 删除旧 dingtalk-devapp(已被重写的 dingtalk-dev 覆盖)
- robot-quickstart.md:命令全改 dws dev(建号两步 submit+result、dev connect),
  下载源改 wxianfeng,去掉写死版本号用 releases/latest

验证:go build ./... 通过;go vet 干净;internal/app、internal/helpers 单测全过
2026-06-23 10:21:29 +08:00
qinzeandClaude Opus 4.8 51db546686 Merge origin/feat/dws-devapp into feat/dws-dev
合并远端团队的 MCP 对齐与 CR 修复,与本地 dev 命令重构 + schema 实时拉取 + flag 对齐 MCP 参数名集成。

冲突解决要点:
- 工具名常量:两边已收敛到同一组 op-app 真实工具名,取 dev 子树(ours)标识
- 保留 schema 能力:32 个 annotateDevAppTool 注解 + flag=kebab(MCP参数名)不变
- 吸收远端 CR 修复:normalizeDevAppServiceResult 解包 ServiceResult、
  remove_dev_app_permissions 结果扁平化、生命周期工具状态标记
- 旧 dingtalk-devapp 技能目录保持删除(已重命名为 dingtalk-dev)
- devdoc article search 统一 --page-size/pageSize,--size 留兼容别名

构建/vet/internal 测试全绿(28 包,0 失败)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:01:50 +08:00
qinzeandClaude Opus 4.8 63b6112fa2 test(dev): dev 命令集测试 + 建联/端点路由适配
helpers 单测、connect/card/stream 测试,app runner 与 direct_runtime devapp
端点路由。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:09:18 +08:00
修雨 b4c9db8807 feat(connect): custom channel for self-built/unsupported AI (issue #37)
Add a generic 'custom' channel plus an --agent-cmd flag so 'dws devapp
robot connect' can forward to ANY headless AI CLI (question appended as
the trailing arg, stdout used as the reply). Onboards tools that aren't
built-in (e.g. LobsterAI) or self-built agents with no code changes:
--agent-cmd forces channel=custom unless --channel is set explicitly,
and auto-detection falls back to custom when DWS_AGENT_CMD is present.

Also (issue #39): print a one-time hint at connect start when neither a
work dir nor a knowledge source is set, pointing at --agent-workdir /
--knowledge-* / --agent-model so the bot can match terminal answer
quality. Quickstart gains matching FAQ entries plus a note that step 3
(robot connect) produces no approval ticket (issue #19).
2026-06-16 15:04:04 +08:00
修雨 ee2fa735b2 feat(connect): confirm_policy — manual / auto / remember for others' requests
RoleConfig.confirm_policy was parsed but never acted on. Wire the three
strategies for how OTHERS' action requests are confirmed (the owner's own
requests always auto-run regardless):

- manual (default): ask the owner every time
- auto: run without asking — full trust — still fully audited
- remember: ask once per action verb, then reuse that decision (approve →
  auto-run same verb; reject → auto-decline same verb)

- gateDecision(requester, verb) returns auto / ask / reject
- ApprovalRequest.Verb carries the action verb (remember key + audit)
- handleOwnerDecision caches the decision per verb under the remember policy
- applyRoleConfig fills opts.ConfirmPolicy; launchConnector wires + logs it

Tests: auto runs without asking, manual asks, remember reuses the 1st decision.
2026-06-15 17:22:45 +08:00
修雨 e58e805b17 feat(connect): enforce role capability scopes — keep a twin in its lane
RoleConfig.allowed_scopes was parsed but never enforced. Now an action whose
product is outside the role's allowlist is refused before it reaches the gate,
so an HR-assistant role can't be made to touch code/drive.

- applyRoleConfig fills opts.RoleScopes from the role's allowed_scopes
- the orchestrator gains allowedScopes + scopeAllows(product); empty = allow all
- handleReply refuses an out-of-scope action up front and tells the requester
  it is out of the twin's lane (a capability boundary, not an approval, so safe
  to surface); nothing reaches the gate or the owner
- launchConnector wires opts.RoleScopes into both orchestrators and logs the
  boundary at startup

Tests: out-of-scope refused (not gated), in-scope proceeds, empty = allow all.
2026-06-15 17:13:02 +08:00
修雨 79bab762bf feat(connect): private owner approval, self auto-approve, online-sheet audit
Builds on the text-approval gate with the digital-twin model the owner asked
for: the approval stays strictly between the bot and its owner, and every
action is auditable in DingTalk.

- private owner DM: an action request is no longer posted into the requester's
  conversation; the bot DMs the OWNER privately (robot oToMessages/batchSend,
  reusing the bot's own credentials) and the requester sees nothing until the
  result. Decision matching moved from per-conversation to latestPending, since
  the owner replies from their own 1:1 chat, not the request conversation.
- owner self auto-approve: when the requester IS the owner, asking is the
  authorization — the action runs immediately with no second confirmation, but
  is still recorded (auto_approved=true, decided_by=owner) for audit.
- online-sheet audit: optional --audit-sheet / --audit-sheet-tab appends one
  row per terminal action (time, summary, requester, state, decided_by, auto,
  error, id) to a DingTalk online sheet via sheet append_rows, reviewable in
  DingTalk on any device — beyond the local approvals JSON.
- aiCardClient.sendOTOText: proactive 1:1 robot text send primitive.
- requester gets the final outcome (executed/failed/rejected) by 1:1 message;
  the approval process itself is never exposed to them.

Tests: parseDecisionWord, latestPending, private-DM approve/reject,
owner self auto-approve, audit terminal records, classifier wiring intact.
2026-06-15 14:47:02 +08:00
修雨 45b4f088af feat(connect): text approval fallback — owner confirms in chat, no card needed
The confirmation gate previously REQUIRED a hand-built interactive card
template (per app, manual button param binding in the card console) — and
without one the gate silently turned off. That manual step is too clunky to
be the default. Add a text-approval mode so the digital twin works with zero
card-platform setup: when --owner-user-id is set but no --approval-card-
template, the bot posts a plain confirmation and the owner replies 「同意」/
「拒绝」 in chat to decide.

- newTextApprovalOrchestrator: gate enabled on owner alone (no card sender)
- text mode is async, NOT blocking: handleReply posts the prompt and returns;
  the owner's decision arrives as an ordinary inbound message captured by
  handleOwnerDecision (blocking on Await would deadlock the per-conversation
  worker that must also process that reply)
- gate.pendingForConv maps an owner reply (carries only the conversation) back
  to its request; parseDecisionWord matches a WHOLE-message keyword so an
  embedded 「同意」 never accidentally approves
- only the configured owner can decide; non-owner / non-keyword pass through
- devapp_connect: no card template now falls back to text mode instead of
  disabling the gate
- existing card flow refactored into handleReplyCard, unchanged behavior
- tests: parseDecisionWord, pendingForConv, text-mode approve/reject,
  non-owner rejected, non-decision pass-through
2026-06-14 11:05:13 +08:00
修雨 8b39dac5db feat(connect): wire role config into the connector runtime
Sprint 2 item 2: a digital-employee role config (connect_role.go) was a
defined-but-unused schema. Wire it into the connector so a single
--role-config <file> supplies the bot's owner, persona and knowledge
sources instead of scattering them across flags.

- add --role-config / DWS_ROLE_CONFIG; load + validate in launchConnector
  with a client_id-must-match-this-bot guard (one role == one bot)
- applyRoleConfig merges role -> options with explicit-flag-wins semantics
  (a flag/env always overrides the role; knowledge sources are additive)
- owner feeds the approval gate; persona is prepended to every forwarded
  prompt; role knowledge sources load and merge into the same retriever
- AllowedScopes / ConfirmPolicy are parsed and logged but not yet enforced
  (scope gating + auto/remember strategies are a later slice; no dead opts
  fields carried for them)
- tests: applyRoleConfig flag-wins / fill-empty / nil-role

Scope enforcement and confirm-policy behavior intentionally deferred.
2026-06-14 10:41:37 +08:00
修雨 d43ec228aa feat(connect): digital-twin confirmation gate vertical slice
Add the M2 confirmation gate (digital twin): an action request from a
group member is not executed directly. The bot sends an [Approve]/[Reject]
card to the owner over the existing Stream long-connection and only runs the
planned command after the owner approves.

- connect_approval.go: gate engine. ApprovalRequest model, Pending to
  Approved/Rejected to Executed/Failed state machine, thread-safe store with
  crash-safe atomic on-disk persistence (~/.dws/connect/<clientId>/approvals,
  0600, restart-recoverable), Submit/Decide/Await/Get API, and the simplified
  [[ACTION:...]] marker detection for execution-class requests.
- connect_approval_card.go: approvalCardSender boundary (DI-fakeable), the
  card-callback decoder (approval id + decision from action params, OutTrackId
  fallback), the orchestrator driving Submit to card to Await to execute to
  reply, and the real DingTalk interactive-card sender.
- connect_stream.go / devapp_connect.go: minimal wiring. Register the card
  callback router, decorate the prompt for action detection, route a marked
  reply through the gate, --owner-user-id / --approval-card-template flags.

Tests: state machine + persistence/recovery, card-action decode, reject path
(no execution), and a no-network end-to-end approve chain. gofmt/vet/build/
test all green.
2026-06-13 23:34:37 +08:00
修雨 25f69b867e Merge pull request #18 from PeterGuy326/feat/connect-knowledge-wiki
feat(connect): mount DingTalk wiki space as a knowledge source
2026-06-13 22:58:23 +08:00
修雨 dd0990691e Merge pull request #16 from PeterGuy326/feat/connect-session-persist
feat(connect): persist conversation sessions across connector restarts
2026-06-13 22:58:11 +08:00
修雨 cf277f3c8f feat(connect): add wiki/doc knowledge source for the Q&A connector
Extend the connector's knowledge retrieval (previously local .md/.txt only)
to a DingTalk knowledge base via --knowledge-source wiki:<spaceId> (and
doc:<docId>). On startup the connector pulls the space by reusing the existing
`doc` tools that back `dws doc list` / `dws doc read` (list_nodes +
get_document_content) through the shared executor.Runner -- no new DingTalk API
call is implemented. Pulled documents are dumped as markdown into a local cache
(~/.dws/connect/<clientId>/knowledge/wiki-<spaceId>/) and indexed by the same
chunker/retriever as the local-directory source.

Refresh on startup; on a pull failure (network/permission) fall back to a stale
cache if present, otherwise an empty knowledge base -- the connection is never
blocked. The legacy --knowledge-dir source is untouched and coexists.

Node listing uses a shallow id lookup so a response wrapper object is not
mistaken for a single node (which would drop sibling nodes).
2026-06-13 21:43:48 +08:00
修雨 59100407dc feat(connect): persist conversation sessions across connector restarts
convSessions held the DingTalk-conversation to agent-session map purely
in memory, so a connector restart dropped every chat's multi-turn
context - the core weakness of an always-on digital employee.

Persist the map to ~/.dws/connect/<clientId>/sessions.json (scoped per
clientId so multiple bots on one machine stay isolated), reusing the
existing config dir, 0600 FilePerm and lock-id sanitizer conventions:

- newConvSessions restores the map on startup; a missing or corrupt
  file degrades to an empty map with a warning, never a panic or a
  blocked connection.
- args/reset persist after every mutation, under the existing mutex so
  the snapshot is race-free, via a temp-file + rename atomic write.
- saves are best-effort: a write failure only logs a warning and never
  blocks message handling.
- an empty path (clientId missing or --agent-memory off) keeps the map
  in memory only, preserving the original behaviour exactly.

forwarderForChannel now takes clientId to derive the per-bot store path.
Adds unit tests for reload, corrupt-file degradation, in-memory mode and
concurrent access (race-clean).
2026-06-13 21:39:45 +08:00
修雨 10de987e81 fix(connect): stop echoing provider 422 into chat; drop haiku pin for custom providers
issue #14: the "422" seen in the group was not from the DingTalk card API but
from the claude subprocess's model provider, echoed back as the reply.

Root cause:
- claudecode's spec pins --model claude-haiku-4-5-20251001 in both argvTail and
  streamArgvTail.
- claudeUserSettingsEnv (issue #10) injects the user's ANTHROPIC_BASE_URL /
  ANTHROPIC_AUTH_TOKEN into the claude subprocess. With a third-party provider
  relay that has no mapping for that exact haiku pin, the provider returns
  "API Error: 422 ...".
- execForwarder.forward used cmd.Output() and returned any non-empty stdout as
  the answer without checking err, so the raw 422 text was forwarded to the chat
  on every message.

Fix (A + B):
A. In forwarderForChannel, when a custom provider base URL is in effect
   (injected via Claude settings or already in the process env) and the user did
   not pick a model (--agent-model / DWS_AGENT_MODEL), strip the built-in model
   pin so the provider's default model applies. Official-login users keep the
   pin; an explicit --agent-model still wins.
B. In forward and forwardStream, treat stdout that starts with "API Error:"
   (or a process error) as a failure: return a short, actionable Chinese hint
   ("...请用 --agent-model <model> 指定模型后重连.") instead of echoing the raw
   backend error. Normal replies are unaffected.

Adds unit tests for the haiku-drop wiring, the API-Error guard, and the new
helpers.
2026-06-13 14:43:24 +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
修雨 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
修雨 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
修雨 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
修雨 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
修雨 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