@ai-setting/roy-agent-core 1.7.49 → 1.7.50
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/dist/config/index.js +7 -7
- package/dist/env/agent/index.js +10 -10
- package/dist/env/commands/index.js +7 -7
- package/dist/env/context/index.js +3 -3
- package/dist/env/debug/index.js +7 -7
- package/dist/env/event-source/index.js +17 -14
- package/dist/env/hook/index.js +9 -6
- package/dist/env/index.js +38 -34
- package/dist/env/llm/index.js +7 -7
- package/dist/env/log-trace/index.js +7 -7
- package/dist/env/mcp/index.js +7 -7
- package/dist/env/memory/index.js +11 -11
- package/dist/env/plugin/index.js +7 -7
- package/dist/env/prompt/index.js +8 -8
- package/dist/env/session/index.js +7 -7
- package/dist/env/skill/index.js +7 -7
- package/dist/env/task/delegate/index.js +8 -8
- package/dist/env/task/index.js +17 -17
- package/dist/env/task/plugins/index.js +10 -9
- package/dist/env/task/storage/index.js +2 -2
- package/dist/env/task/tools/index.js +3 -3
- package/dist/env/tool/built-in/index.js +2 -2
- package/dist/env/tool/index.js +8 -8
- package/dist/env/workflow/engine/index.js +12 -9
- package/dist/env/workflow/hooks/index.js +25 -0
- package/dist/env/workflow/index.js +19 -17
- package/dist/env/workflow/nodes/index.js +2 -2
- package/dist/env/workflow/plugins/index.js +18 -0
- package/dist/env/workflow/service/index.js +2 -2
- package/dist/env/workflow/storage/index.js +3 -3
- package/dist/env/workflow/tools/index.js +11 -11
- package/dist/env/workflow/utils/index.js +4 -4
- package/dist/index.js +79 -70
- package/dist/shared/@ai-setting/{roy-agent-core-3ajh3cdj.js → roy-agent-core-0cfaqnz9.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-trd6my5j.js → roy-agent-core-1cv9j4dg.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-48f17g3x.js → roy-agent-core-2gay3sjt.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-e302nvzt.js → roy-agent-core-2k4xffaa.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-1pjct4nj.js → roy-agent-core-35s1w7tc.js} +230 -96
- package/dist/shared/@ai-setting/{roy-agent-core-v6zdngr7.js → roy-agent-core-3tv1q6f2.js} +8 -8
- package/dist/shared/@ai-setting/{roy-agent-core-mqaht7mm.js → roy-agent-core-4z3rq5gw.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-fm77q3tn.js → roy-agent-core-5apmk4qw.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-xmfyt3dg.js → roy-agent-core-6167phd4.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-wt37tszm.js → roy-agent-core-7evvpfwc.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-azdsyhvx.js → roy-agent-core-7g03zkkb.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-v2d0bt7e.js → roy-agent-core-8s9jd2qy.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-8pqq9yjt.js → roy-agent-core-978b6wfx.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-9csa89jx.js +46 -0
- package/dist/shared/@ai-setting/{roy-agent-core-cnsjr3ws.js → roy-agent-core-9jzysq50.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-5ja22qkf.js → roy-agent-core-ak4dc3kg.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-ncaqkyfj.js → roy-agent-core-athdr2m0.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-x8ebdrtn.js → roy-agent-core-avyd65je.js} +9 -5
- package/dist/shared/@ai-setting/{roy-agent-core-223v204d.js → roy-agent-core-cg9f4zch.js} +69 -4
- package/dist/shared/@ai-setting/{roy-agent-core-bt6h398s.js → roy-agent-core-dsevjbck.js} +20 -7
- package/dist/shared/@ai-setting/{roy-agent-core-5hjtbvxf.js → roy-agent-core-dv714bqh.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-k0mx0tkh.js → roy-agent-core-dwx9hjwx.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-zc9k2n3c.js → roy-agent-core-f28va833.js} +4 -4
- package/dist/shared/@ai-setting/roy-agent-core-fs36xgcs.js +13 -0
- package/dist/shared/@ai-setting/{roy-agent-core-ydwtpzyf.js → roy-agent-core-fvs48d38.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-k0sy76cw.js → roy-agent-core-gpf2059p.js} +17 -13
- package/dist/shared/@ai-setting/{roy-agent-core-emjpkx9s.js → roy-agent-core-k8b440n3.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-j783tcqb.js → roy-agent-core-nde5gzta.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-e2569ptp.js → roy-agent-core-ne2t1pay.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-xvf97xs4.js → roy-agent-core-p8ak03s0.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-98wwn2px.js → roy-agent-core-q6twb0df.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-kazn764f.js → roy-agent-core-q9gnnv6v.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-56j2q0w6.js → roy-agent-core-qamktze3.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-khx0h4j2.js → roy-agent-core-qcspfszy.js} +11 -4
- package/dist/shared/@ai-setting/{roy-agent-core-rqyxdrxg.js → roy-agent-core-qm9jda8p.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-yq45m8mm.js → roy-agent-core-rnwr39b0.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-vrj9c5y6.js → roy-agent-core-rpnjzztt.js} +14 -7
- package/dist/shared/@ai-setting/{roy-agent-core-hqtp5m8v.js → roy-agent-core-rpwn590p.js} +13 -13
- package/dist/shared/@ai-setting/{roy-agent-core-ed4tbkyj.js → roy-agent-core-rx16h2m1.js} +7 -3
- package/dist/shared/@ai-setting/{roy-agent-core-5vf48m39.js → roy-agent-core-s7jpkfm1.js} +75 -754
- package/dist/shared/@ai-setting/{roy-agent-core-ecb8am8s.js → roy-agent-core-skzjs973.js} +5 -5
- package/dist/shared/@ai-setting/{roy-agent-core-f51kwn6m.js → roy-agent-core-t37wbysj.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-xn12d221.js → roy-agent-core-tdbv7ff4.js} +5 -3
- package/dist/shared/@ai-setting/{roy-agent-core-yes3mr8n.js → roy-agent-core-tjg71zqp.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-yge38dkn.js → roy-agent-core-vrsxhw9e.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-sea232w4.js → roy-agent-core-vy69rpxy.js} +13 -13
- package/dist/shared/@ai-setting/roy-agent-core-wxsgay02.js +302 -0
- package/dist/shared/@ai-setting/{roy-agent-core-c0mnxxck.js → roy-agent-core-x8n1r7h4.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-m9drr1fd.js → roy-agent-core-yd21n6c7.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-zff8vvs5.js → roy-agent-core-yhsssn86.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-zd077w51.js +1037 -0
- package/dist/shared/@ai-setting/{roy-agent-core-2bbj3jqd.js → roy-agent-core-zd3ddm4k.js} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TracedAs,
|
|
3
|
+
init_decorator
|
|
4
|
+
} from "./roy-agent-core-dsevjbck.js";
|
|
5
|
+
import {
|
|
6
|
+
createLogger,
|
|
7
|
+
init_logger
|
|
8
|
+
} from "./roy-agent-core-8seq65bp.js";
|
|
9
|
+
import {
|
|
10
|
+
__legacyDecorateClassTS
|
|
11
|
+
} from "./roy-agent-core-fs0mn2jk.js";
|
|
12
|
+
|
|
13
|
+
// src/env/task/plugins/lark-card-patch-notifier.ts
|
|
14
|
+
init_logger();
|
|
15
|
+
init_decorator();
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
var logger = createLogger("lark-cli:card-patch-notifier");
|
|
18
|
+
var HEADER_TEMPLATE = {
|
|
19
|
+
running: "blue",
|
|
20
|
+
completed: "green",
|
|
21
|
+
failed: "red",
|
|
22
|
+
stopped: "grey"
|
|
23
|
+
};
|
|
24
|
+
var STATUS_PHRASE = {
|
|
25
|
+
running: "实时同步中",
|
|
26
|
+
completed: "\uD83C\uDF89 全部完成!",
|
|
27
|
+
failed: "\uD83D\uDEA8 任务失败",
|
|
28
|
+
stopped: "⏹️ 已停止"
|
|
29
|
+
};
|
|
30
|
+
var SPINNER_EMOJIS = ["⏳", "\uD83D\uDD04", "\uD83D\uDD03", "⌛", "⏰", "\uD83D\uDD01"];
|
|
31
|
+
var PROGRESS_BAR_CELLS = 20;
|
|
32
|
+
function formatTimestampForDisplay(iso) {
|
|
33
|
+
if (!iso)
|
|
34
|
+
return "";
|
|
35
|
+
const d = new Date(iso);
|
|
36
|
+
if (isNaN(d.getTime())) {
|
|
37
|
+
return iso;
|
|
38
|
+
}
|
|
39
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
40
|
+
timeZone: "Asia/Shanghai",
|
|
41
|
+
hour: "2-digit",
|
|
42
|
+
minute: "2-digit",
|
|
43
|
+
second: "2-digit",
|
|
44
|
+
hour12: false
|
|
45
|
+
}).format(d);
|
|
46
|
+
}
|
|
47
|
+
function sanitizeInlineText(text, maxLen = 300) {
|
|
48
|
+
if (!text)
|
|
49
|
+
return "";
|
|
50
|
+
const flat = text.replace(/\r\n?/g, `
|
|
51
|
+
`).replace(/^#{1,6}\s+/gm, "").replace(/^\s*[-*+]\s+/gm, "· ").replace(/^\s*\d+\.\s+/gm, "· ").replace(/\*\*/g, "").replace(/`/g, "").replace(/\n+/g, " · ").replace(/\s{2,}/g, " ").replace(/(?:\s*·\s*){2,}/g, " · ").replace(/^(\s*·\s*)+/, "").replace(/(\s*·\s*)+$/, "").trim();
|
|
52
|
+
return flat.length <= maxLen ? flat : `${flat.slice(0, maxLen)}…`;
|
|
53
|
+
}
|
|
54
|
+
function formatDateTimeForDisplay(value) {
|
|
55
|
+
const d = new Date(value);
|
|
56
|
+
if (isNaN(d.getTime()))
|
|
57
|
+
return value;
|
|
58
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
59
|
+
timeZone: "Asia/Shanghai",
|
|
60
|
+
year: "numeric",
|
|
61
|
+
month: "2-digit",
|
|
62
|
+
day: "2-digit",
|
|
63
|
+
hour: "2-digit",
|
|
64
|
+
minute: "2-digit",
|
|
65
|
+
hour12: false
|
|
66
|
+
}).formatToParts(d);
|
|
67
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
68
|
+
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
|
|
69
|
+
}
|
|
70
|
+
function looksLikeIsoDateTime(v) {
|
|
71
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(v);
|
|
72
|
+
}
|
|
73
|
+
function renderProgressBar(progress) {
|
|
74
|
+
const clamped = Math.max(0, Math.min(100, progress));
|
|
75
|
+
const filled = Math.round(clamped / 100 * PROGRESS_BAR_CELLS);
|
|
76
|
+
const bar = "▓".repeat(filled) + "░".repeat(PROGRESS_BAR_CELLS - filled);
|
|
77
|
+
return `${bar} ${Math.round(clamped)}%`;
|
|
78
|
+
}
|
|
79
|
+
function humanizeValue(value, maxLen = 200) {
|
|
80
|
+
const truncate = (t) => t.length <= maxLen ? t : `${t.slice(0, maxLen)}…`;
|
|
81
|
+
if (value === null || value === undefined)
|
|
82
|
+
return "";
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
if (looksLikeIsoDateTime(value))
|
|
85
|
+
return formatDateTimeForDisplay(value);
|
|
86
|
+
return truncate(sanitizeInlineText(value, maxLen));
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
89
|
+
return String(value);
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
return truncate(value.map((v) => humanizeValue(v, 80)).filter((t) => t.length > 0).join("、"));
|
|
93
|
+
}
|
|
94
|
+
if (typeof value === "object") {
|
|
95
|
+
const entries = Object.entries(value).map(([k, v]) => {
|
|
96
|
+
const rendered = humanizeValue(v, 80);
|
|
97
|
+
return rendered ? `${k}: ${rendered}` : "";
|
|
98
|
+
}).filter((t) => t.length > 0);
|
|
99
|
+
return truncate(entries.join(" · "));
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
var CONTEXT_LABELS = {
|
|
104
|
+
worktree_path: "\uD83C\uDF3F Worktree",
|
|
105
|
+
branch: "\uD83C\uDF3F 分支",
|
|
106
|
+
created_at: "\uD83D\uDD50 创建时间",
|
|
107
|
+
prior_context: "\uD83D\uDCDC 背景",
|
|
108
|
+
project_path: "\uD83D\uDCC2 项目",
|
|
109
|
+
type: "\uD83C\uDFF7️ 类型",
|
|
110
|
+
feature: "\uD83C\uDFF7️ 特性",
|
|
111
|
+
goal: "\uD83C\uDFAF 目标",
|
|
112
|
+
feishu_chat_id: "\uD83D\uDCAC 飞书群",
|
|
113
|
+
trigger_message_id: "✉️ 触发消息",
|
|
114
|
+
trigger_sender_id: "\uD83D\uDC64 触发人",
|
|
115
|
+
related_task: "\uD83D\uDD17 关联任务",
|
|
116
|
+
repo: "\uD83D\uDCE6 仓库",
|
|
117
|
+
commit: "\uD83D\uDD16 Commit",
|
|
118
|
+
session_id: "\uD83E\uDDF5 Session",
|
|
119
|
+
worktree: "\uD83C\uDF3F Worktree",
|
|
120
|
+
larkPerTaskChatId: "\uD83D\uDDC2️ 任务群聊",
|
|
121
|
+
larkPerTaskChatProfile: "\uD83D\uDDC2️ 群聊 Profile"
|
|
122
|
+
};
|
|
123
|
+
var CONTEXT_SKIP_KEYS = new Set([
|
|
124
|
+
"larkPerTaskChatTaskId",
|
|
125
|
+
"larkPerTaskChatCreatedAt"
|
|
126
|
+
]);
|
|
127
|
+
function renderTaskInfoLines(meta) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
if (meta.id !== undefined)
|
|
130
|
+
lines.push(`- **\uD83C\uDD94 任务 ID**:#${meta.id}`);
|
|
131
|
+
if (meta.title) {
|
|
132
|
+
lines.push(`- **\uD83D\uDCCC 标题**:${sanitizeInlineText(meta.title, 120)}`);
|
|
133
|
+
}
|
|
134
|
+
if (meta.description) {
|
|
135
|
+
lines.push(`- **\uD83D\uDCDD 描述**:${sanitizeInlineText(meta.description, 300)}`);
|
|
136
|
+
}
|
|
137
|
+
if (meta.goals) {
|
|
138
|
+
lines.push(`- **\uD83C\uDFAF 目标**:${sanitizeInlineText(meta.goals, 300)}`);
|
|
139
|
+
}
|
|
140
|
+
if (meta.parentTaskId)
|
|
141
|
+
lines.push(`- **\uD83D\uDD17 父任务**:#${meta.parentTaskId}`);
|
|
142
|
+
if (meta.priority || meta.type) {
|
|
143
|
+
lines.push(`- **⚡ 优先级 / 类型**:${meta.priority ?? "—"} / ${meta.type ?? "—"}`);
|
|
144
|
+
}
|
|
145
|
+
if (meta.currentStatus) {
|
|
146
|
+
lines.push(`- **\uD83D\uDCF6 当前状态**:${sanitizeInlineText(meta.currentStatus, 200)}`);
|
|
147
|
+
}
|
|
148
|
+
const project = realValue(meta.projectPath);
|
|
149
|
+
if (project)
|
|
150
|
+
lines.push(`- **\uD83D\uDCC2 项目**:\`${sanitizeInlineText(project, 160)}\``);
|
|
151
|
+
const contextLines = renderContextAsLines(meta.context);
|
|
152
|
+
lines.push(...contextLines);
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
function realValue(v) {
|
|
156
|
+
if (!v)
|
|
157
|
+
return;
|
|
158
|
+
const t = v.trim();
|
|
159
|
+
if (t.length === 0 || t === "unknown" || t === "undefined" || t === "null") {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
return t;
|
|
163
|
+
}
|
|
164
|
+
function renderContextAsLines(context) {
|
|
165
|
+
const raw = realValue(context);
|
|
166
|
+
if (!raw)
|
|
167
|
+
return [];
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(raw);
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
175
|
+
return [];
|
|
176
|
+
const obj = parsed;
|
|
177
|
+
const lines = [];
|
|
178
|
+
const knownKeys = Object.keys(obj).filter((k) => CONTEXT_LABELS[k] && !CONTEXT_SKIP_KEYS.has(k));
|
|
179
|
+
const otherKeys = Object.keys(obj).filter((k) => !CONTEXT_LABELS[k] && !CONTEXT_SKIP_KEYS.has(k));
|
|
180
|
+
for (const k of knownKeys) {
|
|
181
|
+
const rendered = humanizeValue(obj[k], 160);
|
|
182
|
+
if (rendered)
|
|
183
|
+
lines.push(`- **${CONTEXT_LABELS[k]}**:${rendered}`);
|
|
184
|
+
}
|
|
185
|
+
for (const k of otherKeys) {
|
|
186
|
+
const rendered = humanizeValue(obj[k], 160);
|
|
187
|
+
if (rendered)
|
|
188
|
+
lines.push(`- **${k}**:${rendered}`);
|
|
189
|
+
}
|
|
190
|
+
return lines;
|
|
191
|
+
}
|
|
192
|
+
function renderMilestones(milestones) {
|
|
193
|
+
if (milestones.length === 0) {
|
|
194
|
+
return "_(暂无里程碑)_";
|
|
195
|
+
}
|
|
196
|
+
return milestones.map((m) => {
|
|
197
|
+
const ts = formatTimestampForDisplay(m.timestamp) || "—";
|
|
198
|
+
const head = `- **${ts}** ${sanitizeInlineText(m.title, 120)}`;
|
|
199
|
+
return m.description ? `${head}
|
|
200
|
+
- ${sanitizeInlineText(m.description, 300)}` : head;
|
|
201
|
+
}).join(`
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
function renderProcessSteps(steps) {
|
|
205
|
+
if (steps.length === 0) {
|
|
206
|
+
return "_(暂无过程记录)_";
|
|
207
|
+
}
|
|
208
|
+
return steps.map((s) => {
|
|
209
|
+
const startTime = formatTimestampForDisplay(s.timestamp);
|
|
210
|
+
const endTime = formatTimestampForDisplay(s.endTimestamp) || "现在";
|
|
211
|
+
const range = startTime ? `${startTime} → ${endTime}` : endTime;
|
|
212
|
+
const title = s.category ? `${s.title}(${s.category})` : s.title;
|
|
213
|
+
const head = `- **${range}** ${sanitizeInlineText(title, 120)}`;
|
|
214
|
+
return s.description ? `${head}
|
|
215
|
+
- ${sanitizeInlineText(s.description, 300)}` : head;
|
|
216
|
+
}).join(`
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
219
|
+
function milestoneKey(m) {
|
|
220
|
+
return `${m.title}|${m.timestamp ?? ""}|${m.description ?? ""}`;
|
|
221
|
+
}
|
|
222
|
+
function processStepKey(s) {
|
|
223
|
+
return `${s.title}|${s.timestamp ?? ""}|${s.endTimestamp ?? ""}|${s.description ?? ""}`;
|
|
224
|
+
}
|
|
225
|
+
function buildTaskCard(opts) {
|
|
226
|
+
const status = opts.status ?? "running";
|
|
227
|
+
const milestones = opts.milestones ?? [];
|
|
228
|
+
const processSteps = opts.processSteps ?? [];
|
|
229
|
+
const updateCount = opts.updateCount ?? 0;
|
|
230
|
+
const spinner = opts.spinner ?? "";
|
|
231
|
+
const elements = [];
|
|
232
|
+
const infoLines = renderTaskInfoLines(opts.meta);
|
|
233
|
+
if (infoLines.length > 0) {
|
|
234
|
+
elements.push({ tag: "markdown", content: infoLines.join(`
|
|
235
|
+
`) });
|
|
236
|
+
elements.push({ tag: "hr" });
|
|
237
|
+
}
|
|
238
|
+
elements.push({ tag: "markdown", content: `## \uD83C\uDFAF 里程碑序列` });
|
|
239
|
+
elements.push({ tag: "markdown", content: renderMilestones(milestones) });
|
|
240
|
+
elements.push({ tag: "hr" });
|
|
241
|
+
elements.push({ tag: "markdown", content: `## \uD83D\uDD27 过程信息` });
|
|
242
|
+
elements.push({ tag: "markdown", content: renderProcessSteps(processSteps) });
|
|
243
|
+
elements.push({ tag: "hr" });
|
|
244
|
+
const effectiveProgress = status === "completed" ? 100 : opts.progress;
|
|
245
|
+
elements.push({
|
|
246
|
+
tag: "markdown",
|
|
247
|
+
content: effectiveProgress !== undefined ? `**\uD83D\uDCCA 进度**:${renderProgressBar(effectiveProgress)}` : `**\uD83D\uDCCA 进度**:进行中(第 ${updateCount} 次更新,尚未上报百分比)`
|
|
248
|
+
});
|
|
249
|
+
const taskLabel = opts.meta.id !== undefined ? `#${opts.meta.id}` : "任务";
|
|
250
|
+
return {
|
|
251
|
+
schema: "2.0",
|
|
252
|
+
header: {
|
|
253
|
+
title: { tag: "plain_text", content: `\uD83D\uDCCA Task 实时更新(${taskLabel})` },
|
|
254
|
+
subtitle: {
|
|
255
|
+
tag: "plain_text",
|
|
256
|
+
content: `spinner: '${spinner}' · 第 ${updateCount} 次更新 | ` + STATUS_PHRASE[status]
|
|
257
|
+
},
|
|
258
|
+
template: HEADER_TEMPLATE[status]
|
|
259
|
+
},
|
|
260
|
+
body: { elements }
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function buildChatAppLink(chatId, base = DEFAULT_APP_LINK_BASE) {
|
|
264
|
+
return `${base}/client/chat/open?openChatId=${chatId}`;
|
|
265
|
+
}
|
|
266
|
+
var DEFAULT_APP_LINK_BASE = "https://applink.feishu.cn";
|
|
267
|
+
function buildRedirectCard(opts) {
|
|
268
|
+
const taskLabel = opts.taskId !== undefined ? `#${opts.taskId}` : "本任务";
|
|
269
|
+
const title = opts.taskTitle ? ` ${sanitizeInlineText(opts.taskTitle, 120)}` : "";
|
|
270
|
+
const link = buildChatAppLink(opts.targetChatId);
|
|
271
|
+
const groupName = opts.targetChatName ? sanitizeInlineText(opts.targetChatName, 60) : "专属群";
|
|
272
|
+
return {
|
|
273
|
+
schema: "2.0",
|
|
274
|
+
header: {
|
|
275
|
+
title: { tag: "plain_text", content: "\uD83D\uDCC2 任务卡片已改投专属群" },
|
|
276
|
+
subtitle: { tag: "plain_text", content: "这里不是本任务的群,进展请看专属群" },
|
|
277
|
+
template: "grey"
|
|
278
|
+
},
|
|
279
|
+
body: {
|
|
280
|
+
elements: [
|
|
281
|
+
{
|
|
282
|
+
tag: "markdown",
|
|
283
|
+
content: [
|
|
284
|
+
`- **任务**:${taskLabel}${title}`,
|
|
285
|
+
`- **专属群**:[${groupName}](${link})`,
|
|
286
|
+
`- **群 ID**:\`${opts.targetChatId}\`(搜群名也能进)`,
|
|
287
|
+
opts.reason ? `- **原因**:${sanitizeInlineText(opts.reason, 280)}` : ""
|
|
288
|
+
].filter(Boolean).join(`
|
|
289
|
+
`)
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
var WORKFLOW_STATUS_LABEL = {
|
|
296
|
+
running: "\uD83D\uDD04 运行中",
|
|
297
|
+
completed: "✅ 已完成",
|
|
298
|
+
failed: "\uD83D\uDEA8 已失败",
|
|
299
|
+
stopped: "⏹️ 已停止"
|
|
300
|
+
};
|
|
301
|
+
var WORKFLOW_STATUS_PHRASE = {
|
|
302
|
+
running: "实时同步中",
|
|
303
|
+
completed: "\uD83C\uDF89 流程跑完!",
|
|
304
|
+
failed: "\uD83D\uDEA8 流程失败",
|
|
305
|
+
stopped: "⏹️ 流程已停止"
|
|
306
|
+
};
|
|
307
|
+
function formatDurationMs(ms) {
|
|
308
|
+
if (ms === undefined || ms === null || Number.isNaN(ms))
|
|
309
|
+
return "";
|
|
310
|
+
if (ms < 1000)
|
|
311
|
+
return `${Math.round(ms)}ms`;
|
|
312
|
+
if (ms < 60000) {
|
|
313
|
+
const s = ms / 1000;
|
|
314
|
+
return Number.isInteger(s) ? `${s}s` : `${s.toFixed(1)}s`;
|
|
315
|
+
}
|
|
316
|
+
const m = ms / 60000;
|
|
317
|
+
return Number.isInteger(m) ? `${m}m` : `${m.toFixed(1)}m`;
|
|
318
|
+
}
|
|
319
|
+
function deriveProgress(opts) {
|
|
320
|
+
if (opts.progress !== undefined)
|
|
321
|
+
return opts.progress;
|
|
322
|
+
if (typeof opts.completedNodes === "number" && typeof opts.totalNodes === "number" && opts.totalNodes > 0) {
|
|
323
|
+
return opts.completedNodes / opts.totalNodes * 100;
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
function renderWorkflowInfoLines(meta, extra) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
if (meta.workflowName) {
|
|
330
|
+
lines.push(`- **\uD83D\uDD04 工作流**:${sanitizeInlineText(meta.workflowName, 120)}`);
|
|
331
|
+
}
|
|
332
|
+
if (meta.runId) {
|
|
333
|
+
lines.push(`- **\uD83C\uDD94 Run ID**:\`${sanitizeInlineText(meta.runId, 120)}\``);
|
|
334
|
+
}
|
|
335
|
+
if (meta.workflowId) {
|
|
336
|
+
lines.push(`- **\uD83C\uDFF7️ Workflow ID**:\`${sanitizeInlineText(meta.workflowId, 120)}\``);
|
|
337
|
+
}
|
|
338
|
+
lines.push(`- **\uD83D\uDCF6 状态**:${WORKFLOW_STATUS_LABEL[extra.status]}`);
|
|
339
|
+
if (typeof extra.totalNodes === "number" && extra.totalNodes > 0) {
|
|
340
|
+
const done = typeof extra.completedNodes === "number" ? extra.completedNodes : 0;
|
|
341
|
+
lines.push(`- **\uD83E\uDDE9 节点进度**:${done}/${extra.totalNodes}`);
|
|
342
|
+
}
|
|
343
|
+
if (extra.currentNode) {
|
|
344
|
+
const type = extra.currentNodeType ? `(${extra.currentNodeType})` : "";
|
|
345
|
+
lines.push(`- **\uD83E\uDDED 当前节点**:${sanitizeInlineText(extra.currentNode, 80)}${type}`);
|
|
346
|
+
}
|
|
347
|
+
const duration = formatDurationMs(extra.durationMs);
|
|
348
|
+
if (duration) {
|
|
349
|
+
lines.push(`- **⚡ 耗时**:${duration}`);
|
|
350
|
+
}
|
|
351
|
+
if (extra.error) {
|
|
352
|
+
const label = extra.status === "stopped" ? "⏹️ 停止原因" : "\uD83D\uDEA8 失败原因";
|
|
353
|
+
lines.push(`- **${label}**:${sanitizeInlineText(extra.error, 300)}`);
|
|
354
|
+
}
|
|
355
|
+
if (meta.input !== undefined && meta.input !== null) {
|
|
356
|
+
const rendered = humanizeValue(meta.input, 200);
|
|
357
|
+
if (rendered) {
|
|
358
|
+
lines.push(`- **\uD83D\uDCE5 触发输入**:${rendered}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return lines;
|
|
362
|
+
}
|
|
363
|
+
function buildWorkflowCard(opts) {
|
|
364
|
+
const status = opts.status ?? "running";
|
|
365
|
+
const milestones = opts.milestones ?? [];
|
|
366
|
+
const processSteps = opts.processSteps ?? [];
|
|
367
|
+
const updateCount = opts.updateCount ?? 0;
|
|
368
|
+
const spinner = opts.spinner ?? "";
|
|
369
|
+
const elements = [];
|
|
370
|
+
const infoLines = renderWorkflowInfoLines(opts.meta, {
|
|
371
|
+
status,
|
|
372
|
+
totalNodes: opts.totalNodes,
|
|
373
|
+
completedNodes: opts.completedNodes,
|
|
374
|
+
currentNode: opts.currentNode,
|
|
375
|
+
currentNodeType: opts.currentNodeType,
|
|
376
|
+
durationMs: opts.durationMs,
|
|
377
|
+
error: opts.error
|
|
378
|
+
});
|
|
379
|
+
if (infoLines.length > 0) {
|
|
380
|
+
elements.push({ tag: "markdown", content: infoLines.join(`
|
|
381
|
+
`) });
|
|
382
|
+
elements.push({ tag: "hr" });
|
|
383
|
+
}
|
|
384
|
+
elements.push({ tag: "markdown", content: `## \uD83C\uDFAF 里程碑序列` });
|
|
385
|
+
elements.push({ tag: "markdown", content: renderMilestones(milestones) });
|
|
386
|
+
elements.push({ tag: "hr" });
|
|
387
|
+
elements.push({ tag: "markdown", content: `## \uD83D\uDD27 过程信息` });
|
|
388
|
+
elements.push({ tag: "markdown", content: renderProcessSteps(processSteps) });
|
|
389
|
+
const effectiveProgress = status === "completed" ? 100 : deriveProgress({
|
|
390
|
+
completedNodes: opts.completedNodes,
|
|
391
|
+
totalNodes: opts.totalNodes,
|
|
392
|
+
progress: opts.progress
|
|
393
|
+
});
|
|
394
|
+
elements.push({ tag: "hr" });
|
|
395
|
+
elements.push({
|
|
396
|
+
tag: "markdown",
|
|
397
|
+
content: effectiveProgress !== undefined ? `**\uD83D\uDCCA 进度**:${renderProgressBar(effectiveProgress)}` : `**\uD83D\uDCCA 进度**:进行中(第 ${updateCount} 次更新,尚未上报百分比)`
|
|
398
|
+
});
|
|
399
|
+
const title = opts.meta.workflowName || opts.meta.runId || "workflow";
|
|
400
|
+
return {
|
|
401
|
+
schema: "2.0",
|
|
402
|
+
header: {
|
|
403
|
+
title: { tag: "plain_text", content: `\uD83D\uDD04 Workflow 实时更新(${sanitizeInlineText(title, 80)})` },
|
|
404
|
+
subtitle: {
|
|
405
|
+
tag: "plain_text",
|
|
406
|
+
content: `spinner: '${spinner}' · 第 ${updateCount} 次更新 | ` + WORKFLOW_STATUS_PHRASE[status]
|
|
407
|
+
},
|
|
408
|
+
template: HEADER_TEMPLATE[status]
|
|
409
|
+
},
|
|
410
|
+
body: { elements }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
class MutexImpl {
|
|
415
|
+
chain = Promise.resolve();
|
|
416
|
+
async run(fn) {
|
|
417
|
+
const prev = this.chain;
|
|
418
|
+
const next = prev.then(() => fn());
|
|
419
|
+
this.chain = next.catch(() => {});
|
|
420
|
+
return next;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
class LarkCardPatchNotifier {
|
|
425
|
+
options;
|
|
426
|
+
mutexes = new Map;
|
|
427
|
+
states = new Map;
|
|
428
|
+
workflowStates = new Map;
|
|
429
|
+
redirectsSent = new Set;
|
|
430
|
+
constructor(options = {}) {
|
|
431
|
+
this.options = {
|
|
432
|
+
pinOnStart: true,
|
|
433
|
+
unpinOnFinish: true,
|
|
434
|
+
...options
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
mutexFor(messageId) {
|
|
438
|
+
let m = this.mutexes.get(messageId);
|
|
439
|
+
if (!m) {
|
|
440
|
+
m = new MutexImpl;
|
|
441
|
+
this.mutexes.set(messageId, m);
|
|
442
|
+
}
|
|
443
|
+
return m;
|
|
444
|
+
}
|
|
445
|
+
async startTask(opts) {
|
|
446
|
+
const now = new Date().toISOString();
|
|
447
|
+
const spinner = SPINNER_EMOJIS[0];
|
|
448
|
+
const milestones = opts.milestones?.length ? opts.milestones : [{ title: "Task 创建", timestamp: now }];
|
|
449
|
+
const card = buildTaskCard({
|
|
450
|
+
status: "running",
|
|
451
|
+
meta: opts.meta,
|
|
452
|
+
milestones,
|
|
453
|
+
processSteps: [],
|
|
454
|
+
updateCount: 1,
|
|
455
|
+
spinner
|
|
456
|
+
});
|
|
457
|
+
const stdout = await this.runLarkCli(this.buildSendCardArgs({ chatId: opts.chatId, profile: opts.profile, card }), "send");
|
|
458
|
+
const messageId = this.extractMessageId(stdout);
|
|
459
|
+
if (!messageId) {
|
|
460
|
+
throw new Error(`lark-cli +messages-send did not return message_id: ${stdout.slice(0, 500)}`);
|
|
461
|
+
}
|
|
462
|
+
this.states.set(messageId, {
|
|
463
|
+
chatId: opts.chatId,
|
|
464
|
+
profile: opts.profile,
|
|
465
|
+
meta: opts.meta,
|
|
466
|
+
startedAt: now,
|
|
467
|
+
updateCount: 1,
|
|
468
|
+
spinnerIndex: 0,
|
|
469
|
+
milestones,
|
|
470
|
+
processSteps: []
|
|
471
|
+
});
|
|
472
|
+
let pinned = false;
|
|
473
|
+
let pinError;
|
|
474
|
+
if (this.options.pinOnStart) {
|
|
475
|
+
try {
|
|
476
|
+
await this.runLarkCli(this.buildPinArgs({ messageId, profile: opts.profile }), "pin");
|
|
477
|
+
pinned = true;
|
|
478
|
+
} catch (pinErr) {
|
|
479
|
+
pinError = pinErr.message;
|
|
480
|
+
logger.warn(`Pin failed (non-fatal) for message ${messageId}: ${pinError}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { messageId, pinned, pinError };
|
|
484
|
+
}
|
|
485
|
+
async sendRedirectNotice(opts) {
|
|
486
|
+
const redirectKey = opts.taskId !== undefined ? `${opts.taskId}:${opts.targetChatId}` : undefined;
|
|
487
|
+
if (redirectKey) {
|
|
488
|
+
if (this.redirectsSent.has(redirectKey)) {
|
|
489
|
+
logger.debug(`sendRedirectNotice: 已发过改投卡片(${redirectKey})→ 跳过重复发送`);
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
this.redirectsSent.add(redirectKey);
|
|
493
|
+
}
|
|
494
|
+
const card = buildRedirectCard({
|
|
495
|
+
taskId: opts.taskId,
|
|
496
|
+
taskTitle: opts.taskTitle,
|
|
497
|
+
targetChatId: opts.targetChatId,
|
|
498
|
+
targetChatName: opts.targetChatName,
|
|
499
|
+
reason: opts.reason
|
|
500
|
+
});
|
|
501
|
+
try {
|
|
502
|
+
await this.runLarkCli(this.buildSendCardArgs({
|
|
503
|
+
chatId: opts.chatId,
|
|
504
|
+
profile: opts.profile,
|
|
505
|
+
card
|
|
506
|
+
}), "send-redirect");
|
|
507
|
+
return true;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
if (redirectKey)
|
|
510
|
+
this.redirectsSent.delete(redirectKey);
|
|
511
|
+
logger.warn(`sendRedirectNotice failed (non-fatal) for chat ${opts.chatId}: ${err.message}`);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async updateTask(opts) {
|
|
516
|
+
await this.mutexFor(opts.messageId).run(async () => {
|
|
517
|
+
const state = this.states.get(opts.messageId) ?? {
|
|
518
|
+
chatId: opts.chatId ?? "",
|
|
519
|
+
profile: opts.profile,
|
|
520
|
+
meta: opts.meta ?? { title: "任务" },
|
|
521
|
+
startedAt: new Date().toISOString(),
|
|
522
|
+
updateCount: 0,
|
|
523
|
+
spinnerIndex: 0,
|
|
524
|
+
milestones: [],
|
|
525
|
+
processSteps: []
|
|
526
|
+
};
|
|
527
|
+
const mergedMilestones = mergeUnique(state.milestones, opts.milestones ?? [], milestoneKey);
|
|
528
|
+
const mergedSteps = mergeUnique(state.processSteps, opts.processSteps ?? [], processStepKey);
|
|
529
|
+
const updateCount = state.updateCount + 1;
|
|
530
|
+
const spinner = SPINNER_EMOJIS[updateCount % SPINNER_EMOJIS.length];
|
|
531
|
+
const card = buildTaskCard({
|
|
532
|
+
status: opts.status,
|
|
533
|
+
meta: mergeTaskMeta(state.meta, opts.meta),
|
|
534
|
+
progress: opts.progress ?? state.progress,
|
|
535
|
+
milestones: mergedMilestones,
|
|
536
|
+
processSteps: mergedSteps,
|
|
537
|
+
updateCount,
|
|
538
|
+
spinner
|
|
539
|
+
});
|
|
540
|
+
await this.runLarkCli(this.buildPatchCardArgs({
|
|
541
|
+
messageId: opts.messageId,
|
|
542
|
+
profile: opts.profile ?? state.profile,
|
|
543
|
+
card
|
|
544
|
+
}), "patch");
|
|
545
|
+
this.states.set(opts.messageId, {
|
|
546
|
+
...state,
|
|
547
|
+
chatId: opts.chatId ?? state.chatId,
|
|
548
|
+
profile: opts.profile ?? state.profile,
|
|
549
|
+
meta: mergeTaskMeta(state.meta, opts.meta),
|
|
550
|
+
updateCount,
|
|
551
|
+
spinnerIndex: spinnerIndexFor(updateCount),
|
|
552
|
+
milestones: mergedMilestones,
|
|
553
|
+
processSteps: mergedSteps,
|
|
554
|
+
progress: opts.progress ?? state.progress
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async finishTask(opts) {
|
|
559
|
+
await this.mutexFor(opts.messageId).run(async () => {
|
|
560
|
+
const state = this.states.get(opts.messageId);
|
|
561
|
+
const now = new Date().toISOString();
|
|
562
|
+
const updateCount = (state?.updateCount ?? 0) + 1;
|
|
563
|
+
const spinner = SPINNER_EMOJIS[updateCount % SPINNER_EMOJIS.length];
|
|
564
|
+
const finalMilestone = {
|
|
565
|
+
title: opts.status === "completed" ? "Task 完成" : "Task 失败",
|
|
566
|
+
timestamp: now,
|
|
567
|
+
description: opts.finalStep
|
|
568
|
+
};
|
|
569
|
+
const card = buildTaskCard({
|
|
570
|
+
status: opts.status,
|
|
571
|
+
meta: mergeTaskMeta(state?.meta, opts.meta),
|
|
572
|
+
progress: opts.status === "completed" ? 100 : opts.progress ?? state?.progress,
|
|
573
|
+
milestones: mergeUnique(state?.milestones ?? [], [finalMilestone], milestoneKey),
|
|
574
|
+
processSteps: state?.processSteps ?? [],
|
|
575
|
+
updateCount,
|
|
576
|
+
spinner
|
|
577
|
+
});
|
|
578
|
+
await this.runLarkCli(this.buildPatchCardArgs({
|
|
579
|
+
messageId: opts.messageId,
|
|
580
|
+
profile: opts.profile ?? state?.profile,
|
|
581
|
+
card
|
|
582
|
+
}), "patch");
|
|
583
|
+
});
|
|
584
|
+
if (this.options.unpinOnFinish) {
|
|
585
|
+
const state = this.states.get(opts.messageId);
|
|
586
|
+
try {
|
|
587
|
+
await this.runLarkCli(this.buildUnpinArgs({
|
|
588
|
+
messageId: opts.messageId,
|
|
589
|
+
profile: opts.profile ?? state?.profile
|
|
590
|
+
}), "unpin");
|
|
591
|
+
} catch (unpinErr) {
|
|
592
|
+
logger.warn(`Unpin failed (non-fatal) for ${opts.messageId}: ${unpinErr.message}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
this.states.delete(opts.messageId);
|
|
596
|
+
this.mutexes.delete(opts.messageId);
|
|
597
|
+
}
|
|
598
|
+
trackedCardCount() {
|
|
599
|
+
return this.states.size;
|
|
600
|
+
}
|
|
601
|
+
trackedWorkflowCardCount() {
|
|
602
|
+
return this.workflowStates.size;
|
|
603
|
+
}
|
|
604
|
+
async startWorkflow(opts) {
|
|
605
|
+
const now = new Date().toISOString();
|
|
606
|
+
const spinner = SPINNER_EMOJIS[0];
|
|
607
|
+
const milestones = opts.milestones?.length ? opts.milestones : [{ title: "Workflow 启动", timestamp: now }];
|
|
608
|
+
const processSteps = opts.processSteps ?? [];
|
|
609
|
+
const card = buildWorkflowCard({
|
|
610
|
+
status: "running",
|
|
611
|
+
meta: opts.meta,
|
|
612
|
+
milestones,
|
|
613
|
+
processSteps,
|
|
614
|
+
totalNodes: opts.totalNodes,
|
|
615
|
+
completedNodes: opts.completedNodes ?? 0,
|
|
616
|
+
updateCount: 1,
|
|
617
|
+
spinner
|
|
618
|
+
});
|
|
619
|
+
const stdout = await this.runLarkCli(this.buildSendCardArgs({ chatId: opts.chatId, profile: opts.profile, card }), "send");
|
|
620
|
+
const messageId = this.extractMessageId(stdout);
|
|
621
|
+
if (!messageId) {
|
|
622
|
+
throw new Error(`lark-cli +messages-send did not return message_id: ${stdout.slice(0, 500)}`);
|
|
623
|
+
}
|
|
624
|
+
this.workflowStates.set(messageId, {
|
|
625
|
+
chatId: opts.chatId,
|
|
626
|
+
profile: opts.profile,
|
|
627
|
+
meta: opts.meta,
|
|
628
|
+
startedAt: now,
|
|
629
|
+
updateCount: 1,
|
|
630
|
+
spinnerIndex: 0,
|
|
631
|
+
milestones,
|
|
632
|
+
processSteps,
|
|
633
|
+
completedNodes: opts.completedNodes ?? 0,
|
|
634
|
+
totalNodes: opts.totalNodes,
|
|
635
|
+
status: "running"
|
|
636
|
+
});
|
|
637
|
+
let pinned = false;
|
|
638
|
+
let pinError;
|
|
639
|
+
if (this.options.pinOnStart) {
|
|
640
|
+
try {
|
|
641
|
+
await this.runLarkCli(this.buildPinArgs({ messageId, profile: opts.profile }), "pin");
|
|
642
|
+
pinned = true;
|
|
643
|
+
} catch (pinErr) {
|
|
644
|
+
pinError = pinErr.message;
|
|
645
|
+
logger.warn(`Pin failed (non-fatal) for workflow message ${messageId}: ${pinError}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return { messageId, pinned, pinError };
|
|
649
|
+
}
|
|
650
|
+
async updateWorkflow(opts) {
|
|
651
|
+
await this.mutexFor(opts.messageId).run(async () => {
|
|
652
|
+
const state = this.workflowStates.get(opts.messageId) ?? {
|
|
653
|
+
chatId: opts.chatId ?? "",
|
|
654
|
+
profile: opts.profile,
|
|
655
|
+
meta: opts.meta ?? { runId: opts.messageId },
|
|
656
|
+
startedAt: new Date().toISOString(),
|
|
657
|
+
updateCount: 0,
|
|
658
|
+
spinnerIndex: 0,
|
|
659
|
+
milestones: [],
|
|
660
|
+
processSteps: [],
|
|
661
|
+
status: "running"
|
|
662
|
+
};
|
|
663
|
+
const mergedMilestones = mergeUnique(state.milestones, opts.milestones ?? [], milestoneKey);
|
|
664
|
+
const mergedSteps = mergeUnique(state.processSteps, opts.processSteps ?? [], processStepKey);
|
|
665
|
+
const updateCount = state.updateCount + 1;
|
|
666
|
+
const spinner = SPINNER_EMOJIS[updateCount % SPINNER_EMOJIS.length];
|
|
667
|
+
const status = opts.status ?? state.status ?? "running";
|
|
668
|
+
const completedNodes = opts.completedNodes ?? state.completedNodes;
|
|
669
|
+
const totalNodes = opts.totalNodes ?? state.totalNodes;
|
|
670
|
+
const card = buildWorkflowCard({
|
|
671
|
+
status,
|
|
672
|
+
meta: mergeWorkflowMeta(state.meta, opts.meta),
|
|
673
|
+
progress: opts.progress ?? state.progress,
|
|
674
|
+
completedNodes,
|
|
675
|
+
totalNodes,
|
|
676
|
+
currentNode: opts.currentNode,
|
|
677
|
+
currentNodeType: opts.currentNodeType,
|
|
678
|
+
durationMs: opts.durationMs ?? state.durationMs,
|
|
679
|
+
error: opts.error ?? state.error,
|
|
680
|
+
milestones: mergedMilestones,
|
|
681
|
+
processSteps: mergedSteps,
|
|
682
|
+
updateCount,
|
|
683
|
+
spinner
|
|
684
|
+
});
|
|
685
|
+
await this.runLarkCli(this.buildPatchCardArgs({
|
|
686
|
+
messageId: opts.messageId,
|
|
687
|
+
profile: opts.profile ?? state.profile,
|
|
688
|
+
card
|
|
689
|
+
}), "patch");
|
|
690
|
+
this.workflowStates.set(opts.messageId, {
|
|
691
|
+
...state,
|
|
692
|
+
chatId: opts.chatId ?? state.chatId,
|
|
693
|
+
profile: opts.profile ?? state.profile,
|
|
694
|
+
meta: mergeWorkflowMeta(state.meta, opts.meta),
|
|
695
|
+
updateCount,
|
|
696
|
+
spinnerIndex: spinnerIndexFor(updateCount),
|
|
697
|
+
milestones: mergedMilestones,
|
|
698
|
+
processSteps: mergedSteps,
|
|
699
|
+
progress: opts.progress ?? state.progress,
|
|
700
|
+
completedNodes,
|
|
701
|
+
totalNodes,
|
|
702
|
+
durationMs: opts.durationMs ?? state.durationMs,
|
|
703
|
+
error: opts.error ?? state.error,
|
|
704
|
+
status
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
async finishWorkflow(opts) {
|
|
709
|
+
await this.mutexFor(opts.messageId).run(async () => {
|
|
710
|
+
const state = this.workflowStates.get(opts.messageId);
|
|
711
|
+
const now = new Date().toISOString();
|
|
712
|
+
const updateCount = (state?.updateCount ?? 0) + 1;
|
|
713
|
+
const spinner = SPINNER_EMOJIS[updateCount % SPINNER_EMOJIS.length];
|
|
714
|
+
const titleByStatus = {
|
|
715
|
+
completed: "Workflow 完成",
|
|
716
|
+
failed: "Workflow 失败",
|
|
717
|
+
stopped: "Workflow 停止"
|
|
718
|
+
};
|
|
719
|
+
const finalMilestone = {
|
|
720
|
+
title: titleByStatus[opts.status],
|
|
721
|
+
timestamp: now,
|
|
722
|
+
description: opts.finalStep
|
|
723
|
+
};
|
|
724
|
+
const completedNodes = opts.completedNodes ?? state?.completedNodes;
|
|
725
|
+
const totalNodes = opts.totalNodes ?? state?.totalNodes;
|
|
726
|
+
const card = buildWorkflowCard({
|
|
727
|
+
status: opts.status,
|
|
728
|
+
meta: mergeWorkflowMeta(state?.meta, opts.meta),
|
|
729
|
+
progress: opts.status === "completed" ? 100 : opts.progress ?? state?.progress,
|
|
730
|
+
completedNodes,
|
|
731
|
+
totalNodes,
|
|
732
|
+
durationMs: opts.durationMs ?? state?.durationMs,
|
|
733
|
+
error: opts.error ?? state?.error,
|
|
734
|
+
milestones: mergeUnique(state?.milestones ?? [], [finalMilestone], milestoneKey),
|
|
735
|
+
processSteps: state?.processSteps ?? [],
|
|
736
|
+
updateCount,
|
|
737
|
+
spinner
|
|
738
|
+
});
|
|
739
|
+
await this.runLarkCli(this.buildPatchCardArgs({
|
|
740
|
+
messageId: opts.messageId,
|
|
741
|
+
profile: opts.profile ?? state?.profile,
|
|
742
|
+
card
|
|
743
|
+
}), "patch");
|
|
744
|
+
});
|
|
745
|
+
if (this.options.unpinOnFinish) {
|
|
746
|
+
const state = this.workflowStates.get(opts.messageId);
|
|
747
|
+
try {
|
|
748
|
+
await this.runLarkCli(this.buildUnpinArgs({
|
|
749
|
+
messageId: opts.messageId,
|
|
750
|
+
profile: opts.profile ?? state?.profile
|
|
751
|
+
}), "unpin");
|
|
752
|
+
} catch (unpinErr) {
|
|
753
|
+
logger.warn(`Unpin failed (non-fatal) for workflow ${opts.messageId}: ${unpinErr.message}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
this.workflowStates.delete(opts.messageId);
|
|
757
|
+
this.mutexes.delete(opts.messageId);
|
|
758
|
+
}
|
|
759
|
+
buildSendCardArgs(opts) {
|
|
760
|
+
const args = [
|
|
761
|
+
"im",
|
|
762
|
+
"+messages-send",
|
|
763
|
+
"--chat-id",
|
|
764
|
+
opts.chatId,
|
|
765
|
+
"--msg-type",
|
|
766
|
+
"interactive",
|
|
767
|
+
"--content",
|
|
768
|
+
JSON.stringify(opts.card),
|
|
769
|
+
"--as",
|
|
770
|
+
"bot"
|
|
771
|
+
];
|
|
772
|
+
if (opts.profile) {
|
|
773
|
+
args.unshift("--profile", opts.profile);
|
|
774
|
+
}
|
|
775
|
+
return args;
|
|
776
|
+
}
|
|
777
|
+
buildPatchCardArgs(opts) {
|
|
778
|
+
const data = JSON.stringify({ content: JSON.stringify(opts.card) });
|
|
779
|
+
const args = [
|
|
780
|
+
"im",
|
|
781
|
+
"messages",
|
|
782
|
+
"patch",
|
|
783
|
+
"--message-id",
|
|
784
|
+
opts.messageId,
|
|
785
|
+
"--data",
|
|
786
|
+
data,
|
|
787
|
+
"--as",
|
|
788
|
+
"bot"
|
|
789
|
+
];
|
|
790
|
+
if (opts.profile) {
|
|
791
|
+
args.unshift("--profile", opts.profile);
|
|
792
|
+
}
|
|
793
|
+
return args;
|
|
794
|
+
}
|
|
795
|
+
buildPinArgs(opts) {
|
|
796
|
+
const args = [
|
|
797
|
+
"im",
|
|
798
|
+
"pins",
|
|
799
|
+
"create",
|
|
800
|
+
"--data",
|
|
801
|
+
JSON.stringify({ message_id: opts.messageId }),
|
|
802
|
+
"--as",
|
|
803
|
+
"bot"
|
|
804
|
+
];
|
|
805
|
+
if (opts.profile) {
|
|
806
|
+
args.unshift("--profile", opts.profile);
|
|
807
|
+
}
|
|
808
|
+
return args;
|
|
809
|
+
}
|
|
810
|
+
buildUnpinArgs(opts) {
|
|
811
|
+
const args = [
|
|
812
|
+
"im",
|
|
813
|
+
"pins",
|
|
814
|
+
"delete",
|
|
815
|
+
"--message-id",
|
|
816
|
+
opts.messageId,
|
|
817
|
+
"--yes",
|
|
818
|
+
"--as",
|
|
819
|
+
"bot"
|
|
820
|
+
];
|
|
821
|
+
if (opts.profile) {
|
|
822
|
+
args.unshift("--profile", opts.profile);
|
|
823
|
+
}
|
|
824
|
+
return args;
|
|
825
|
+
}
|
|
826
|
+
runLarkCli(argv, op) {
|
|
827
|
+
return new Promise((resolve, reject) => {
|
|
828
|
+
if (process.env.ROY_HOOK_DEBUG === "1") {
|
|
829
|
+
console.error(`[DEBUG LarkCardPatchNotifier.runLarkCli] op=${op} args=${JSON.stringify(argv).slice(0, 300)}`);
|
|
830
|
+
}
|
|
831
|
+
const proc = spawn("lark-cli", argv, {
|
|
832
|
+
shell: false,
|
|
833
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
834
|
+
});
|
|
835
|
+
let stdout = "";
|
|
836
|
+
let stderr = "";
|
|
837
|
+
proc.stdout?.on("data", (data) => {
|
|
838
|
+
stdout += data.toString();
|
|
839
|
+
});
|
|
840
|
+
proc.stderr?.on("data", (data) => {
|
|
841
|
+
stderr += data.toString();
|
|
842
|
+
});
|
|
843
|
+
proc.on("close", (code) => {
|
|
844
|
+
if (process.env.ROY_HOOK_DEBUG === "1") {
|
|
845
|
+
console.error(`[DEBUG LarkCardPatchNotifier.runLarkCli] op=${op} CLOSE code=${code} stdoutLen=${stdout.length} stderrLen=${stderr.length}`);
|
|
846
|
+
}
|
|
847
|
+
if (code === 0) {
|
|
848
|
+
resolve(stdout);
|
|
849
|
+
} else {
|
|
850
|
+
reject(new Error(`lark-cli ${op} exited with code ${code}: ${sanitize(stderr || stdout)}`));
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
proc.on("error", (error) => {
|
|
854
|
+
reject(error);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
extractMessageId(stdout) {
|
|
859
|
+
try {
|
|
860
|
+
const obj = JSON.parse(stdout);
|
|
861
|
+
const data = obj?.data ?? obj;
|
|
862
|
+
const id = data?.message_id ?? data?.messageId ?? obj?.message_id ?? obj?.messageId;
|
|
863
|
+
return typeof id === "string" ? id : undefined;
|
|
864
|
+
} catch {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
__legacyDecorateClassTS([
|
|
870
|
+
TracedAs("lark-card.startTask", {
|
|
871
|
+
recordParams: true,
|
|
872
|
+
recordResult: true,
|
|
873
|
+
flattenParamFilter: true,
|
|
874
|
+
paramFilter: (args) => ({
|
|
875
|
+
chatId: args[0]?.chatId,
|
|
876
|
+
profile: args[0]?.profile,
|
|
877
|
+
taskId: args[0]?.taskId
|
|
878
|
+
}),
|
|
879
|
+
resultFilter: (r) => ({
|
|
880
|
+
messageId: r?.messageId,
|
|
881
|
+
pinned: r?.pinned
|
|
882
|
+
})
|
|
883
|
+
})
|
|
884
|
+
], LarkCardPatchNotifier.prototype, "startTask", null);
|
|
885
|
+
__legacyDecorateClassTS([
|
|
886
|
+
TracedAs("lark-card.updateTask", {
|
|
887
|
+
recordParams: true,
|
|
888
|
+
recordResult: false,
|
|
889
|
+
flattenParamFilter: true,
|
|
890
|
+
paramFilter: (args) => ({
|
|
891
|
+
messageId: args[0]?.messageId,
|
|
892
|
+
profile: args[0]?.profile,
|
|
893
|
+
status: args[0]?.status,
|
|
894
|
+
taskId: args[0]?.taskId,
|
|
895
|
+
progress: args[0]?.progress,
|
|
896
|
+
newMilestones: args[0]?.milestones?.length ?? 0,
|
|
897
|
+
newProcessSteps: args[0]?.processSteps?.length ?? 0
|
|
898
|
+
})
|
|
899
|
+
})
|
|
900
|
+
], LarkCardPatchNotifier.prototype, "updateTask", null);
|
|
901
|
+
__legacyDecorateClassTS([
|
|
902
|
+
TracedAs("lark-card.finishTask", {
|
|
903
|
+
recordParams: true,
|
|
904
|
+
recordResult: false,
|
|
905
|
+
flattenParamFilter: true,
|
|
906
|
+
paramFilter: (args) => ({
|
|
907
|
+
messageId: args[0]?.messageId,
|
|
908
|
+
profile: args[0]?.profile,
|
|
909
|
+
status: args[0]?.status,
|
|
910
|
+
taskId: args[0]?.taskId
|
|
911
|
+
})
|
|
912
|
+
})
|
|
913
|
+
], LarkCardPatchNotifier.prototype, "finishTask", null);
|
|
914
|
+
__legacyDecorateClassTS([
|
|
915
|
+
TracedAs("lark-card.startWorkflow", {
|
|
916
|
+
recordParams: true,
|
|
917
|
+
recordResult: true,
|
|
918
|
+
flattenParamFilter: true,
|
|
919
|
+
paramFilter: (args) => ({
|
|
920
|
+
chatId: args[0]?.chatId,
|
|
921
|
+
profile: args[0]?.profile,
|
|
922
|
+
runId: args[0]?.meta?.runId,
|
|
923
|
+
totalNodes: args[0]?.totalNodes
|
|
924
|
+
}),
|
|
925
|
+
resultFilter: (r) => ({
|
|
926
|
+
messageId: r?.messageId,
|
|
927
|
+
pinned: r?.pinned
|
|
928
|
+
})
|
|
929
|
+
})
|
|
930
|
+
], LarkCardPatchNotifier.prototype, "startWorkflow", null);
|
|
931
|
+
__legacyDecorateClassTS([
|
|
932
|
+
TracedAs("lark-card.updateWorkflow", {
|
|
933
|
+
recordParams: true,
|
|
934
|
+
recordResult: false,
|
|
935
|
+
flattenParamFilter: true,
|
|
936
|
+
paramFilter: (args) => ({
|
|
937
|
+
messageId: args[0]?.messageId,
|
|
938
|
+
profile: args[0]?.profile,
|
|
939
|
+
status: args[0]?.status,
|
|
940
|
+
currentNode: args[0]?.currentNode,
|
|
941
|
+
completedNodes: args[0]?.completedNodes,
|
|
942
|
+
totalNodes: args[0]?.totalNodes,
|
|
943
|
+
newMilestones: args[0]?.milestones?.length ?? 0,
|
|
944
|
+
newProcessSteps: args[0]?.processSteps?.length ?? 0
|
|
945
|
+
})
|
|
946
|
+
})
|
|
947
|
+
], LarkCardPatchNotifier.prototype, "updateWorkflow", null);
|
|
948
|
+
__legacyDecorateClassTS([
|
|
949
|
+
TracedAs("lark-card.finishWorkflow", {
|
|
950
|
+
recordParams: true,
|
|
951
|
+
recordResult: false,
|
|
952
|
+
flattenParamFilter: true,
|
|
953
|
+
paramFilter: (args) => ({
|
|
954
|
+
messageId: args[0]?.messageId,
|
|
955
|
+
profile: args[0]?.profile,
|
|
956
|
+
status: args[0]?.status,
|
|
957
|
+
runId: args[0]?.meta?.runId
|
|
958
|
+
})
|
|
959
|
+
})
|
|
960
|
+
], LarkCardPatchNotifier.prototype, "finishWorkflow", null);
|
|
961
|
+
__legacyDecorateClassTS([
|
|
962
|
+
TracedAs("lark-card.runLarkCli", {
|
|
963
|
+
recordParams: true,
|
|
964
|
+
recordResult: true,
|
|
965
|
+
flattenParamFilter: true,
|
|
966
|
+
paramFilter: (args) => {
|
|
967
|
+
const argv = args[0] ?? [];
|
|
968
|
+
const op = args[1] ?? "unknown";
|
|
969
|
+
return {
|
|
970
|
+
op,
|
|
971
|
+
argv: argv.join(" ").slice(0, 300),
|
|
972
|
+
hasProfile: argv.includes("--profile"),
|
|
973
|
+
msgType: valueAfter(argv, "--msg-type"),
|
|
974
|
+
contentLength: (valueAfter(argv, "--content") ?? "").length,
|
|
975
|
+
dataLength: (valueAfter(argv, "--data") ?? "").length
|
|
976
|
+
};
|
|
977
|
+
},
|
|
978
|
+
resultFilter: (r) => ({
|
|
979
|
+
stdoutLength: (r ?? "").length
|
|
980
|
+
})
|
|
981
|
+
})
|
|
982
|
+
], LarkCardPatchNotifier.prototype, "runLarkCli", null);
|
|
983
|
+
function mergeTaskMeta(base, incoming) {
|
|
984
|
+
const merged = { ...base ?? {} };
|
|
985
|
+
if (!incoming)
|
|
986
|
+
return merged;
|
|
987
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
988
|
+
if (v === undefined || v === null || v === "")
|
|
989
|
+
continue;
|
|
990
|
+
merged[k] = v;
|
|
991
|
+
}
|
|
992
|
+
return merged;
|
|
993
|
+
}
|
|
994
|
+
function mergeWorkflowMeta(base, incoming) {
|
|
995
|
+
const merged = { runId: base?.runId ?? incoming?.runId ?? "" };
|
|
996
|
+
const assign = (src) => {
|
|
997
|
+
if (!src)
|
|
998
|
+
return;
|
|
999
|
+
for (const [k, v] of Object.entries(src)) {
|
|
1000
|
+
if (v === undefined || v === null || v === "")
|
|
1001
|
+
continue;
|
|
1002
|
+
merged[k] = v;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
assign(base);
|
|
1006
|
+
assign(incoming);
|
|
1007
|
+
return merged;
|
|
1008
|
+
}
|
|
1009
|
+
function mergeUnique(base, incoming, keyOf) {
|
|
1010
|
+
const seen = new Set(base.map(keyOf));
|
|
1011
|
+
const merged = [...base];
|
|
1012
|
+
for (const item of incoming) {
|
|
1013
|
+
const k = keyOf(item);
|
|
1014
|
+
if (seen.has(k))
|
|
1015
|
+
continue;
|
|
1016
|
+
seen.add(k);
|
|
1017
|
+
merged.push(item);
|
|
1018
|
+
}
|
|
1019
|
+
return merged;
|
|
1020
|
+
}
|
|
1021
|
+
function spinnerIndexFor(updateCount) {
|
|
1022
|
+
return updateCount % SPINNER_EMOJIS.length;
|
|
1023
|
+
}
|
|
1024
|
+
function valueAfter(argv, flag) {
|
|
1025
|
+
const idx = argv.indexOf(flag);
|
|
1026
|
+
if (idx < 0 || idx + 1 >= argv.length)
|
|
1027
|
+
return;
|
|
1028
|
+
return argv[idx + 1];
|
|
1029
|
+
}
|
|
1030
|
+
function sanitize(text) {
|
|
1031
|
+
const trimmed = text.trim();
|
|
1032
|
+
if (trimmed.length <= 500)
|
|
1033
|
+
return trimmed;
|
|
1034
|
+
return `${trimmed.slice(0, 500)}…`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export { humanizeValue, formatDurationMs, LarkCardPatchNotifier };
|