@xdxer/dingtalk-agent 0.1.5-beta.3 → 0.1.5-beta.4

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.en.md +92 -65
  3. package/README.md +92 -65
  4. package/dist/bin/dingtalk-agent.js +142 -15
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-platform.js +1 -0
  7. package/dist/src/agent-platform.js.map +1 -1
  8. package/dist/src/development-workspace.js +66 -3
  9. package/dist/src/development-workspace.js.map +1 -1
  10. package/dist/src/multica-deploy.js +101 -9
  11. package/dist/src/multica-deploy.js.map +1 -1
  12. package/dist/src/multica-provider.js +157 -2
  13. package/dist/src/multica-provider.js.map +1 -1
  14. package/dist/src/schedule-plan.js +105 -0
  15. package/dist/src/schedule-plan.js.map +1 -0
  16. package/docs/ARCHITECTURE.md +133 -18
  17. package/docs/PRIOR-ART.md +4 -0
  18. package/docs/SELF-TEST.md +1 -1
  19. package/docs/architecture/agent-platform-connection-layer.svg +120 -0
  20. package/docs/architecture/digital-employee-composition.svg +92 -0
  21. package/docs/architecture/dingtalk-agent-architecture.svg +125 -0
  22. package/docs/assets/digital-employee-at-work.svg +77 -0
  23. package/docs/schemas/multica-workspace-run.schema.json +150 -33
  24. package/docs/schemas/project.schema.json +22 -0
  25. package/docs/schemas/workspace-scaffold.schema.json +37 -0
  26. package/examples/agents/README.md +1 -1
  27. package/lab/project-workspace/fake-multica-provider.mjs +18 -2
  28. package/lab/project-workspace/opencode-provider-suite.json +1 -1
  29. package/lab/robot-eval/suite.json +1 -1
  30. package/package.json +2 -2
  31. package/skills/core/dingtalk-agent-compose/SKILL.md +4 -3
  32. package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
  33. package/skills/core/dingtalk-agent-compose/references/drive-and-schedules.md +124 -0
  34. package/skills/core/dingtalk-basic-behavior/SKILL.md +5 -4
  35. package/skills/core/dingtalk-basic-behavior/references/event-to-behavior.md +15 -1
  36. package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +3 -0
  37. package/skills/core/dingtalk-basic-behavior/references/risk-authority-and-privacy.md +12 -0
  38. package/skills/platforms/deap/PLATFORM.md +30 -1
  39. package/skills/platforms/multica-dingtalk/PLATFORM.md +25 -0
  40. package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +3 -2
  41. package/skills/platforms/multica-dingtalk/multica-external/SKILL.md +15 -0
  42. package/docs/assets/agent-delivery-lifecycle.svg +0 -103
@@ -63,7 +63,11 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
63
63
  throw new Error(`Multica workspace run 前置条件不完整: ${plan.blocking.join(', ')}`);
64
64
  }
65
65
  const command = options.command || env.DTA_MULTICA_BIN || 'multica';
66
- const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
66
+ // A real Issue-channel run does agent work plus issue get/comment/status side-tasks and
67
+ // routinely needs minutes; the old 30s default timed out on almost every honest run. Wait
68
+ // 4 minutes by default (still capped at 5). A run that overruns is resumable via
69
+ // `workspace run --status --run-id` against the same Issue — no duplicate Issue.
70
+ const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
67
71
  const calls = [];
68
72
  const inspection = inspectMulticaWorkspace(info.root, name, {
69
73
  execute: true, yes: true, persist: false, command, env,
@@ -86,6 +90,18 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
86
90
  '--description', normalizedPrompt, '--assignee-id', plan.agentId, '--output', 'json',
87
91
  ]), 'workspace-run.issue.create', 'write', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue');
88
92
  const issueId = resourceId(issue.id, 'workspace run issue.id');
