chainlesschain 0.45.4 → 0.45.6
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.
- package/README.md +54 -0
- package/package.json +1 -1
- package/src/commands/orchestrate.js +513 -0
- package/src/commands/ui.js +6 -1
- package/src/index.js +6 -0
- package/src/lib/agent-router.js +397 -0
- package/src/lib/claude-code-bridge.js +353 -0
- package/src/lib/notifiers/dingtalk.js +99 -0
- package/src/lib/notifiers/feishu.js +112 -0
- package/src/lib/notifiers/index.js +183 -0
- package/src/lib/notifiers/telegram.js +131 -0
- package/src/lib/notifiers/websocket.js +67 -0
- package/src/lib/notifiers/wecom.js +74 -0
- package/src/lib/orchestrator.js +438 -0
- package/src/lib/web-ui-server.js +118 -3
- package/src/lib/ws-server.js +87 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationManager — unified multi-channel notification dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Supported channels:
|
|
5
|
+
* telegram — Telegram Bot API
|
|
6
|
+
* wecom — 企业微信群机器人 Webhook
|
|
7
|
+
* dingtalk — 钉钉群机器人 Webhook (支持加签)
|
|
8
|
+
* feishu — 飞书群机器人 Webhook (支持签名校验)
|
|
9
|
+
* websocket — Real-time push back over the triggering WS connection
|
|
10
|
+
*
|
|
11
|
+
* Supported incoming command sources (via HTTP Webhook receiver):
|
|
12
|
+
* wecom — 企业微信 回调 (XML format)
|
|
13
|
+
* dingtalk — 钉钉 outgoing webhook
|
|
14
|
+
* feishu — 飞书 event subscription
|
|
15
|
+
*
|
|
16
|
+
* Configuration (any combination via env vars or options):
|
|
17
|
+
* TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
|
|
18
|
+
* WECOM_WEBHOOK_URL
|
|
19
|
+
* DINGTALK_WEBHOOK_URL [+ DINGTALK_SECRET]
|
|
20
|
+
* FEISHU_WEBHOOK_URL [+ FEISHU_SECRET]
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* const nm = NotificationManager.fromEnv();
|
|
24
|
+
* await nm.notifySuccess({ taskId, description, ... });
|
|
25
|
+
*
|
|
26
|
+
* // With WebSocket channel:
|
|
27
|
+
* nm.addWebSocketChannel({ send: (data) => ws._send(ws, data), requestId });
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { TelegramNotifier } from "./telegram.js";
|
|
31
|
+
import { WeComNotifier } from "./wecom.js";
|
|
32
|
+
import { DingTalkNotifier } from "./dingtalk.js";
|
|
33
|
+
import { FeishuNotifier } from "./feishu.js";
|
|
34
|
+
import { WebSocketNotifier } from "./websocket.js";
|
|
35
|
+
|
|
36
|
+
// ─── Incoming command parsers ─────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a 钉钉 outgoing webhook payload into a command string.
|
|
40
|
+
* DingTalk sends: { msgtype: "text", text: { content: "..." }, ... }
|
|
41
|
+
*/
|
|
42
|
+
export function parseDingTalkIncoming(body) {
|
|
43
|
+
if (body?.msgtype === "text") {
|
|
44
|
+
return body.text?.content?.trim() || null;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a 飞书 event subscription payload into a command string.
|
|
51
|
+
* Feishu sends: { event: { message: { content: '{"text":"..."}' } } }
|
|
52
|
+
*/
|
|
53
|
+
export function parseFeishuIncoming(body) {
|
|
54
|
+
try {
|
|
55
|
+
const content = body?.event?.message?.content;
|
|
56
|
+
if (content) {
|
|
57
|
+
const parsed = JSON.parse(content);
|
|
58
|
+
return parsed.text?.trim() || null;
|
|
59
|
+
}
|
|
60
|
+
} catch (_err) {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a 企业微信 回调 XML body into a command string.
|
|
68
|
+
* WeCom sends XML; this extracts the <Content> field using simple regex
|
|
69
|
+
* (no heavy XML parser needed for plain text messages).
|
|
70
|
+
*/
|
|
71
|
+
export function parseWeComIncoming(xmlBody) {
|
|
72
|
+
const match = String(xmlBody).match(
|
|
73
|
+
/<Content><!\[CDATA\[([^\]]*)\]\]><\/Content>/,
|
|
74
|
+
);
|
|
75
|
+
return match ? match[1].trim() : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Notification Manager ─────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export class NotificationManager {
|
|
81
|
+
constructor() {
|
|
82
|
+
/** @type {Array<object>} registered notifier instances */
|
|
83
|
+
this._channels = [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a NotificationManager from environment variables.
|
|
88
|
+
* Any configured channel is automatically added.
|
|
89
|
+
*/
|
|
90
|
+
static fromEnv(options = {}) {
|
|
91
|
+
const nm = new NotificationManager();
|
|
92
|
+
|
|
93
|
+
const telegram = new TelegramNotifier(options.telegram || {});
|
|
94
|
+
if (telegram.isConfigured) nm.add("telegram", telegram);
|
|
95
|
+
|
|
96
|
+
const wecom = new WeComNotifier(options.wecom || {});
|
|
97
|
+
if (wecom.isConfigured) nm.add("wecom", wecom);
|
|
98
|
+
|
|
99
|
+
const dingtalk = new DingTalkNotifier(options.dingtalk || {});
|
|
100
|
+
if (dingtalk.isConfigured) nm.add("dingtalk", dingtalk);
|
|
101
|
+
|
|
102
|
+
const feishu = new FeishuNotifier(options.feishu || {});
|
|
103
|
+
if (feishu.isConfigured) nm.add("feishu", feishu);
|
|
104
|
+
|
|
105
|
+
return nm;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register a notifier under a name.
|
|
110
|
+
* @param {string} name
|
|
111
|
+
* @param {object} notifier - must implement notifySuccess/notifyFailure/notifyStart
|
|
112
|
+
*/
|
|
113
|
+
add(name, notifier) {
|
|
114
|
+
// Remove existing channel with same name before re-adding
|
|
115
|
+
this._channels = this._channels.filter((c) => c.name !== name);
|
|
116
|
+
this._channels.push({ name, notifier });
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Add (or replace) the WebSocket channel.
|
|
122
|
+
* Call this when an orchestration task is triggered from a WS connection.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} options
|
|
125
|
+
* @param {Function} options.send - (data: object) => void
|
|
126
|
+
* @param {string} [options.requestId] - correlating request ID
|
|
127
|
+
*/
|
|
128
|
+
addWebSocketChannel(options) {
|
|
129
|
+
const notifier = new WebSocketNotifier(options);
|
|
130
|
+
this.add("websocket", notifier);
|
|
131
|
+
return notifier; // caller may use directly for agent output streaming
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Remove a channel by name. */
|
|
135
|
+
remove(name) {
|
|
136
|
+
this._channels = this._channels.filter((c) => c.name !== name);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** List active channel names. */
|
|
141
|
+
get activeChannels() {
|
|
142
|
+
return this._channels.map((c) => c.name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get isConfigured() {
|
|
146
|
+
return this._channels.length > 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Broadcast methods ──────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/** Fan-out to all channels; collect results. */
|
|
152
|
+
async _broadcast(method, payload) {
|
|
153
|
+
const results = await Promise.allSettled(
|
|
154
|
+
this._channels.map(({ name, notifier }) =>
|
|
155
|
+
notifier[method]?.(payload)
|
|
156
|
+
.then((r) => ({ channel: name, ...r }))
|
|
157
|
+
.catch((err) => ({ channel: name, ok: false, reason: err.message })),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
return results.map((r) =>
|
|
161
|
+
r.status === "fulfilled" ? r.value : { ok: false, ...r.reason },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async notifyStart(summary) {
|
|
166
|
+
return this._broadcast("notifyStart", summary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async notifySuccess(summary) {
|
|
170
|
+
return this._broadcast("notifySuccess", summary);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async notifyFailure(summary) {
|
|
174
|
+
return this._broadcast("notifyFailure", summary);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Re-export individual notifiers for direct use
|
|
179
|
+
export { TelegramNotifier } from "./telegram.js";
|
|
180
|
+
export { WeComNotifier } from "./wecom.js";
|
|
181
|
+
export { DingTalkNotifier } from "./dingtalk.js";
|
|
182
|
+
export { FeishuNotifier } from "./feishu.js";
|
|
183
|
+
export { WebSocketNotifier } from "./websocket.js";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Notifier — sends CI pass/fail notifications via Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Configure via .chainlesschain/config.json:
|
|
5
|
+
* { "telegramBotToken": "...", "telegramChatId": "..." }
|
|
6
|
+
* or environment variables:
|
|
7
|
+
* TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TELEGRAM_API = "https://api.telegram.org";
|
|
11
|
+
|
|
12
|
+
export class TelegramNotifier {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string} options.token - Bot token (from BotFather)
|
|
16
|
+
* @param {string} options.chatId - Chat/group/channel ID
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.token = options.token || process.env.TELEGRAM_BOT_TOKEN || "";
|
|
20
|
+
this.chatId = options.chatId || process.env.TELEGRAM_CHAT_ID || "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get isConfigured() {
|
|
24
|
+
return Boolean(this.token && this.chatId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Send a raw text message (Markdown V2).
|
|
29
|
+
* @param {string} text
|
|
30
|
+
*/
|
|
31
|
+
async send(text) {
|
|
32
|
+
if (!this.isConfigured) return { ok: false, reason: "not configured" };
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${TELEGRAM_API}/bot${this.token}/sendMessage`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
chat_id: this.chatId,
|
|
40
|
+
text,
|
|
41
|
+
parse_mode: "HTML",
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return { ok: data.ok, data };
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { ok: false, reason: err.message };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Notify that a task's CI pipeline passed.
|
|
53
|
+
* @param {object} summary
|
|
54
|
+
* @param {string} summary.taskId
|
|
55
|
+
* @param {string} summary.description - Short task description
|
|
56
|
+
* @param {number} summary.agentCount - How many agents worked on it
|
|
57
|
+
* @param {number} summary.duration - Total ms
|
|
58
|
+
* @param {string[]} summary.filesChanged
|
|
59
|
+
*/
|
|
60
|
+
async notifySuccess(summary) {
|
|
61
|
+
const {
|
|
62
|
+
taskId,
|
|
63
|
+
description,
|
|
64
|
+
agentCount = 1,
|
|
65
|
+
duration,
|
|
66
|
+
filesChanged = [],
|
|
67
|
+
} = summary;
|
|
68
|
+
const mins = duration ? Math.round(duration / 60_000) : "?";
|
|
69
|
+
const files = filesChanged.length
|
|
70
|
+
? `\n📁 <b>Files:</b> ${filesChanged.slice(0, 5).join(", ")}${filesChanged.length > 5 ? ` +${filesChanged.length - 5} more` : ""}`
|
|
71
|
+
: "";
|
|
72
|
+
|
|
73
|
+
const text =
|
|
74
|
+
`✅ <b>CI Passed</b>\n` +
|
|
75
|
+
`📝 <b>Task:</b> ${_escape(description)}\n` +
|
|
76
|
+
`🤖 <b>Agents:</b> ${agentCount} ⏱ <b>Time:</b> ${mins}m` +
|
|
77
|
+
files +
|
|
78
|
+
`\n🔑 <code>${taskId}</code>`;
|
|
79
|
+
|
|
80
|
+
return this.send(text);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Notify that CI failed and agents are being retried.
|
|
85
|
+
* @param {object} summary
|
|
86
|
+
* @param {string} summary.taskId
|
|
87
|
+
* @param {string} summary.description
|
|
88
|
+
* @param {string[]} summary.errors - CI error messages
|
|
89
|
+
* @param {number} summary.retryNumber - Which retry attempt
|
|
90
|
+
*/
|
|
91
|
+
async notifyFailure(summary) {
|
|
92
|
+
const { taskId, description, errors = [], retryNumber = 1 } = summary;
|
|
93
|
+
const errPreview = errors
|
|
94
|
+
.slice(0, 3)
|
|
95
|
+
.map((e) => ` • ${_escape(e.slice(0, 120))}`)
|
|
96
|
+
.join("\n");
|
|
97
|
+
|
|
98
|
+
const text =
|
|
99
|
+
`❌ <b>CI Failed</b> (retry #${retryNumber})\n` +
|
|
100
|
+
`📝 <b>Task:</b> ${_escape(description)}\n` +
|
|
101
|
+
(errPreview ? `\n<b>Errors:</b>\n${errPreview}\n` : "") +
|
|
102
|
+
`🔄 Dispatching fix to agents…\n` +
|
|
103
|
+
`🔑 <code>${taskId}</code>`;
|
|
104
|
+
|
|
105
|
+
return this.send(text);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Notify that orchestration is starting.
|
|
110
|
+
* @param {object} summary
|
|
111
|
+
* @param {string} summary.taskId
|
|
112
|
+
* @param {string} summary.description
|
|
113
|
+
* @param {number} summary.subtaskCount
|
|
114
|
+
*/
|
|
115
|
+
async notifyStart(summary) {
|
|
116
|
+
const { taskId, description, subtaskCount = 1 } = summary;
|
|
117
|
+
const text =
|
|
118
|
+
`🚀 <b>Orchestration Started</b>\n` +
|
|
119
|
+
`📝 <b>Task:</b> ${_escape(description)}\n` +
|
|
120
|
+
`🤖 <b>Subtasks:</b> ${subtaskCount}\n` +
|
|
121
|
+
`🔑 <code>${taskId}</code>`;
|
|
122
|
+
return this.send(text);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function _escape(str) {
|
|
127
|
+
return String(str)
|
|
128
|
+
.replace(/&/g, "&")
|
|
129
|
+
.replace(/</g, "<")
|
|
130
|
+
.replace(/>/g, ">");
|
|
131
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Notifier — pushes orchestration events back over the active WS connection.
|
|
3
|
+
*
|
|
4
|
+
* When `cc orchestrate` is triggered via the WebSocket interface, notifications
|
|
5
|
+
* are sent to the originating client in real-time instead of (or in addition to)
|
|
6
|
+
* external channels.
|
|
7
|
+
*
|
|
8
|
+
* The caller passes a `sendFn` bound to the specific WS client:
|
|
9
|
+
* const wsNotifier = new WebSocketNotifier({ send: (data) => _send(ws, data) });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class WebSocketNotifier {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {Function} options.send - (data: object) => void — bound to one WS client
|
|
16
|
+
* @param {string} [options.taskId] - correlate events to the originating request
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this._send = options.send || (() => {});
|
|
20
|
+
this.requestId = options.requestId || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get isConfigured() {
|
|
24
|
+
return typeof this._send === "function";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Send a typed orchestration event to the WebSocket client. */
|
|
28
|
+
send(event, payload = {}) {
|
|
29
|
+
this._send({
|
|
30
|
+
type: "orchestrate:event",
|
|
31
|
+
event,
|
|
32
|
+
requestId: this.requestId,
|
|
33
|
+
...payload,
|
|
34
|
+
ts: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async notifyStart(summary) {
|
|
39
|
+
this.send("start", summary);
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async notifySuccess(summary) {
|
|
44
|
+
this.send("ci:pass", summary);
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async notifyFailure(summary) {
|
|
49
|
+
this.send("ci:fail", summary);
|
|
50
|
+
return { ok: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Forward real-time agent output chunk to the client. */
|
|
54
|
+
sendAgentOutput({ agentId, taskId, chunk }) {
|
|
55
|
+
this.send("agent:output", { agentId, taskId, chunk });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Forward task status update. */
|
|
59
|
+
sendStatus(task) {
|
|
60
|
+
this.send("task:status", {
|
|
61
|
+
taskId: task.id,
|
|
62
|
+
status: task.status,
|
|
63
|
+
retries: task.retries,
|
|
64
|
+
subtasks: task.subtasks?.length,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom (企业微信) Notifier — sends notifications via 企业微信群机器人 Webhook.
|
|
3
|
+
*
|
|
4
|
+
* Configure via env:
|
|
5
|
+
* WECOM_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
|
|
6
|
+
* or options.webhookUrl
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class WeComNotifier {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.webhookUrl = options.webhookUrl || process.env.WECOM_WEBHOOK_URL || "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get isConfigured() {
|
|
15
|
+
return Boolean(this.webhookUrl);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Send a markdown message to the 企业微信 group bot.
|
|
20
|
+
* @param {string} content - Markdown content (企业微信 subset)
|
|
21
|
+
*/
|
|
22
|
+
async send(content) {
|
|
23
|
+
if (!this.isConfigured) return { ok: false, reason: "not configured" };
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(this.webhookUrl, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
msgtype: "markdown",
|
|
30
|
+
markdown: { content },
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
return { ok: data.errcode === 0, data };
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { ok: false, reason: err.message };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async notifySuccess(summary) {
|
|
41
|
+
const { taskId, description, agentCount = 1, duration } = summary;
|
|
42
|
+
const mins = duration ? Math.round(duration / 60_000) : "?";
|
|
43
|
+
const content =
|
|
44
|
+
`## ✅ CI 通过\n` +
|
|
45
|
+
`**任务**: ${description.slice(0, 100)}\n` +
|
|
46
|
+
`**Agent 数**: ${agentCount} **耗时**: ${mins}m\n` +
|
|
47
|
+
`**ID**: \`${taskId}\``;
|
|
48
|
+
return this.send(content);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async notifyFailure(summary) {
|
|
52
|
+
const { taskId, description, errors = [], retryNumber = 1 } = summary;
|
|
53
|
+
const errLines = errors
|
|
54
|
+
.slice(0, 3)
|
|
55
|
+
.map((e) => `> ${e.slice(0, 100)}`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
const content =
|
|
58
|
+
`## ❌ CI 失败 (第 ${retryNumber} 次重试)\n` +
|
|
59
|
+
`**任务**: ${description.slice(0, 100)}\n` +
|
|
60
|
+
(errLines ? `**错误**:\n${errLines}\n` : "") +
|
|
61
|
+
`**ID**: \`${taskId}\``;
|
|
62
|
+
return this.send(content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async notifyStart(summary) {
|
|
66
|
+
const { taskId, description, subtaskCount = 1 } = summary;
|
|
67
|
+
const content =
|
|
68
|
+
`## 🚀 开始编排\n` +
|
|
69
|
+
`**任务**: ${description.slice(0, 100)}\n` +
|
|
70
|
+
`**子任务**: ${subtaskCount}\n` +
|
|
71
|
+
`**ID**: \`${taskId}\``;
|
|
72
|
+
return this.send(content);
|
|
73
|
+
}
|
|
74
|
+
}
|