@xdxer/dingtalk-agent 0.1.4-beta.10 → 0.1.4-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,280 @@
1
+ ---
2
+ name: multica-fde-external
3
+ description: External Python wrapper defining the full developer chain for delivering an agent on the Multica (DingTalk-FDE fork) platform — workspace bootstrap, runtime/agent provisioning, skill assembly (push/pull), DingTalk robot & account binding, execution-trace observability, cron scheduling (autopilot), and health checks. Use when provisioning, operating, debugging, or observing a Multica-managed agent from the command line with only an endpoint and a token.
4
+ ---
5
+
6
+ # Multica FDE External Skill
7
+
8
+ This skill is the **Multica adapter** of the Managed Agent Platform developer
9
+ chain: everything a developer needs to take an agent from nothing to a
10
+ customer-facing DingTalk bot, then observe, schedule, and health-check it.
11
+ Multica may be only one of several managed platforms; this skill defines
12
+ exactly which capabilities the Multica backend provides and wraps each one as
13
+ a script command. An external orchestrator can fetch this skill (see
14
+ *Skill logistics*) and execute its scripts.
15
+
16
+ Deliberately **decoupled from the Go CLI**: nothing patches `multica`;
17
+ everything is plain HTTPS against the server. Only two inputs are needed —
18
+ **server URL** and **token** — both reusable from the CLI's stored config.
19
+
20
+ - Script: `scripts/multica_ext.py` — single file, stdlib only, Python >= 3.9.
21
+ - Proxy-immune: the script ignores `HTTP(S)_PROXY` env vars by design (unlike
22
+ curl or the Go CLI), so an exported shell proxy cannot break pre/intranet
23
+ endpoints.
24
+ - All commands print JSON to stdout (status notes on stderr) — pipe to `jq`.
25
+ - Global flags may appear before or after the command.
26
+
27
+ ## Credentials
28
+
29
+ Resolution order (first hit wins):
30
+
31
+ 1. Flags: `--server-url`, `--token`, `--workspace <slug|uuid>`
32
+ 2. Env: `MULTICA_SERVER_URL`, `MULTICA_TOKEN`, `MULTICA_WORKSPACE_ID`
33
+ 3. `--profile <name>` → `~/.multica/profiles/<name>/config.json`
34
+ 4. `~/.multica/config.json` (keys: `server_url`, `workspace_id`, `token`)
35
+
36
+ The token is a `mul_` personal access token or login JWT; mint PATs with
37
+ `POST /api/tokens`.
38
+
39
+ ```bash
40
+ PY=".agents/skills/multica-fde-external/scripts/multica_ext.py"
41
+ python3 $PY --help
42
+ python3 $PY whoami --profile pre-fde
43
+ ```
44
+
45
+ ## The developer chain
46
+
47
+ ### 1 — Provision: workspace, runtime, agent
48
+
49
+ ```bash
50
+ python3 $PY workspace-init --profile pre-fde --name "FDE 团队" --slug fde-team \
51
+ --repo https://code.example.com/org/repo.git --complete-onboarding
52
+ python3 $PY runtime-create-fc --workspace fde-team --template-id <id> # cloud FC-E2B runtime
53
+ python3 $PY agent-create --workspace fde-team --name "FDE 教练" \
54
+ --instructions "..." --model claude-sonnet-5 # sole runtime auto-picked
55
+ ```
56
+
57
+ `workspace-init` is idempotent (409 → reuse when you are a member); repos
58
+ merge, never clobber; repo registration needs owner/admin. Slugs:
59
+ `^[a-z0-9]+(-[a-z0-9]+)*$`, reserved list enforced. An agent needs a runtime:
60
+ FC-E2B cloud (`runtime-create-fc`, requires server-side FC-E2B enablement) or
61
+ a local daemon. `runtime-templates` lists templates; when `--template-id` is
62
+ omitted and exactly one exists it is auto-picked.
63
+
64
+ ### 2 — Define & assemble: where the agent comes from, what skills it has
65
+
66
+ - `agent-source --agent <uuid>` — which git repo backs the agent (the
67
+ per-agent `agent_sources` link: `github` or `managed_git`; `null` = not
68
+ git-backed). `agent-source-sync` re-syncs github-sourced agents
69
+ (`managed_git` refuses with 409 by design — updated by the platform).
70
+ - Skills: `skill-list`, `skill-get`, `skill-push --dir <path>`
71
+ (local directory → workspace, `--on-conflict overwrite` replaces content
72
+ AND files wholesale), `skill-pull --skill <id-or-name> --dest <dir>`
73
+ (workspace → local directory). Assign skills to an agent with the Go CLI
74
+ (`multica agent skills add`) or `PUT /api/agents/{id}/skills`.
75
+ - Gap to know: there is no API to bind an *arbitrary* git URL to an agent;
76
+ per-agent git links are created only by the GitHub publish flow or the
77
+ managed FDE provision flow. Workspace-level repos (`workspace-repos`) and
78
+ the two flows above are the supported paths.
79
+
80
+ ### 3 — Deliver: bind the agent to a customer-facing DingTalk bot
81
+
82
+ The binding is a **channel installation** (one robot app per agent), not a
83
+ Skill object. AppKey/AppSecret are `client_id`/`client_secret` on the wire;
84
+ the secret is encrypted at rest (`MULTICA_DINGTALK_SECRET_KEY`) and never
85
+ returned. Inbound messages use DingTalk Stream Mode — no public webhook
86
+ registration needed.
87
+
88
+ ```bash
89
+ # Self-service: inject an existing robot app's credentials (owner/admin)
90
+ python3 $PY dingtalk-bind --agent <uuid> --client-id <AppKey> --client-secret-stdin < secret.txt
91
+ # Or scan-to-create: DingTalk mints the app, server captures credentials
92
+ python3 $PY dingtalk-begin --agent <uuid> --wait
93
+ python3 $PY dingtalk-list ; python3 $PY dingtalk-revoke --installation <uuid>
94
+ # Personal-account binding family (dingtalk_account):
95
+ python3 $PY dingtalk-account-begin --agent <uuid> # prints QR URL
96
+ python3 $PY dingtalk-account-list ; python3 $PY dingtalk-account-unbind --installation <uuid>
97
+ ```
98
+
99
+ Bind and unbind always come in pairs: robot `dingtalk-bind`/`dingtalk-begin`
100
+ ↔ `dingtalk-revoke`; account `dingtalk-account-begin` ↔
101
+ `dingtalk-account-unbind`. `dingtalk-account-begin` returns the binding link
102
+ (`qr_code_url`) directly — hand it to the counterpart to scan; check the
103
+ result with `dingtalk-account-list` (`dws_identity` carries the bound
104
+ account's org/display name).
105
+
106
+ Server rules: one robot per agent; re-binding the same `client_id` **moves**
107
+ the installation; cross-workspace `client_id` reuse → 409. Error map for
108
+ `dingtalk-bind`: **502** "dingtalk credentials check failed" = wrong
109
+ AppKey/AppSecret; **409** = ownership/agent conflict; **404** = agent not in
110
+ this workspace; `configured: false` in `dingtalk-list` = server lacks the
111
+ secret key. Deletes are audit-kept (`status: revoked`).
112
+
113
+ ### 4 — Observe: the agent's working and reasoning process
114
+
115
+ The full step-by-step trace (reasoning text, every tool call with input JSON,
116
+ every tool result, errors) is **persisted forever** in `task_messages` and
117
+ retrievable by REST — no backend changes needed:
118
+
119
+ ```bash
120
+ python3 $PY agent-tasks --agent <uuid> # run history (status, error, failure_reason, kind)
121
+ python3 $PY task-trace --task <uuid> --text # full reasoning/tool trace, human-readable
122
+ python3 $PY task-trace --task <uuid> --follow --text # live: polls until the task ends
123
+ python3 $PY issue-tasks --issue FDE-1 # all runs for an issue (uuid or identifier)
124
+ python3 $PY issue-timeline --issue FDE-1 # comments + activity log merged (status changes, task_completed...)
125
+ python3 $PY issue-usage --issue FDE-1 # aggregated token usage
126
+ python3 $PY task-snapshot # workspace-wide: active tasks + each agent's latest outcome
127
+ ```
128
+
129
+ Chat surfaces are REST-pollable too (`/api/chat/sessions/...` — session
130
+ messages, `pending-task`); realtime push is WebSocket `/ws` only (no SSE),
131
+ but a poll-only client fully reconstructs any past run from `task-trace`.
132
+
133
+ ### 5 — Schedule: cron automations (autopilot)
134
+
135
+ ```bash
136
+ python3 $PY autopilot-create --title "每日巡检" --agent <uuid> \
137
+ --description "<the prompt for each run>" --cron "0 9 * * 1-5" --timezone Asia/Shanghai
138
+ python3 $PY autopilot-trigger --autopilot <uuid> # run once, now
139
+ python3 $PY autopilot-runs --autopilot <uuid> # history: status, linked issue/task
140
+ python3 $PY autopilot-set-status --autopilot <uuid> --status paused # pause / active
141
+ python3 $PY autopilot-list ; python3 $PY autopilot-get --autopilot <uuid>
142
+ ```
143
+
144
+ Cron is standard 5-field (no seconds, no `@daily`), IANA timezone (default
145
+ UTC in the API; this script defaults `Asia/Shanghai`). `create_issue` mode
146
+ creates an issue per run (title template supports `{{date}}`) and enqueues an
147
+ agent task; `run_only` enqueues a task with no issue. Runs are idempotent per
148
+ (trigger, planned time); missed fires collapse to the latest, >5 min late is
149
+ skipped. Webhook triggers also exist (`POST /api/autopilots/{id}/triggers`
150
+ with `kind: webhook`).
151
+
152
+ ### 6 — Check: doctor report
153
+
154
+ ```bash
155
+ python3 $PY agent-check --agent <uuid>
156
+ ```
157
+
158
+ Composite JSON report: agent status/archived, runtime health (online /
159
+ unstable = offline but seen <5 min ago / offline / missing), active tasks +
160
+ last outcome (with `failure_reason`), recent failure count, DingTalk robot +
161
+ account binding state, plus a `checks[]` verdict list; `healthy` = no `fail`
162
+ (warns don't fail the check).
163
+
164
+ ### 7 — Basics: issues, comments, chat, members, tokens
165
+
166
+ Everyday Multica operations so an orchestrator needs nothing but this script:
167
+
168
+ ```bash
169
+ python3 $PY issue-create --title "..." --assignee-agent <uuid> # assigning an agent triggers a run
170
+ python3 $PY issue-get --issue FDE-1 ; python3 $PY issue-list --open-only --assignee <uuid>
171
+ python3 $PY issue-update --issue FDE-1 --status done
172
+ python3 $PY comment-add --issue FDE-1 --content "..." # "/note " prefix suppresses agent triggering
173
+ python3 $PY comment-list --issue FDE-1 --recent 10
174
+ python3 $PY chat-send --agent <uuid> --content "你好" --wait # direct conversation — the no-DingTalk e2e test path
175
+ python3 $PY member-list ; python3 $PY member-invite --email a@b.c --role member
176
+ python3 $PY token-create --name ci-bot --expires-days 90 # plaintext token shown once
177
+ ```
178
+
179
+ `chat-send` creates a session when `--session` is omitted and with `--wait`
180
+ polls `pending-task` until the assistant reply lands — the fastest way to
181
+ smoke-test a freshly provisioned agent before binding any channel.
182
+
183
+ ## Skill logistics: how skills move (the three planes)
184
+
185
+ - **Up (local → workspace):** `skill-push --dir` (this script);
186
+ `multica skill import --url|--file` (URL / `.skill`/`.zip` archive);
187
+ or the runtime *local-skills* API — the daemon scans the machine's
188
+ home-level skill dirs (`~/.claude/skills`, `~/.codex/skills`, the
189
+ cross-tool universal root `~/.agents/skills`, Claude plugin roots, etc.;
190
+ a "skill" = a directory with `SKILL.md`) and the workspace can list and
191
+ import them (`POST /api/runtimes/{id}/local-skills[/import]`, owner-only
192
+ for import).
193
+ - **Store:** workspace DB (`content` = SKILL.md, `files[]` = supporting
194
+ files; `SKILL.md` is a reserved file path).
195
+ - **Down (workspace → local):** automatic — the daemon materializes assigned
196
+ skills into each task's exec env (per-provider dirs, e.g.
197
+ `.claude/skills`, ephemeral, task-scoped). Manual/persistent — the Go CLI
198
+ has **no** download command; `skill-pull` (this script) is the downlink:
199
+ it writes `SKILL.md` + files into a local directory, byte-identical to
200
+ what was pushed. An external orchestrator can therefore
201
+ `skill-pull --skill multica-fde-external --dest <dir>` and execute the
202
+ scripts it receives.
203
+
204
+ ## Command → API map
205
+
206
+ | Command | API |
207
+ | --- | --- |
208
+ | `whoami` | `GET /api/me` |
209
+ | `workspace-list` / `workspace-create` / `workspace-init` | `GET`/`POST /api/workspaces` (+ `PUT /api/workspaces/{id}`, `POST /api/me/onboarding/complete`) |
210
+ | `workspace-repos` | `GET`/`PUT /api/workspaces/{id}` (`repos` JSONB) |
211
+ | `runtime-list` / `runtime-templates` / `runtime-create-fc` | `GET /api/runtimes`, `GET`/`POST /api/runtimes/fc-e2b(/templates)` |
212
+ | `agent-list` / `agent-get` / `agent-create` | `GET`/`POST /api/agents` |
213
+ | `agent-source` / `agent-source-sync` | `GET /api/agents/{id}/source`, `POST .../source/sync` |
214
+ | `agent-tasks` | `GET /api/agents/{id}/tasks` |
215
+ | `task-trace` | `GET /api/tasks/{taskId}/messages?since=` (+ `GET /api/agent-task-snapshot` for follow-stop) |
216
+ | `issue-tasks` / `issue-timeline` / `issue-usage` | `GET /api/issues/{id}/task-runs` / `/timeline` / `/usage` |
217
+ | `task-snapshot` | `GET /api/agent-task-snapshot` |
218
+ | `autopilot-*` | `GET`/`POST`/`PATCH /api/autopilots`, `POST .../trigger`, `GET .../runs`, `POST .../triggers` |
219
+ | `agent-check` | composite of agents/runtimes/snapshot/tasks/dingtalk endpoints |
220
+ | `dingtalk-bind` / `dingtalk-begin` | `POST /api/workspaces/{id}/dingtalk/install/manual` / `install/begin` + status poll |
221
+ | `dingtalk-list` / `dingtalk-revoke` | `GET`/`DELETE .../dingtalk/installations` |
222
+ | `dingtalk-account-*` | `POST`/`GET`/`DELETE .../dingtalk/account-bindings` |
223
+ | `skill-list` / `skill-get` / `skill-pull` / `skill-push` | `GET /api/skills(/{id})`, `POST /api/skills`, `PUT /api/skills/{id}` |
224
+ | `issue-create` / `issue-get` / `issue-list` / `issue-update` | `POST`/`GET`/`PUT /api/issues(/{id})` |
225
+ | `comment-add` / `comment-list` | `POST`/`GET /api/issues/{id}/comments` |
226
+ | `chat-send` | `POST /api/chat/sessions(/{id}/messages)` + `GET .../pending-task` + `GET .../messages` |
227
+ | `member-list` / `member-invite` | `GET`/`POST /api/workspaces/{id}/members` |
228
+ | `token-create` | `POST /api/tokens` |
229
+
230
+ Auth: `Authorization: Bearer <token>`; workspace-scoped generic routes use
231
+ the `X-Workspace-ID` header, `/api/workspaces/{id}/...` routes take the UUID
232
+ in the path. No cookies, no CSRF.
233
+
234
+ ## Distribution
235
+
236
+ This directory is self-contained (SKILL.md + scripts):
237
+
238
+ - Import into any Multica workspace: `multica skill import` from a URL or
239
+ `.skill`/`.zip` archive, or `skill-push` from this script. Files land in
240
+ agent envs without the exec bit — always invoke via
241
+ `python3 .../multica_ext.py` / `sh .../bootstrap.sh`, never `./x`.
242
+ - Or vendor into a dedicated git repo later; nothing depends on this
243
+ monorepo.
244
+
245
+ ### Bootstrap: getting this skill onto a fresh machine
246
+
247
+ The workspace itself is the registry — no extra storage needed. `skill-pull`
248
+ lives inside the skill, so the first fetch uses `scripts/bootstrap.sh`
249
+ (copy it from a runbook, teammate, or this repo):
250
+
251
+ ```bash
252
+ # Path A — authenticated multica CLI (no pull command needed: skill get
253
+ # outputs everything; bootstrap just writes it to disk):
254
+ sh bootstrap.sh --profile pre-fde --workspace-id <uuid> [--dest <dir>]
255
+
256
+ # Path B — no CLI at all, just endpoint + token (+ curl, python3):
257
+ MULTICA_SERVER_URL=... MULTICA_TOKEN=... MULTICA_WORKSPACE_ID=... \
258
+ sh bootstrap.sh --curl [--dest <dir>]
259
+ ```
260
+
261
+ Both paths produce a byte-identical copy of what `skill-push` uploaded, and
262
+ bootstrap.sh ships inside the bundle, so a pulled copy can bootstrap the
263
+ next machine. After bootstrap, stay current with
264
+ `python3 scripts/multica_ext.py skill-pull --skill multica-fde-external --dest . --force`.
265
+
266
+ Equivalent one-liner where bootstrap.sh is unavailable:
267
+
268
+ ```bash
269
+ multica skill get <skill-id> --output json | python3 -c '
270
+ import json,sys,pathlib
271
+ sk=json.load(sys.stdin); d=pathlib.Path(sk["name"]); d.mkdir(exist_ok=True)
272
+ (d/"SKILL.md").write_text(sk.get("content") or "")
273
+ for f in sk.get("files") or []:
274
+ p=d/f["path"]; p.parent.mkdir(parents=True,exist_ok=True); p.write_text(f.get("content") or "")'
275
+ ```
276
+
277
+ OSS (or any object store) is an optional *anonymous* channel — useful only
278
+ when the target machine has no Multica credentials yet. It needs a publish
279
+ step on every update and bucket/ACL management, so prefer the
280
+ workspace-as-registry paths above when a token exists.
@@ -0,0 +1,78 @@
1
+ #!/bin/sh
2
+ # Bootstrap: fetch the multica-fde-external skill from a Multica workspace
3
+ # onto this machine. Solves the chicken-and-egg problem (skill-pull lives
4
+ # inside the skill) using only tools already present: the multica CLI, or
5
+ # curl with endpoint+token. python3 is required either way (the skill's
6
+ # script needs it too).
7
+ #
8
+ # Path A (default) — authenticated multica CLI:
9
+ # sh bootstrap.sh [--profile <name>] [--workspace-id <uuid>] [--dest <dir>]
10
+ # Path B — no CLI, just endpoint + token:
11
+ # MULTICA_SERVER_URL=... MULTICA_TOKEN=... MULTICA_WORKSPACE_ID=... \
12
+ # sh bootstrap.sh --curl [--dest <dir>]
13
+ #
14
+ # curl runs with --noproxy '*' so an exported shell proxy cannot break
15
+ # intranet/pre endpoints (the Go CLI needs proxies unset by the caller).
16
+ set -eu
17
+
18
+ SKILL_NAME="multica-fde-external"
19
+ DEST=""
20
+ PROFILE=""
21
+ WS_ID=""
22
+ MODE="cli"
23
+
24
+ while [ $# -gt 0 ]; do
25
+ case "$1" in
26
+ --profile) PROFILE="$2"; shift 2 ;;
27
+ --workspace-id) WS_ID="$2"; shift 2 ;;
28
+ --dest) DEST="$2"; shift 2 ;;
29
+ --name) SKILL_NAME="$2"; shift 2 ;;
30
+ --curl) MODE="curl"; shift ;;
31
+ *) echo "unknown argument: $1" >&2; exit 1 ;;
32
+ esac
33
+ done
34
+
35
+ RESOLVE='
36
+ import json, sys
37
+ name = sys.argv[1]
38
+ ids = [s.get("id") for s in json.load(sys.stdin) if isinstance(s, dict) and s.get("name") == name]
39
+ print(ids[0] if ids else "", end="")
40
+ '
41
+
42
+ UNPACK='
43
+ import json, sys, pathlib
44
+ sk = json.load(sys.stdin)
45
+ dest = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else sk["name"]
46
+ d = pathlib.Path(dest)
47
+ d.mkdir(parents=True, exist_ok=True)
48
+ (d / "SKILL.md").write_text(sk.get("content") or "", encoding="utf-8")
49
+ count = 1
50
+ for f in sk.get("files") or []:
51
+ rel = f.get("path") or ""
52
+ parts = pathlib.Path(rel).parts
53
+ if not parts or pathlib.Path(rel).is_absolute() or ".." in parts or rel == "SKILL.md":
54
+ continue
55
+ p = d / rel
56
+ p.parent.mkdir(parents=True, exist_ok=True)
57
+ p.write_text(f.get("content") or "", encoding="utf-8")
58
+ count += 1
59
+ print("pulled %d file(s) -> %s" % (count, d.resolve()))
60
+ '
61
+
62
+ if [ "$MODE" = "cli" ]; then
63
+ CLI="multica"
64
+ [ -n "$PROFILE" ] && CLI="$CLI --profile $PROFILE"
65
+ [ -n "$WS_ID" ] && CLI="$CLI --workspace-id $WS_ID"
66
+ SID=$($CLI skill list --output json | python3 -c "$RESOLVE" "$SKILL_NAME")
67
+ [ -n "$SID" ] || { echo "skill \"$SKILL_NAME\" not found in the workspace" >&2; exit 1; }
68
+ $CLI skill get "$SID" --output json | python3 -c "$UNPACK" "$DEST"
69
+ else
70
+ : "${MULTICA_SERVER_URL:?set MULTICA_SERVER_URL}"
71
+ : "${MULTICA_TOKEN:?set MULTICA_TOKEN}"
72
+ : "${MULTICA_WORKSPACE_ID:?set MULTICA_WORKSPACE_ID}"
73
+ AUTH="Authorization: Bearer $MULTICA_TOKEN"
74
+ WS="X-Workspace-ID: $MULTICA_WORKSPACE_ID"
75
+ SID=$(curl -fsS --noproxy '*' -H "$AUTH" -H "$WS" "$MULTICA_SERVER_URL/api/skills" | python3 -c "$RESOLVE" "$SKILL_NAME")
76
+ [ -n "$SID" ] || { echo "skill \"$SKILL_NAME\" not found in the workspace" >&2; exit 1; }
77
+ curl -fsS --noproxy '*' -H "$AUTH" -H "$WS" "$MULTICA_SERVER_URL/api/skills/$SID" | python3 -c "$UNPACK" "$DEST"
78
+ fi