@alexlikevibe/lark-channel-bridge 0.1.0
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/LICENSE +21 -0
- package/README.md +372 -0
- package/README.zh.md +372 -0
- package/bin/lark-channel-bridge.mjs +2 -0
- package/dist/cli.js +15073 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +604 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
// src/card/tool-render.ts
|
|
2
|
+
var HEADER_SUMMARY_MAX = 80;
|
|
3
|
+
var BODY_FIELD_MAX = 600;
|
|
4
|
+
var OUTPUT_MAX = 1200;
|
|
5
|
+
var BODY_TOTAL_MAX = 2500;
|
|
6
|
+
function toolHeaderText(tool) {
|
|
7
|
+
const icon = tool.status === "done" ? "\u2705" : tool.status === "error" ? "\u274C" : "\u23F3";
|
|
8
|
+
const summary = summarizeInput(tool.name, tool.input);
|
|
9
|
+
return summary ? `${icon} **${tool.name}** \u2014 ${summary}` : `${icon} **${tool.name}**`;
|
|
10
|
+
}
|
|
11
|
+
function toolBodyMd(tool) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
const inputMd = renderInput(tool);
|
|
14
|
+
if (inputMd) parts.push(inputMd);
|
|
15
|
+
if (tool.output) {
|
|
16
|
+
const truncated = truncate(tool.output, OUTPUT_MAX);
|
|
17
|
+
if (tool.status === "error") {
|
|
18
|
+
parts.push(`**Error**
|
|
19
|
+
\`\`\`
|
|
20
|
+
${truncated}
|
|
21
|
+
\`\`\``);
|
|
22
|
+
} else if (tool.name === "Bash") {
|
|
23
|
+
parts.push(renderBashOutput(truncated));
|
|
24
|
+
} else {
|
|
25
|
+
parts.push(`**Output**
|
|
26
|
+
\`\`\`
|
|
27
|
+
${truncated}
|
|
28
|
+
\`\`\``);
|
|
29
|
+
}
|
|
30
|
+
} else if (tool.status === "running") {
|
|
31
|
+
parts.push("_\u8FD0\u884C\u4E2D\u2026_");
|
|
32
|
+
}
|
|
33
|
+
const body = parts.join("\n\n");
|
|
34
|
+
if (body.length <= BODY_TOTAL_MAX) return body;
|
|
35
|
+
return `${body.slice(0, BODY_TOTAL_MAX)}\u2026
|
|
36
|
+
|
|
37
|
+
_\uFF08body \u5DF2\u622A\u65AD,\u5B8C\u6574\u5185\u5BB9\u67E5 \`/doctor\` \u6216\u65E5\u5FD7\uFF09_`;
|
|
38
|
+
}
|
|
39
|
+
function summarizeInput(name, input) {
|
|
40
|
+
if (!input || typeof input !== "object") return "";
|
|
41
|
+
const rec = input;
|
|
42
|
+
const pick = (key, max = HEADER_SUMMARY_MAX) => {
|
|
43
|
+
const v = rec[key];
|
|
44
|
+
if (typeof v !== "string") return "";
|
|
45
|
+
const oneLine = v.replace(/\s+/g, " ").trim();
|
|
46
|
+
return oneLine.length > max ? `${oneLine.slice(0, max)}\u2026` : oneLine;
|
|
47
|
+
};
|
|
48
|
+
switch (name) {
|
|
49
|
+
case "Bash":
|
|
50
|
+
return pick("command");
|
|
51
|
+
case "Read":
|
|
52
|
+
case "Edit":
|
|
53
|
+
case "Write":
|
|
54
|
+
case "NotebookEdit":
|
|
55
|
+
return shortenPath(pick("file_path"));
|
|
56
|
+
case "Grep": {
|
|
57
|
+
const pat = pick("pattern", 40);
|
|
58
|
+
const path = pick("path", 30);
|
|
59
|
+
return path ? `${pat} in ${shortenPath(path)}` : pat;
|
|
60
|
+
}
|
|
61
|
+
case "Glob":
|
|
62
|
+
return pick("pattern");
|
|
63
|
+
case "WebFetch":
|
|
64
|
+
return pick("url");
|
|
65
|
+
case "WebSearch":
|
|
66
|
+
return pick("query", 60);
|
|
67
|
+
case "Agent":
|
|
68
|
+
case "Task":
|
|
69
|
+
return pick("description") || pick("subagent_type");
|
|
70
|
+
default:
|
|
71
|
+
return pick("command") || pick("file_path") || pick("path") || pick("query");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function renderInput(tool) {
|
|
75
|
+
const input = tool.input;
|
|
76
|
+
if (!input || typeof input !== "object") return "";
|
|
77
|
+
const rec = input;
|
|
78
|
+
const str = (k) => typeof rec[k] === "string" ? rec[k] : "";
|
|
79
|
+
switch (tool.name) {
|
|
80
|
+
case "Bash": {
|
|
81
|
+
const cmd = str("command");
|
|
82
|
+
return cmd ? `**Command**
|
|
83
|
+
\`\`\`bash
|
|
84
|
+
${truncate(cmd, BODY_FIELD_MAX)}
|
|
85
|
+
\`\`\`` : "";
|
|
86
|
+
}
|
|
87
|
+
case "Read":
|
|
88
|
+
case "Edit":
|
|
89
|
+
case "Write":
|
|
90
|
+
case "NotebookEdit": {
|
|
91
|
+
const fp = str("file_path");
|
|
92
|
+
return fp ? `**File** \`${fp}\`` : "";
|
|
93
|
+
}
|
|
94
|
+
case "Grep": {
|
|
95
|
+
const lines = [];
|
|
96
|
+
if (str("pattern")) lines.push(`**Pattern** \`${str("pattern")}\``);
|
|
97
|
+
if (str("path")) lines.push(`**Path** \`${str("path")}\``);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
case "WebFetch":
|
|
101
|
+
return str("url") ? `**URL** ${str("url")}` : "";
|
|
102
|
+
case "WebSearch":
|
|
103
|
+
return str("query") ? `**Query** \`${truncate(str("query"), BODY_FIELD_MAX)}\`` : "";
|
|
104
|
+
default:
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function renderBashOutput(out) {
|
|
109
|
+
return `**Output**
|
|
110
|
+
\`\`\`
|
|
111
|
+
${out}
|
|
112
|
+
\`\`\``;
|
|
113
|
+
}
|
|
114
|
+
function shortenPath(p) {
|
|
115
|
+
return p;
|
|
116
|
+
}
|
|
117
|
+
function truncate(s, max) {
|
|
118
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/card/run-renderer.ts
|
|
122
|
+
var REASONING_MAX = 1500;
|
|
123
|
+
var COLLAPSE_TOOL_THRESHOLD = 3;
|
|
124
|
+
function renderCard(state, options = {}) {
|
|
125
|
+
const elements = [];
|
|
126
|
+
if (state.reasoning.content) {
|
|
127
|
+
elements.push(reasoningPanel(state.reasoning.content, state.reasoning.active));
|
|
128
|
+
}
|
|
129
|
+
for (const group of groupBlocks(state.blocks)) {
|
|
130
|
+
if (group.kind === "text") {
|
|
131
|
+
if (group.content.trim()) {
|
|
132
|
+
elements.push(markdown(group.content));
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
elements.push(...renderToolGroup(group.tools, state.terminal !== "running"));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (state.terminal === "interrupted") {
|
|
139
|
+
elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
|
|
140
|
+
} else if (state.terminal === "idle_timeout") {
|
|
141
|
+
const mins = state.idleTimeoutMinutes ?? 0;
|
|
142
|
+
elements.push(noteMd(`_\u23F1 ${mins} \u5206\u949F\u65E0\u54CD\u5E94,\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
|
|
143
|
+
} else if (state.terminal === "error" && state.errorMsg) {
|
|
144
|
+
elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
|
|
145
|
+
} else if (state.terminal === "done" && elements.length === 0) {
|
|
146
|
+
elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
|
|
147
|
+
}
|
|
148
|
+
if (state.terminal === "running") {
|
|
149
|
+
if (state.footer) elements.push(footerStatus(state.footer));
|
|
150
|
+
elements.push(stopButton(options));
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
schema: "2.0",
|
|
154
|
+
config: {
|
|
155
|
+
streaming_mode: state.terminal === "running",
|
|
156
|
+
summary: { content: summaryText(state) }
|
|
157
|
+
},
|
|
158
|
+
body: { elements }
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function* groupBlocks(blocks) {
|
|
162
|
+
let toolBuf = [];
|
|
163
|
+
for (const b of blocks) {
|
|
164
|
+
if (b.kind === "tool") {
|
|
165
|
+
toolBuf.push(b.tool);
|
|
166
|
+
} else {
|
|
167
|
+
if (toolBuf.length > 0) {
|
|
168
|
+
yield { kind: "tools", tools: toolBuf };
|
|
169
|
+
toolBuf = [];
|
|
170
|
+
}
|
|
171
|
+
yield { kind: "text", content: b.content };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (toolBuf.length > 0) yield { kind: "tools", tools: toolBuf };
|
|
175
|
+
}
|
|
176
|
+
function renderToolGroup(tools, finalized) {
|
|
177
|
+
if (tools.length === 0) return [];
|
|
178
|
+
if (tools.length < COLLAPSE_TOOL_THRESHOLD) {
|
|
179
|
+
return tools.map((t) => toolPanel(t, false));
|
|
180
|
+
}
|
|
181
|
+
if (finalized) {
|
|
182
|
+
return [collapsedToolSummary(tools, true)];
|
|
183
|
+
}
|
|
184
|
+
const prior = tools.slice(0, -1);
|
|
185
|
+
const latest = tools[tools.length - 1];
|
|
186
|
+
const out = [];
|
|
187
|
+
if (prior.length > 0) out.push(collapsedToolSummary(prior, false));
|
|
188
|
+
if (latest) out.push(toolPanel(latest, true));
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function reasoningPanel(content, active2) {
|
|
192
|
+
const title = active2 ? "\u{1F9E0} **\u601D\u8003\u4E2D**" : "\u{1F9E0} **\u601D\u8003\u5B8C\u6210\uFF0C\u70B9\u51FB\u67E5\u770B**";
|
|
193
|
+
return collapsiblePanel({
|
|
194
|
+
title,
|
|
195
|
+
expanded: active2,
|
|
196
|
+
border: "grey",
|
|
197
|
+
body: truncate2(content, REASONING_MAX)
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function toolPanel(tool, expanded) {
|
|
201
|
+
return collapsiblePanel({
|
|
202
|
+
title: toolHeaderText(tool),
|
|
203
|
+
expanded,
|
|
204
|
+
border: tool.status === "error" ? "red" : "grey",
|
|
205
|
+
body: toolBodyMd(tool) || "_\u65E0\u8F93\u51FA_"
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function collapsedToolSummary(tools, finalized) {
|
|
209
|
+
const suffix = finalized ? "\uFF08\u5DF2\u7ED3\u675F\uFF09" : "";
|
|
210
|
+
const title = `\u2615 **${tools.length} \u4E2A\u5DE5\u5177\u8C03\u7528${suffix}**`;
|
|
211
|
+
const headerList = tools.map((t) => `- ${toolHeaderText(t)}`).join("\n");
|
|
212
|
+
return {
|
|
213
|
+
tag: "collapsible_panel",
|
|
214
|
+
expanded: false,
|
|
215
|
+
header: panelHeader(title),
|
|
216
|
+
border: { color: "blue", corner_radius: "5px" },
|
|
217
|
+
vertical_spacing: "8px",
|
|
218
|
+
padding: "8px 8px 8px 8px",
|
|
219
|
+
elements: [{ tag: "markdown", content: headerList, text_size: "notation" }]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function collapsiblePanel(opts) {
|
|
223
|
+
return {
|
|
224
|
+
tag: "collapsible_panel",
|
|
225
|
+
expanded: opts.expanded,
|
|
226
|
+
header: panelHeader(opts.title),
|
|
227
|
+
border: { color: opts.border, corner_radius: "5px" },
|
|
228
|
+
vertical_spacing: "8px",
|
|
229
|
+
padding: "8px 8px 8px 8px",
|
|
230
|
+
elements: [{ tag: "markdown", content: opts.body, text_size: "notation" }]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function panelHeader(titleMd) {
|
|
234
|
+
return {
|
|
235
|
+
title: { tag: "markdown", content: titleMd },
|
|
236
|
+
vertical_align: "center",
|
|
237
|
+
icon: { tag: "standard_icon", token: "down-small-ccm_outlined", size: "16px 16px" },
|
|
238
|
+
icon_position: "follow_text",
|
|
239
|
+
icon_expanded_angle: -180
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function markdown(content) {
|
|
243
|
+
return { tag: "markdown", content };
|
|
244
|
+
}
|
|
245
|
+
function noteMd(content) {
|
|
246
|
+
return { tag: "markdown", content, text_size: "notation" };
|
|
247
|
+
}
|
|
248
|
+
function stopButton(options) {
|
|
249
|
+
const value = { cmd: "stop" };
|
|
250
|
+
if (options.signCallback) {
|
|
251
|
+
value.__bridge_cb = true;
|
|
252
|
+
value.bridge_token = options.signCallback("stop");
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
tag: "button",
|
|
256
|
+
text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
|
|
257
|
+
type: "danger",
|
|
258
|
+
behaviors: [{ type: "callback", value }]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function footerStatus(status) {
|
|
262
|
+
const text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
|
|
263
|
+
return noteMd(text);
|
|
264
|
+
}
|
|
265
|
+
function summaryText(state) {
|
|
266
|
+
if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
|
|
267
|
+
if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
|
|
268
|
+
if (state.terminal === "error") return "\u51FA\u9519";
|
|
269
|
+
if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
|
|
270
|
+
if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
|
|
271
|
+
if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
|
|
272
|
+
return "\u601D\u8003\u4E2D";
|
|
273
|
+
}
|
|
274
|
+
function truncate2(s, max) {
|
|
275
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/card/text-renderer.ts
|
|
279
|
+
function renderText(state) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
for (const block of state.blocks) {
|
|
282
|
+
const piece = renderBlock(block);
|
|
283
|
+
if (piece) parts.push(piece);
|
|
284
|
+
}
|
|
285
|
+
if (state.terminal === "interrupted") {
|
|
286
|
+
parts.push("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_");
|
|
287
|
+
} else if (state.terminal === "idle_timeout") {
|
|
288
|
+
const mins = state.idleTimeoutMinutes ?? 0;
|
|
289
|
+
parts.push(`_\u23F1 ${mins} \u5206\u949F\u65E0\u54CD\u5E94,\u5DF2\u81EA\u52A8\u7EC8\u6B62_`);
|
|
290
|
+
} else if (state.terminal === "error" && state.errorMsg) {
|
|
291
|
+
parts.push(`\u26A0\uFE0F agent \u5931\u8D25:${state.errorMsg}`);
|
|
292
|
+
} else if (state.terminal === "running" && state.footer) {
|
|
293
|
+
parts.push(footerLine(state.footer));
|
|
294
|
+
}
|
|
295
|
+
return parts.join("\n\n");
|
|
296
|
+
}
|
|
297
|
+
function renderBlock(block) {
|
|
298
|
+
if (block.kind === "text") {
|
|
299
|
+
return block.content.trim();
|
|
300
|
+
}
|
|
301
|
+
return toolLine(block.tool);
|
|
302
|
+
}
|
|
303
|
+
function toolLine(tool) {
|
|
304
|
+
return `> ${toolHeaderText(tool)}`;
|
|
305
|
+
}
|
|
306
|
+
function footerLine(status) {
|
|
307
|
+
if (status === "thinking") return "_\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026_";
|
|
308
|
+
if (status === "tool_running") return "_\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177\u2026_";
|
|
309
|
+
return "_\u270D\uFE0F \u6B63\u5728\u8F93\u51FA\u2026_";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/card/run-state.ts
|
|
313
|
+
var initialState = {
|
|
314
|
+
blocks: [],
|
|
315
|
+
reasoning: { content: "", active: false },
|
|
316
|
+
footer: "thinking",
|
|
317
|
+
terminal: "running"
|
|
318
|
+
};
|
|
319
|
+
function closeStreamingText(blocks) {
|
|
320
|
+
return blocks.map(
|
|
321
|
+
(b) => b.kind === "text" && b.streaming ? { ...b, streaming: false } : b
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
function reduce(state, evt) {
|
|
325
|
+
switch (evt.type) {
|
|
326
|
+
case "text": {
|
|
327
|
+
const last = state.blocks[state.blocks.length - 1];
|
|
328
|
+
if (last && last.kind === "text" && last.streaming) {
|
|
329
|
+
const next = { ...last, content: last.content + evt.delta };
|
|
330
|
+
return {
|
|
331
|
+
...state,
|
|
332
|
+
blocks: [...state.blocks.slice(0, -1), next],
|
|
333
|
+
reasoning: { ...state.reasoning, active: false },
|
|
334
|
+
footer: "streaming"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
...state,
|
|
339
|
+
blocks: [...state.blocks, { kind: "text", content: evt.delta, streaming: true }],
|
|
340
|
+
reasoning: { ...state.reasoning, active: false },
|
|
341
|
+
footer: "streaming"
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
case "thinking": {
|
|
345
|
+
return {
|
|
346
|
+
...state,
|
|
347
|
+
reasoning: { content: state.reasoning.content + evt.delta, active: true },
|
|
348
|
+
footer: "thinking"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
case "tool_use": {
|
|
352
|
+
const tool = {
|
|
353
|
+
id: evt.id,
|
|
354
|
+
name: evt.name,
|
|
355
|
+
input: evt.input,
|
|
356
|
+
status: "running"
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
...state,
|
|
360
|
+
blocks: [...closeStreamingText(state.blocks), { kind: "tool", tool }],
|
|
361
|
+
reasoning: { ...state.reasoning, active: false },
|
|
362
|
+
footer: "tool_running"
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
case "tool_result": {
|
|
366
|
+
const blocks = state.blocks.map((b) => {
|
|
367
|
+
if (b.kind !== "tool" || b.tool.id !== evt.id) return b;
|
|
368
|
+
return {
|
|
369
|
+
...b,
|
|
370
|
+
tool: {
|
|
371
|
+
...b.tool,
|
|
372
|
+
status: evt.isError ? "error" : "done",
|
|
373
|
+
output: evt.output
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
return { ...state, blocks };
|
|
378
|
+
}
|
|
379
|
+
case "error": {
|
|
380
|
+
const terminal = evt.terminationReason === "interrupted" ? "interrupted" : evt.terminationReason === "timeout" ? "idle_timeout" : "error";
|
|
381
|
+
return {
|
|
382
|
+
...state,
|
|
383
|
+
terminal,
|
|
384
|
+
errorMsg: terminal === "error" ? evt.message : state.errorMsg,
|
|
385
|
+
footer: null
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
case "done": {
|
|
389
|
+
const terminal = evt.terminationReason === "interrupted" ? "interrupted" : evt.terminationReason === "timeout" ? "idle_timeout" : "done";
|
|
390
|
+
return {
|
|
391
|
+
...state,
|
|
392
|
+
blocks: closeStreamingText(state.blocks),
|
|
393
|
+
reasoning: { ...state.reasoning, active: false },
|
|
394
|
+
terminal,
|
|
395
|
+
footer: null
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
default:
|
|
399
|
+
return state;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function markInterrupted(state) {
|
|
403
|
+
return {
|
|
404
|
+
...state,
|
|
405
|
+
blocks: closeStreamingText(state.blocks),
|
|
406
|
+
reasoning: { ...state.reasoning, active: false },
|
|
407
|
+
terminal: "interrupted",
|
|
408
|
+
footer: null
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function finalizeIfRunning(state) {
|
|
412
|
+
if (state.terminal !== "running") return state;
|
|
413
|
+
return {
|
|
414
|
+
...state,
|
|
415
|
+
blocks: closeStreamingText(state.blocks),
|
|
416
|
+
reasoning: { ...state.reasoning, active: false },
|
|
417
|
+
terminal: "done",
|
|
418
|
+
footer: null
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/core/logger.ts
|
|
423
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
424
|
+
import { createWriteStream, mkdirSync } from "fs";
|
|
425
|
+
import { open, readdir, rm, stat } from "fs/promises";
|
|
426
|
+
import { join } from "path";
|
|
427
|
+
|
|
428
|
+
// src/core/telemetry.ts
|
|
429
|
+
var noop = {
|
|
430
|
+
emit() {
|
|
431
|
+
},
|
|
432
|
+
recordError() {
|
|
433
|
+
},
|
|
434
|
+
recordMetric() {
|
|
435
|
+
},
|
|
436
|
+
flush() {
|
|
437
|
+
},
|
|
438
|
+
close() {
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
var active = noop;
|
|
442
|
+
function telemetry() {
|
|
443
|
+
return active;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/core/logger.ts
|
|
447
|
+
var DEFAULT_RETENTION_DAYS = Math.max(
|
|
448
|
+
1,
|
|
449
|
+
Number(process.env.LARK_CHANNEL_LOG_DAYS ?? 30) || 30
|
|
450
|
+
);
|
|
451
|
+
var als = new AsyncLocalStorage();
|
|
452
|
+
var RAW_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
|
|
453
|
+
"prompt",
|
|
454
|
+
"stdout",
|
|
455
|
+
"stderr",
|
|
456
|
+
"env",
|
|
457
|
+
"environment",
|
|
458
|
+
"proxy"
|
|
459
|
+
]);
|
|
460
|
+
var RESOURCE_ID_KEYS = /* @__PURE__ */ new Set(["fileKey", "sourceFileKey"]);
|
|
461
|
+
var ID_KEYS = /* @__PURE__ */ new Set([
|
|
462
|
+
"chatId",
|
|
463
|
+
"senderId",
|
|
464
|
+
"sender",
|
|
465
|
+
"openId",
|
|
466
|
+
"operatorId",
|
|
467
|
+
"userId",
|
|
468
|
+
"msgId",
|
|
469
|
+
"messageId",
|
|
470
|
+
"sourceMessageId",
|
|
471
|
+
"sessionId",
|
|
472
|
+
"threadId",
|
|
473
|
+
"docToken",
|
|
474
|
+
"fileToken",
|
|
475
|
+
"fileKey",
|
|
476
|
+
"sourceFileKey",
|
|
477
|
+
"commentId",
|
|
478
|
+
"rootCommentId",
|
|
479
|
+
"replyId",
|
|
480
|
+
"reactionId",
|
|
481
|
+
"scope",
|
|
482
|
+
"appId"
|
|
483
|
+
]);
|
|
484
|
+
var MAX_LOG_STRING_CHARS = 4096;
|
|
485
|
+
var CREDENTIAL_JSON_FIELD_RE = /("(?:secret|app_secret|appSecret|token|access_token|tenant_access_token|app_access_token|authorization)"\s*:\s*")[^"]*(")/gi;
|
|
486
|
+
var ESCAPED_CREDENTIAL_JSON_FIELD_RE = /(\\\"(?:secret|app_secret|appSecret|token|access_token|tenant_access_token|app_access_token|authorization)\\\"\s*:\s*\\\")[^\\]*(\\\")/gi;
|
|
487
|
+
var RESOURCE_JSON_FIELD_RE = /("(?:fileKey|sourceFileKey|file_key|source_file_key|imageKey|image_key|mediaKey|media_key)"\s*:\s*")[^"]*(")/gi;
|
|
488
|
+
var ESCAPED_RESOURCE_JSON_FIELD_RE = /(\\\"(?:fileKey|sourceFileKey|file_key|source_file_key|imageKey|image_key|mediaKey|media_key)\\\"\s*:\s*\\\")[^\\]*(\\\")/gi;
|
|
489
|
+
var EXTERNAL_SANITIZE = { redactIds: true };
|
|
490
|
+
function sanitizeLogValue(key, value, options = EXTERNAL_SANITIZE) {
|
|
491
|
+
const normalizedKey = key.startsWith("_") ? key.slice(1) : key;
|
|
492
|
+
if (value === void 0) return void 0;
|
|
493
|
+
if (RAW_PAYLOAD_KEYS.has(normalizedKey)) return "[REDACTED]";
|
|
494
|
+
if (/token|secret|authorization/i.test(normalizedKey)) return "[REDACTED]";
|
|
495
|
+
if (/attachment.*path|media.*path|^(cwd|cwdRealpath|path|absPath)$/i.test(normalizedKey)) {
|
|
496
|
+
return "[REDACTED_PATH]";
|
|
497
|
+
}
|
|
498
|
+
if (RESOURCE_ID_KEYS.has(normalizedKey)) return "[REDACTED_RESOURCE]";
|
|
499
|
+
if (options.redactIds && ID_KEYS.has(normalizedKey)) return redactId(value);
|
|
500
|
+
if (Array.isArray(value)) {
|
|
501
|
+
return value.map((item) => sanitizeLogValue(key, item, options));
|
|
502
|
+
}
|
|
503
|
+
if (value && typeof value === "object") {
|
|
504
|
+
const nested = {};
|
|
505
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
506
|
+
nested[nestedKey] = sanitizeLogValue(nestedKey, nestedValue, options);
|
|
507
|
+
}
|
|
508
|
+
return nested;
|
|
509
|
+
}
|
|
510
|
+
if (typeof value === "string") {
|
|
511
|
+
const redacted = redactDiagnosticText(value);
|
|
512
|
+
if (redacted.length > MAX_LOG_STRING_CHARS) {
|
|
513
|
+
return `${redacted.slice(0, MAX_LOG_STRING_CHARS)}...[truncated]`;
|
|
514
|
+
}
|
|
515
|
+
return redacted;
|
|
516
|
+
}
|
|
517
|
+
return value;
|
|
518
|
+
}
|
|
519
|
+
function redactId(value) {
|
|
520
|
+
if (typeof value !== "string") return value;
|
|
521
|
+
if (value.length <= 6) return value;
|
|
522
|
+
return `...${value.slice(-6)}`;
|
|
523
|
+
}
|
|
524
|
+
function redactDiagnosticText(text) {
|
|
525
|
+
let out = redactJsonCredentialText(text);
|
|
526
|
+
out = redactResourceText(out);
|
|
527
|
+
out = out.replace(
|
|
528
|
+
/\b(Authorization\s*[:=]\s*Bearer\s+)[A-Za-z0-9._\-+/=]+/gi,
|
|
529
|
+
"$1[REDACTED]"
|
|
530
|
+
);
|
|
531
|
+
out = out.replace(/\b(Bearer\s+)[A-Za-z0-9._\-+/=]+/g, "$1[REDACTED]");
|
|
532
|
+
out = out.replace(
|
|
533
|
+
/\b(access_token|tenant_access_token|app_access_token|app_secret|appSecret|secret|token|doc_token|file_token|authorization)=([^&\s"',}]+)/gi,
|
|
534
|
+
"$1=[REDACTED]"
|
|
535
|
+
);
|
|
536
|
+
out = out.replace(
|
|
537
|
+
/(^|[\s"'=])((?:\/(?:Users|home|tmp|var|private|Volumes|opt|workspace|workspaces|mnt|app|srv|root|data)\/[^\s"',)]+))/g,
|
|
538
|
+
"$1[REDACTED_PATH]"
|
|
539
|
+
);
|
|
540
|
+
out = out.replace(/(^|[\s"'=])(~\/[^\s"',)]+)/g, "$1[REDACTED_PATH]");
|
|
541
|
+
out = out.replace(/[A-Za-z]:\\[^\s"',)]+/g, "[REDACTED_PATH]");
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function redactJsonCredentialText(text) {
|
|
545
|
+
return text.replace(CREDENTIAL_JSON_FIELD_RE, "$1[REDACTED]$2").replace(ESCAPED_CREDENTIAL_JSON_FIELD_RE, "$1[REDACTED]$2");
|
|
546
|
+
}
|
|
547
|
+
function redactResourceText(text) {
|
|
548
|
+
return text.replace(RESOURCE_JSON_FIELD_RE, "$1[REDACTED_RESOURCE]$2").replace(ESCAPED_RESOURCE_JSON_FIELD_RE, "$1[REDACTED_RESOURCE]$2").replace(
|
|
549
|
+
/<\s*(?:file|image|img|audio|video|media|folder)\b[^>]*\bkey\s*=\s*["'][^"']+["'][^>]*>/gi,
|
|
550
|
+
"[REDACTED_RESOURCE]"
|
|
551
|
+
).replace(/!?\[[^\]]*]\((?:file|img|image|media)_[^)]+\)/gi, "[REDACTED_RESOURCE]").replace(
|
|
552
|
+
/\b(?:file|img|image|media)_(?:v\d+_)?[A-Za-z0-9][A-Za-z0-9._-]{8,}\b/g,
|
|
553
|
+
"[REDACTED_RESOURCE]"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
function reportMetric(name, value, tags) {
|
|
557
|
+
try {
|
|
558
|
+
telemetry().recordMetric(name, value, sanitizeMetricTags(tags));
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function reportError(err, ctx) {
|
|
563
|
+
try {
|
|
564
|
+
telemetry().recordError(sanitizeTelemetryError(err), sanitizeTelemetryContext(ctx));
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function sanitizeMetricTags(tags) {
|
|
569
|
+
if (!tags) return void 0;
|
|
570
|
+
const out = {};
|
|
571
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
572
|
+
const sanitized = sanitizeLogValue(key, value);
|
|
573
|
+
out[key] = typeof sanitized === "string" ? sanitized : JSON.stringify(sanitized);
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
function sanitizeTelemetryContext(ctx) {
|
|
578
|
+
if (!ctx) return void 0;
|
|
579
|
+
const out = {};
|
|
580
|
+
for (const [key, value] of Object.entries(ctx)) {
|
|
581
|
+
out[key] = sanitizeLogValue(key, value);
|
|
582
|
+
}
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
function sanitizeTelemetryError(err) {
|
|
586
|
+
if (err instanceof Error) {
|
|
587
|
+
return {
|
|
588
|
+
name: err.name,
|
|
589
|
+
message: sanitizeLogValue("err", err.message),
|
|
590
|
+
...err.stack ? { stack: sanitizeLogValue("stack", err.stack) } : {}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return sanitizeLogValue("err", err);
|
|
594
|
+
}
|
|
595
|
+
export {
|
|
596
|
+
finalizeIfRunning,
|
|
597
|
+
initialState,
|
|
598
|
+
markInterrupted,
|
|
599
|
+
reduce,
|
|
600
|
+
renderCard,
|
|
601
|
+
renderText,
|
|
602
|
+
reportError,
|
|
603
|
+
reportMetric
|
|
604
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alexlikevibe/lark-channel-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bridge Feishu/Lark messenger with local CLI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@10.33.0",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/iefnaf/lark-coding-agent-bridge.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/iefnaf/lark-coding-agent-bridge/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/iefnaf/lark-coding-agent-bridge#readme",
|
|
19
|
+
"bin": {
|
|
20
|
+
"lark-channel-bridge": "bin/lark-channel-bridge.mjs"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"bin",
|
|
31
|
+
"README.md",
|
|
32
|
+
"README.zh.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:unit": "vitest run tests/unit --passWithNoTests",
|
|
41
|
+
"test:integration": "vitest run tests/integration --passWithNoTests",
|
|
42
|
+
"test:process": "vitest run tests/process --passWithNoTests",
|
|
43
|
+
"ci:local": "git diff --check && pnpm test && pnpm typecheck && pnpm build",
|
|
44
|
+
"ci:platform": "pnpm test && pnpm typecheck && pnpm build",
|
|
45
|
+
"prepare": "npm run build",
|
|
46
|
+
"prepublishOnly": "pnpm typecheck && pnpm build"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@clack/prompts": "^1.4.0",
|
|
50
|
+
"@larksuite/channel": "^0.2.0",
|
|
51
|
+
"commander": "^12.1.0",
|
|
52
|
+
"cross-spawn": "^7.0.6",
|
|
53
|
+
"graceful-fs": "^4.2.11",
|
|
54
|
+
"proper-lockfile": "^4.1.2",
|
|
55
|
+
"qrcode-terminal": "^0.12.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/cross-spawn": "^6.0.6",
|
|
59
|
+
"@types/graceful-fs": "^4.1.9",
|
|
60
|
+
"@types/node": "^22.10.0",
|
|
61
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
62
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
63
|
+
"tsup": "^8.3.5",
|
|
64
|
+
"typescript": "^5.6.3",
|
|
65
|
+
"vitest": "^2.1.8"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=20.12.0"
|
|
69
|
+
},
|
|
70
|
+
"pnpm": {
|
|
71
|
+
"onlyBuiltDependencies": [
|
|
72
|
+
"esbuild",
|
|
73
|
+
"protobufjs"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"keywords": [
|
|
77
|
+
"feishu",
|
|
78
|
+
"lark",
|
|
79
|
+
"claude",
|
|
80
|
+
"claude-code",
|
|
81
|
+
"codex",
|
|
82
|
+
"cli",
|
|
83
|
+
"channel",
|
|
84
|
+
"bridge"
|
|
85
|
+
],
|
|
86
|
+
"license": "MIT"
|
|
87
|
+
}
|