93
+ return readbackWorkspaceRun({
94
+ root: info.root, name, plan, issueId, runId, command, env, timeoutMs, calls, out: options.out,
95
+ });
96
+ }
97
+ /**
98
+ * Poll an existing Issue's assignee-agent task to a terminal state, then reconstruct the
99
+ * run report from its trace. Shared by the initial `workspace run` (right after it creates the
100
+ * Issue) and `workspace run --status --run-id` (resume against the SAME Issue, no new write) so
101
+ * a timed-out run reconciles instead of spawning a duplicate Issue.
102
+ */
103
+ function readbackWorkspaceRun(ctx) {
104
+ const { root, name, plan, issueId, runId, command, env, timeoutMs, calls } = ctx;
89
105
  const started = Date.now();
90
106
  const pollIntervalMs = 2_000;
91
107
  const pollCalls = [];
@@ -95,7 +111,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
95
111
  pollAttempts += 1;
96
112
  const runs = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
97
113
  'issue', 'runs', issueId, '--full-id', '--output', 'json',
98
- ]), 'workspace-run.issue.runs', 'read', info.root, env, Math.min(timeoutMs, 30_000), pollCalls).stdout, 'workspace run issue runs');
114
+ ]), 'workspace-run.issue.runs', 'read', root, env, Math.min(timeoutMs, 30_000), pollCalls).stdout, 'workspace run issue runs');
99
115
  task = runs.filter((item) => !optionalString(item.agent_id) ||
100
116
  optionalString(item.agent_id) === plan.agentId)
101
117
  .sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
@@ -111,7 +127,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
111
127
  if (taskId && ['completed', 'failed', 'cancelled'].includes(taskStatus)) {
112
128
  messages = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
113
129
  'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
114
- ]), 'workspace-run.issue.messages', 'read', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run messages');
130
+ ]), 'workspace-run.issue.messages', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run messages');
115
131
  }
116
132
  const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
117
133
  const toolNames = toolUses.map((item) => optionalString(item.tool)).filter(Boolean);
@@ -125,7 +141,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
125
141
  const answer = safeRemoteOutput(replyWrites.at(-1) || texts.at(-1) || '', 64 * 1024);
126
142
  const issueReadback = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
127
143
  'issue', 'get', issueId, '--output', 'json',
128
- ]), 'workspace-run.issue.readback', 'read', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue readback');
144
+ ]), 'workspace-run.issue.readback', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue readback');
129
145
  const issueStatus = optionalString(issueReadback.status);
130
146
  const failures = [];
131
147
  if (!task)
@@ -165,17 +181,88 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
165
181
  calls,
166
182
  evidencePath: '',
167
183
  remoteRead: true,
168
- remoteWrite: true,
184
+ remoteWrite: calls.some((call) => call.kind === 'write'),
169
185
  triggerWrite: false,
170
186
  sideEffect: 'multica-issue-read-write',
171
187
  dingtalkSideEffect: false,
172
188
  };
173
189
  const defaultEvidence = join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
174
- report.evidencePath = relative(info.root, safeProjectPath(info.root, options.out || defaultEvidence, false));
190
+ report.evidencePath = relative(root, safeProjectPath(root, ctx.out || defaultEvidence, false));
175
191
  assertReportRedacted(report);
176
- atomicJson(safeProjectPath(info.root, report.evidencePath, false), report);
192
+ atomicJson(safeProjectPath(root, report.evidencePath, false), report);
177
193
  return report;
178
194
  }
