Commit Graph
8 Commits
Author SHA1 Message Date
修雨 6ce4a635b2 style: gofmt connect_approval.go (struct tag alignment) 2026-06-15 17:26:59 +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
修雨 1a2454a5ab feat(connect): twin can also create calendar events and docs, not just todos
The digital twin previously only knew one action (todo.create). Add two more
owner-scoped write actions so it can act as a real stand-in:

- calendar.create → calendar/create_calendar_event (summary + start/end)
- doc.create → doc/create_document (name)

Both flow through the same gate: detected via [[ACTION:...]] marker, classified
as write (gated), approved/deferred/audited identically. The agent system hint
now lists all three action markers. A shared firstNonEmpty arg helper.

Tests: toPlannedAction maps each verb to the right product/tool, missing
required args degrade to not-an-action, and every action classifies as write.
2026-06-15 16:55:37 +08:00
修雨 82d625603f feat(connect): deferred execution — don't lose a task when the twin can't act yet
When an approved action can't execute right now (most often the connector's
dws login is not the bot owner, so an owner-scoped action like creating a
personal todo fails), the request is no longer dropped with an opaque error.
Instead it is held and retried after the owner recovers.

- new 'deferred' state: an approved request whose execution didn't complete;
  persisted like any other, distinct from terminal 'failed'
- on execution failure (auto-approve path and owner-approve path), mark
  deferred instead of failed, and privately DM the owner what went wrong and
  how to recover (log dws in as the bot owner, then reply 「重试」). The
  requester is NOT told it failed — their task is held, not lost
- 「重试/恢复」owner keyword flushes the backlog: re-execute each deferred
  request oldest-first; on success mark executed and send the requester the
  outcome, on failure keep it deferred
- markDeferred / allDeferred on the gate; deferred counts as approved() so the
  retry can still mark it executed
- tests: deferred-hold-then-retry end to end, isRetryWord

This is the graceful-degradation half of the digital twin: a request survives
an owner-offline / wrong-identity window and completes once the owner is back.
2026-06-15 15:05:45 +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
修雨 9af7f9a034 feat(connect): gate engages via read/write classifier, not every marker
Sprint 2 item 1: wire the dws command read/write classifier into the
digital-twin approval gate. Previously any detected [[ACTION:...]] marker
was unconditionally routed through the owner-confirmation gate. Now the
gate consults ClassifyDwsCommand on the planned command: a read-class
action bypasses the gate and replies normally, while write — and Unknown,
per the CmdClass safety contract — keep the owner sign-off requirement.

- add classifyPlannedAction bridging plannedAction -> CmdClass (classifies
  on LegacyPath, falls back to product + RPC tool verb)
- handleReply skips Submit/card/Await for a read-class action
- tests: classifyPlannedAction table + handleReply read-class bypass
  (override-driven, exercises the wiring end to end)
2026-06-14 10:22:01 +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