195
+ /**
196
+ * Resume a `workspace run` by re-reading the SAME Issue's task instead of creating a new one.
197
+ * Loads the persisted run evidence by runId, confirms the workspace scope still resolves to the
198
+ * same target (fail closed on drift), then polls that Issue's assignee-agent task to a terminal
199
+ * state and rewrites the run report. Read-only against Multica — never enqueues a second Issue.
200
+ */
201
+ export function statusMulticaWorkspaceRun(start, name, options = {}) {
202
+ const env = options.env || process.env;
203
+ const cliVersion = options.cliVersion || '';
204
+ const runId = String(options.runId || '').trim();
205
+ if (!/^multica_workspace_run_[0-9]+_[a-f0-9]{8}$/.test(runId)) {
206
+ throw new Error('workspace run --status 需要一个合法的 --run-id(来自上次 run 的 runId)');
207
+ }
208
+ if (!options.execute) {
209
+ throw new Error('workspace run --status 会向 Multica 独立回读同一 Issue;必须传 --execute');
210
+ }
211
+ if (!options.yes)
212
+ throw new Error('workspace run --status 远端回读必须同时传 --execute --yes');
213
+ const info = inspectAgentProject(start, cliVersion);
214
+ requireMulticaWorkspace(info.root, name, cliVersion);
215
+ // The original run persists evidence to `--out` when given, else the default runId path. Resume
216
+ // must look where the run actually wrote, so honor the same `--out` (and rewrite back to it).
217
+ const evidenceRel = options.out || join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
218
+ const evidenceAbs = safeProjectPath(info.root, evidenceRel, false);
219
+ if (!existsSync(evidenceAbs)) {
220
+ throw new Error(`找不到 run 证据 ${evidenceRel};--run-id 必须是本 Workspace 已有的 runId` +
221
+ `${options.out ? '' : '(若原 run 用了 --out,请对 --status 传相同 --out)'}`);
222
+ }
223
+ const prior = JSON.parse(readFileSync(evidenceAbs, 'utf8'));
224
+ if (prior.$schema !== 'dingtalk-agent/multica-workspace-run@1' ||
225
+ prior.provider !== 'multica' || prior.workspace !== name || !prior.issueId) {
226
+ throw new Error('run 证据不是本 Workspace 的 multica run,或缺少 issueId;已 fail closed');
227
+ }
228
+ // The evidence file must belong to THIS runId. Two runs sharing one --out overwrite each
229
+ // other, so resuming the first runId against that path would otherwise read the second run's
230
+ // Issue while reporting the first runId — cross-wiring answer and run. Fail closed on mismatch.
231
+ if (prior.runId !== runId) {
232
+ throw new Error(`run 证据 ${evidenceRel} 属于 runId ${prior.runId},与 --run-id ${runId} 不一致;已 fail closed` +
233
+ `(同一 --out 被后一次 run 覆盖时会这样)`);
234
+ }
235
+ // Re-resolve scope offline and refuse unless the WHOLE frozen target still matches: an env
236
+ // pointing at a different runtime/agent/profile (or an invalid ref surfacing in blocking) must
237
+ // not let the resume mis-record the old task or overwrite the frozen values. workspaceId alone
238
+ // is not enough.
239
+ const providerPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
240
+ const scopeDrift = providerPlan.blocking.length ? ['blocking:' + providerPlan.blocking.join(',')] : [];
241
+ if (providerPlan.profile !== prior.profile)
242
+ scopeDrift.push('profile');
243
+ if (providerPlan.expected.workspaceId !== prior.workspaceId)
244
+ scopeDrift.push('workspace');
245
+ if (providerPlan.expected.runtimeId !== prior.runtimeId)
246
+ scopeDrift.push('runtime');
247
+ if (providerPlan.expected.agentId !== prior.agentId)
248
+ scopeDrift.push('agent');
249
+ if (scopeDrift.length) {
250
+ throw new Error(`workspace run --status: 当前 workspace scope 与 run 证据不一致 [${scopeDrift.join(', ')}];已 fail closed`);
251
+ }
252
+ const command = options.command || env.DTA_MULTICA_BIN || 'multica';
253
+ const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
254
+ const plan = {
255
+ profile: providerPlan.profile,
256
+ workspaceId: providerPlan.expected.workspaceId,
257
+ runtimeId: providerPlan.expected.runtimeId,
258
+ agentId: providerPlan.expected.agentId,
259
+ promptHash: prior.promptHash,
260
+ };
261
+ return readbackWorkspaceRun({
262
+ root: info.root, name, plan, issueId: prior.issueId, runId,
263
+ command, env, timeoutMs, calls: [], out: options.out,
264
+ });
265
+ }
179
266
  export function planMulticaDeployment(start, name, options = {}) {
180
267
  const action = options.action || 'apply';
181
268
  const env = options.env || process.env;
@@ -299,7 +386,7 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
299
386
  receipt = retireDeployment(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, calls);
300
387
  }
301
388
  else {
302
- receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000, 60_000), Boolean(options.noWait), calls, rollback, temp);
389
+ receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000, 300_000), Boolean(options.noWait), calls, rollback, temp);
303
390
  }
304
391
  }
305
392
  catch (error) {
@@ -866,7 +953,12 @@ function safeSmokeResponsePath(value) {
866
953
  const names = new Set(['reply.md', 'result.md', 'result.json']);
867
954
  if (value.startsWith('./'))
868
955
  return names.has(value.slice(2));
869
- if (!value.startsWith('/') || !names.has(value.split('/').at(-1) || ''))
956
+ // 裸文件名与 `./` 前缀等价:Agent workdir 里写绝对路径、随后用裸名引用同一个文件是
957
+ // 平台的常见写法。只放行精确 basename —— `foo/result.md` 与 `../result.md` 仍然落到
958
+ // 下面的绝对路径分支并被拒绝,白名单没有变宽。
959
+ if (!value.startsWith('/'))
960
+ return names.has(value);
961
+ if (!names.has(value.split('/').at(-1) || ''))
870
962
  return false;
871
963
  return value.split('/').every((part) => !part ||
872
964
  (part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));