loopable-cli 0.2.0 → 0.2.1
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/.output/nitro.json +1 -1
- package/.output/public/assets/_connectorId-CkygNLny.js +1 -0
- package/.output/public/assets/_loopId-Cshlc5Ja.js +1 -0
- package/.output/public/assets/_taskId-BKDIJ_Fg.js +1 -0
- package/.output/public/assets/connector-icon-BUoK6Cru.js +1 -0
- package/.output/public/assets/connectors-CeLBtEtg.js +1 -0
- package/.output/public/assets/gaps-BtFoD7mF.js +1 -0
- package/.output/public/assets/{inbox-CLEXUXCI.js → inbox-BYNjkNDJ.js} +1 -1
- package/.output/public/assets/index-0yP3Jn_G.js +18 -0
- package/.output/public/assets/loop-form-DnSJNc1k.js +1 -0
- package/.output/public/assets/loops-1CvbqO6m.js +1 -0
- package/.output/public/assets/{new-DPEaXKAY.js → new-m5NlqYQT.js} +1 -1
- package/.output/public/assets/refresh-cw-C3xBnKfy.js +1 -0
- package/.output/public/assets/{runners-BVBggS5B.js → runners-D8QVl-Y6.js} +1 -1
- package/.output/public/assets/{setting-fields-BT1naVJX.js → setting-fields-bJ_gTG7Q.js} +1 -1
- package/.output/public/assets/styles-ClTGaXU0.css +2 -0
- package/.output/public/assets/{task-list-CrptnI1N.js → task-list-BAxjcVgr.js} +1 -1
- package/.output/server/{_connectorId-DcSnoRjk.mjs → _connectorId-C9yydJrC.mjs} +90 -9
- package/.output/server/{_loopId-G_TAkYtS.mjs → _loopId-B0VG-eBC.mjs} +42 -13
- package/.output/server/_ssr/{callback-D20cVD2w.mjs → access-DTttr563.mjs} +37 -20
- package/.output/server/_ssr/{agents-Dms_6TBF.mjs → agents-C7B6RJ6-.mjs} +3 -3
- package/.output/server/_ssr/{agents-CrUmIQpi.mjs → agents-DG643yoJ.mjs} +2 -2
- package/.output/server/_ssr/alert-DQvzE7bu.mjs +1 -1
- package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +1 -1
- package/.output/server/_ssr/{connections-ypex3qUd.mjs → connections-Df-aDpfJ.mjs} +814 -15
- package/.output/server/_ssr/connector-icon-WvxwZ5TF.mjs +56 -0
- package/.output/server/_ssr/{connectors-4yzKb0KG.mjs → connectors-BU0IiRge.mjs} +6 -5
- package/.output/server/_ssr/{connectors-BZly0yGF.mjs → connectors-e0mf2pMs.mjs} +14 -3
- package/.output/server/_ssr/{gaps-D2Zh8sEo.mjs → gaps-Cz76ZicM.mjs} +6 -6
- package/.output/server/_ssr/{inbox-C2VBGLa5.mjs → inbox-CAUBa1I3.mjs} +3 -3
- package/.output/server/_ssr/{loop-form-D-U-kU4k.mjs → loop-form-FqSaXiHl.mjs} +5 -5
- package/.output/server/_ssr/{loops-Og_G3KyZ.mjs → loops-D8WuK7Ke.mjs} +199 -6
- package/.output/server/_ssr/{loops-D0XJ8eBb.mjs → loops-Dqoebe8q.mjs} +6 -6
- package/.output/server/_ssr/{new-CajvgIog.mjs → new-bBsVaWD6.mjs} +5 -5
- package/.output/server/_ssr/{router-BbrBz36P.mjs → router-54IDWTVz.mjs} +24 -19
- package/.output/server/_ssr/{runners-B_WuF8UJ.mjs → runners-Cj6z9Mqh.mjs} +26 -3
- package/.output/server/_ssr/{runners-D-0_lfiq.mjs → runners-D2UAdLyj.mjs} +1 -1
- package/.output/server/_ssr/{runners-zqF_zbEc.mjs → runners-DGWaVvWi.mjs} +2 -2
- package/.output/server/_ssr/{setting-fields-mpXfGftI.mjs → setting-fields-DzBlslan.mjs} +2 -2
- package/.output/server/_ssr/ssr.mjs +42 -34
- package/.output/server/_ssr/{start-BgTj-h_C.mjs → start-Vq3Oan3v.mjs} +2 -3
- package/.output/server/_ssr/switch-1pnd_bPJ.mjs +1 -1
- package/.output/server/_ssr/{task-list-BnQJ94Dk.mjs → task-list-_qgBS9LU.mjs} +5 -5
- package/.output/server/_ssr/{tasks-B_QmDFVs.mjs → tasks-BDqznm57.mjs} +16 -5
- package/.output/server/_ssr/{tasks-LIi-W4cy.mjs → tasks-Qtml5TlK.mjs} +3 -3
- package/.output/server/_ssr/textarea-CHJTKtsJ.mjs +1 -1
- package/.output/server/{_tanstack-start-manifest_v-BCHAazE8.mjs → _tanstack-start-manifest_v-CvheE7ZF.mjs} +25 -23
- package/.output/server/{_taskId-CIv0ajIx.mjs → _taskId-V4QLBO2N.mjs} +4 -4
- package/.output/server/index.mjs +139 -132
- package/README.md +24 -19
- package/dist/cli.mjs +24 -59
- package/dist/dispatcher.mjs +811 -31
- package/package.json +21 -17
- package/.output/public/assets/_connectorId-m67WbiJ8.js +0 -1
- package/.output/public/assets/_loopId-Bf4K5sdd.js +0 -1
- package/.output/public/assets/_taskId-eCKkcVOx.js +0 -1
- package/.output/public/assets/connector-icon-qGpNsPgH.js +0 -1
- package/.output/public/assets/connectors-DxXlc9zF.js +0 -1
- package/.output/public/assets/gaps-CoTA-PzA.js +0 -1
- package/.output/public/assets/index-Cndsw84p.js +0 -16
- package/.output/public/assets/loop-form-BvizlZdR.js +0 -1
- package/.output/public/assets/loops-Dbe_mf5T.js +0 -1
- package/.output/public/assets/styles-D3sonCfX.css +0 -2
- package/.output/server/_ssr/access-B7TcOZ7F.mjs +0 -42
- package/.output/server/_ssr/connector-icon-CFrrHjzR.mjs +0 -37
package/dist/dispatcher.mjs
CHANGED
|
@@ -407,11 +407,11 @@ function parse(line) {
|
|
|
407
407
|
return null;
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
|
-
function
|
|
410
|
+
function textOf3(event) {
|
|
411
411
|
return (event.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
412
412
|
}
|
|
413
|
-
function describeTool(
|
|
414
|
-
const [name, body] = Object.entries(
|
|
413
|
+
function describeTool(call3, cwd) {
|
|
414
|
+
const [name, body] = Object.entries(call3 ?? {})[0] ?? [];
|
|
415
415
|
if (!name) return "a tool";
|
|
416
416
|
const verb = name.replace(/ToolCall$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
417
417
|
const args3 = body?.args ?? {};
|
|
@@ -447,9 +447,9 @@ function answerFrom(stdout) {
|
|
|
447
447
|
const whole = events.findLastIndex(
|
|
448
448
|
(event) => event?.type === "assistant" && event.timestamp_ms === void 0
|
|
449
449
|
);
|
|
450
|
-
if (whole >= 0) return { text:
|
|
450
|
+
if (whole >= 0) return { text: textOf3(events[whole]).trim(), failed };
|
|
451
451
|
const lastTool = events.findLastIndex((event) => event?.type === "tool_call");
|
|
452
|
-
const partial = events.slice(lastTool + 1).filter((event) => event?.type === "assistant").map((event) =>
|
|
452
|
+
const partial = events.slice(lastTool + 1).filter((event) => event?.type === "assistant").map((event) => textOf3(event));
|
|
453
453
|
if (partial.length > 0) return { text: partial.join("").trim(), failed };
|
|
454
454
|
return { text: (result?.result ?? "").trim(), failed };
|
|
455
455
|
}
|
|
@@ -2935,7 +2935,7 @@ var scheduleManifest = defineManifest({
|
|
|
2935
2935
|
]
|
|
2936
2936
|
});
|
|
2937
2937
|
|
|
2938
|
-
// src/connectors/
|
|
2938
|
+
// src/connectors/slack/manifest.ts
|
|
2939
2939
|
var folder2 = {
|
|
2940
2940
|
key: "folder",
|
|
2941
2941
|
kind: "text",
|
|
@@ -2943,6 +2943,173 @@ var folder2 = {
|
|
|
2943
2943
|
help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
|
|
2944
2944
|
placeholder: "/Users/you/code/your-project"
|
|
2945
2945
|
};
|
|
2946
|
+
var channel = {
|
|
2947
|
+
key: "channel",
|
|
2948
|
+
kind: "text",
|
|
2949
|
+
label: "Channel",
|
|
2950
|
+
help: "Leave empty to reply in the thread that asked. Set a channel ID when the work started somewhere else.",
|
|
2951
|
+
placeholder: "C0123456789"
|
|
2952
|
+
};
|
|
2953
|
+
var ASK_PROMPT = [
|
|
2954
|
+
"Someone has sent you a message in Slack. Do what it asks, working in this directory,",
|
|
2955
|
+
"and then answer them.",
|
|
2956
|
+
"",
|
|
2957
|
+
"You are answering into Slack. Be brief: a couple of short paragraphs at most,",
|
|
2958
|
+
"no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
|
|
2959
|
+
"",
|
|
2960
|
+
"If the message asks a question about this code, read enough of it to answer",
|
|
2961
|
+
"properly rather than guessing. If it asks for a change, make the change and",
|
|
2962
|
+
"say in one line what you did. If the message is too vague to act on, say what",
|
|
2963
|
+
"you need to know instead of assuming."
|
|
2964
|
+
].join("\n");
|
|
2965
|
+
var slackManifest = defineManifest({
|
|
2966
|
+
id: "slack",
|
|
2967
|
+
name: "Slack bot",
|
|
2968
|
+
tagline: "Paste a bot token from a Slack app \u2014 not a Slack login. @mention or DM the bot, and it answers in the thread.",
|
|
2969
|
+
docsUrl: "https://api.slack.com/apps",
|
|
2970
|
+
icon: "Slack",
|
|
2971
|
+
accent: "bg-[#4a154b] text-white",
|
|
2972
|
+
auth: {
|
|
2973
|
+
kind: "token",
|
|
2974
|
+
helpUrl: "https://api.slack.com/apps",
|
|
2975
|
+
note: "This is a Slack app in the workspace, not a Slack account sign-in. Create an app at api.slack.com/apps, add bot scopes (channels:history, channels:read, groups:history, groups:read, im:history, im:read, mpim:history, mpim:read, chat:write), Install to Workspace, then paste the Bot User OAuth Token. Invite the bot to any channel it should watch.",
|
|
2976
|
+
fields: [
|
|
2977
|
+
{
|
|
2978
|
+
key: "botToken",
|
|
2979
|
+
label: "Bot User OAuth Token",
|
|
2980
|
+
placeholder: "xoxb-...",
|
|
2981
|
+
secret: true
|
|
2982
|
+
}
|
|
2983
|
+
]
|
|
2984
|
+
},
|
|
2985
|
+
workflows: [
|
|
2986
|
+
{
|
|
2987
|
+
id: "slack.ask",
|
|
2988
|
+
name: "Do what I ask the bot",
|
|
2989
|
+
summary: "Runs your agent when someone @mentions the bot or DMs it, and answers in the thread.",
|
|
2990
|
+
trigger: "the bot is @mentioned, or someone DMs it",
|
|
2991
|
+
writes: "a reply in the Slack thread",
|
|
2992
|
+
runsIn: "folder",
|
|
2993
|
+
settings: [folder2],
|
|
2994
|
+
prompt: ASK_PROMPT,
|
|
2995
|
+
guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
|
|
2996
|
+
answer: "text",
|
|
2997
|
+
actionId: "slack.reply",
|
|
2998
|
+
pollEveryMs: 6e4
|
|
2999
|
+
}
|
|
3000
|
+
],
|
|
3001
|
+
actions: [
|
|
3002
|
+
{
|
|
3003
|
+
id: "slack.reply",
|
|
3004
|
+
name: "Reply in Slack",
|
|
3005
|
+
summary: "Post a message back to the thread that asked, or to a channel you name.",
|
|
3006
|
+
target: [channel],
|
|
3007
|
+
accepts: ["text"]
|
|
3008
|
+
}
|
|
3009
|
+
],
|
|
3010
|
+
settings: [],
|
|
3011
|
+
allowsMultipleAccounts: false,
|
|
3012
|
+
byHand: { kind: "none" }
|
|
3013
|
+
});
|
|
3014
|
+
|
|
3015
|
+
// src/connectors/feishu/manifest.ts
|
|
3016
|
+
var folder3 = {
|
|
3017
|
+
key: "folder",
|
|
3018
|
+
kind: "text",
|
|
3019
|
+
label: "Folder to work in",
|
|
3020
|
+
help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
|
|
3021
|
+
placeholder: "/Users/you/code/your-project"
|
|
3022
|
+
};
|
|
3023
|
+
var chat = {
|
|
3024
|
+
key: "chat",
|
|
3025
|
+
kind: "text",
|
|
3026
|
+
label: "Chat ID",
|
|
3027
|
+
help: "Leave empty to reply to the message that asked. Set a chat ID (oc_\u2026) when the work started somewhere else.",
|
|
3028
|
+
placeholder: "oc_..."
|
|
3029
|
+
};
|
|
3030
|
+
var ASK_PROMPT2 = [
|
|
3031
|
+
"Someone has sent you a message in Feishu. Do what it asks, working in this directory,",
|
|
3032
|
+
"and then answer them.",
|
|
3033
|
+
"",
|
|
3034
|
+
"You are answering into Feishu. Be brief: a couple of short paragraphs at most,",
|
|
3035
|
+
"no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
|
|
3036
|
+
"",
|
|
3037
|
+
"If the message asks a question about this code, read enough of it to answer",
|
|
3038
|
+
"properly rather than guessing. If it asks for a change, make the change and",
|
|
3039
|
+
"say in one line what you did. If the message is too vague to act on, say what",
|
|
3040
|
+
"you need to know instead of assuming."
|
|
3041
|
+
].join("\n");
|
|
3042
|
+
var feishuManifest = defineManifest({
|
|
3043
|
+
id: "feishu",
|
|
3044
|
+
name: "Feishu / Lark bot",
|
|
3045
|
+
tagline: "Paste App ID and App Secret from a Feishu or Lark app \u2014 not a Feishu login. @mention the bot in a group, and it answers in the thread.",
|
|
3046
|
+
docsUrl: "https://open.feishu.cn/app",
|
|
3047
|
+
icon: "Feishu",
|
|
3048
|
+
accent: "bg-[#3370ff] text-white",
|
|
3049
|
+
auth: {
|
|
3050
|
+
kind: "token",
|
|
3051
|
+
helpUrl: "https://open.feishu.cn/app",
|
|
3052
|
+
note: "This is a custom Feishu or Lark app, not a Feishu account sign-in. Create an app, enable Bot, add permissions (im:chat:readonly, im:message, im:message.group_msg, im:message:send_as_bot), create a version and publish it, then paste App ID and App Secret. Invite the bot to any group it should watch. Put lark in Open platform for Lark (international).",
|
|
3053
|
+
fields: [
|
|
3054
|
+
{
|
|
3055
|
+
key: "appId",
|
|
3056
|
+
label: "App ID",
|
|
3057
|
+
placeholder: "cli_...",
|
|
3058
|
+
secret: false
|
|
3059
|
+
},
|
|
3060
|
+
{
|
|
3061
|
+
key: "appSecret",
|
|
3062
|
+
label: "App Secret",
|
|
3063
|
+
placeholder: "App Secret",
|
|
3064
|
+
secret: true
|
|
3065
|
+
},
|
|
3066
|
+
{
|
|
3067
|
+
key: "host",
|
|
3068
|
+
label: "Open platform",
|
|
3069
|
+
placeholder: "feishu or lark",
|
|
3070
|
+
secret: false,
|
|
3071
|
+
optional: true
|
|
3072
|
+
}
|
|
3073
|
+
]
|
|
3074
|
+
},
|
|
3075
|
+
workflows: [
|
|
3076
|
+
{
|
|
3077
|
+
id: "feishu.ask",
|
|
3078
|
+
name: "Do what I ask the bot",
|
|
3079
|
+
summary: "Runs your agent when someone @mentions the bot in a group it is in, and answers in the thread.",
|
|
3080
|
+
trigger: "the bot is @mentioned in a group it is in",
|
|
3081
|
+
writes: "a reply in the Feishu thread",
|
|
3082
|
+
runsIn: "folder",
|
|
3083
|
+
settings: [folder3],
|
|
3084
|
+
prompt: ASK_PROMPT2,
|
|
3085
|
+
guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
|
|
3086
|
+
answer: "text",
|
|
3087
|
+
actionId: "feishu.reply",
|
|
3088
|
+
pollEveryMs: 6e4
|
|
3089
|
+
}
|
|
3090
|
+
],
|
|
3091
|
+
actions: [
|
|
3092
|
+
{
|
|
3093
|
+
id: "feishu.reply",
|
|
3094
|
+
name: "Reply in Feishu",
|
|
3095
|
+
summary: "Post a message back to the thread that asked, or to a chat you name.",
|
|
3096
|
+
target: [chat],
|
|
3097
|
+
accepts: ["text"]
|
|
3098
|
+
}
|
|
3099
|
+
],
|
|
3100
|
+
settings: [],
|
|
3101
|
+
allowsMultipleAccounts: false,
|
|
3102
|
+
byHand: { kind: "none" }
|
|
3103
|
+
});
|
|
3104
|
+
|
|
3105
|
+
// src/connectors/wechat/manifest.ts
|
|
3106
|
+
var folder4 = {
|
|
3107
|
+
key: "folder",
|
|
3108
|
+
kind: "text",
|
|
3109
|
+
label: "Folder to work in",
|
|
3110
|
+
help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
|
|
3111
|
+
placeholder: "/Users/you/code/your-project"
|
|
3112
|
+
};
|
|
2946
3113
|
var askers = {
|
|
2947
3114
|
key: "askers",
|
|
2948
3115
|
kind: "select",
|
|
@@ -2964,7 +3131,7 @@ var recipient = {
|
|
|
2964
3131
|
],
|
|
2965
3132
|
default: "source"
|
|
2966
3133
|
};
|
|
2967
|
-
var
|
|
3134
|
+
var ASK_PROMPT3 = [
|
|
2968
3135
|
"Someone has sent you a message. Do what it asks, working in this directory,",
|
|
2969
3136
|
"and then answer them.",
|
|
2970
3137
|
"",
|
|
@@ -2998,8 +3165,8 @@ var wechatManifest = {
|
|
|
2998
3165
|
// by asking, so there is nothing here that could be checked.
|
|
2999
3166
|
writes: "a reply in the chat",
|
|
3000
3167
|
runsIn: "folder",
|
|
3001
|
-
settings: [
|
|
3002
|
-
prompt:
|
|
3168
|
+
settings: [folder4, askers],
|
|
3169
|
+
prompt: ASK_PROMPT3,
|
|
3003
3170
|
guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
|
|
3004
3171
|
answer: "text",
|
|
3005
3172
|
actionId: "wechat.reply"
|
|
@@ -3027,6 +3194,8 @@ var wechatManifest = {
|
|
|
3027
3194
|
// src/connectors/manifests.ts
|
|
3028
3195
|
var CONNECTOR_MANIFESTS = [
|
|
3029
3196
|
githubManifest,
|
|
3197
|
+
slackManifest,
|
|
3198
|
+
feishuManifest,
|
|
3030
3199
|
gmailManifest,
|
|
3031
3200
|
wechatManifest,
|
|
3032
3201
|
scheduleManifest
|
|
@@ -4203,6 +4372,615 @@ var scheduleRuntime = {
|
|
|
4203
4372
|
}
|
|
4204
4373
|
};
|
|
4205
4374
|
|
|
4375
|
+
// src/connectors/slack/api.ts
|
|
4376
|
+
var API = "https://slack.com/api";
|
|
4377
|
+
async function slack(path, token, init = {}) {
|
|
4378
|
+
const url = new URL(`${API}/${path}`);
|
|
4379
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
4380
|
+
url.searchParams.set(key, value);
|
|
4381
|
+
}
|
|
4382
|
+
let response;
|
|
4383
|
+
try {
|
|
4384
|
+
response = await fetch(url.href, {
|
|
4385
|
+
method: init.method ?? "GET",
|
|
4386
|
+
headers: {
|
|
4387
|
+
Authorization: `Bearer ${token}`,
|
|
4388
|
+
...init.body ? { "Content-Type": "application/json" } : {}
|
|
4389
|
+
},
|
|
4390
|
+
body: init.body ? JSON.stringify(init.body) : void 0
|
|
4391
|
+
});
|
|
4392
|
+
} catch (error) {
|
|
4393
|
+
throw new TransientError(`Slack is unreachable: ${error.message}`);
|
|
4394
|
+
}
|
|
4395
|
+
if (response.status >= 500 || response.status === 429) {
|
|
4396
|
+
throw new TransientError(`Slack answered ${response.status}`);
|
|
4397
|
+
}
|
|
4398
|
+
if (!response.ok) throw new Error(`Slack answered ${response.status}`);
|
|
4399
|
+
return await response.json();
|
|
4400
|
+
}
|
|
4401
|
+
function refuse(error) {
|
|
4402
|
+
if (error === "invalid_auth" || error === "not_authed" || error === "token_revoked" || error === "account_inactive") {
|
|
4403
|
+
throw new Error("Slack did not accept that token.");
|
|
4404
|
+
}
|
|
4405
|
+
throw new Error(error ? `Slack refused: ${error}` : "Slack refused the request.");
|
|
4406
|
+
}
|
|
4407
|
+
async function authTest(botToken) {
|
|
4408
|
+
const body = await slack("auth.test", botToken);
|
|
4409
|
+
if (!body.ok) refuse(body.error);
|
|
4410
|
+
const teamUrl = typeof body.url === "string" ? body.url : "";
|
|
4411
|
+
const team = typeof body.team === "string" ? body.team : "";
|
|
4412
|
+
const teamId = typeof body.team_id === "string" ? body.team_id : "";
|
|
4413
|
+
const botUserId = typeof body.user_id === "string" ? body.user_id : "";
|
|
4414
|
+
const botId = typeof body.bot_id === "string" ? body.bot_id : "";
|
|
4415
|
+
if (!teamId || !botUserId) throw new Error("Slack did not return which workspace this bot belongs to.");
|
|
4416
|
+
return { botToken, teamId, team, teamUrl, botUserId, botId };
|
|
4417
|
+
}
|
|
4418
|
+
async function listConversations(credential) {
|
|
4419
|
+
const body = await slack("conversations.list", credential.botToken, {
|
|
4420
|
+
query: {
|
|
4421
|
+
types: "public_channel,private_channel,im,mpim",
|
|
4422
|
+
exclude_archived: "true",
|
|
4423
|
+
limit: "200"
|
|
4424
|
+
}
|
|
4425
|
+
});
|
|
4426
|
+
if (!body.ok) refuse(body.error);
|
|
4427
|
+
const channels = Array.isArray(body.channels) ? body.channels : [];
|
|
4428
|
+
return channels.map((raw) => {
|
|
4429
|
+
const channel2 = raw;
|
|
4430
|
+
if (!channel2.id) return null;
|
|
4431
|
+
return { id: channel2.id, name: channel2.name ?? "", isIm: channel2.is_im === true };
|
|
4432
|
+
}).filter((channel2) => channel2 !== null);
|
|
4433
|
+
}
|
|
4434
|
+
async function history(credential, channelId, oldest) {
|
|
4435
|
+
const query2 = {
|
|
4436
|
+
channel: channelId,
|
|
4437
|
+
limit: "100",
|
|
4438
|
+
inclusive: "false"
|
|
4439
|
+
};
|
|
4440
|
+
if (oldest) query2.oldest = oldest;
|
|
4441
|
+
const body = await slack("conversations.history", credential.botToken, { query: query2 });
|
|
4442
|
+
if (!body.ok) {
|
|
4443
|
+
if (body.error === "not_in_channel" || body.error === "channel_not_found") return [];
|
|
4444
|
+
refuse(body.error);
|
|
4445
|
+
}
|
|
4446
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
4447
|
+
const parsed = [];
|
|
4448
|
+
for (const raw of messages) {
|
|
4449
|
+
const message = raw;
|
|
4450
|
+
if (!message.ts) continue;
|
|
4451
|
+
parsed.push({
|
|
4452
|
+
ts: message.ts,
|
|
4453
|
+
...message.user ? { user: message.user } : {},
|
|
4454
|
+
...message.bot_id ? { botId: message.bot_id } : {},
|
|
4455
|
+
text: message.text ?? "",
|
|
4456
|
+
...message.thread_ts ? { threadTs: message.thread_ts } : {}
|
|
4457
|
+
});
|
|
4458
|
+
}
|
|
4459
|
+
return parsed.sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
4460
|
+
}
|
|
4461
|
+
async function postMessage(credential, input) {
|
|
4462
|
+
const body = await slack("chat.postMessage", credential.botToken, {
|
|
4463
|
+
method: "POST",
|
|
4464
|
+
body: {
|
|
4465
|
+
channel: input.channel,
|
|
4466
|
+
text: input.text,
|
|
4467
|
+
...input.threadTs ? { thread_ts: input.threadTs } : {}
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
if (!body.ok) refuse(body.error);
|
|
4471
|
+
const ts = typeof body.ts === "string" ? body.ts : "";
|
|
4472
|
+
const channel2 = typeof body.channel === "string" ? body.channel : input.channel;
|
|
4473
|
+
if (!ts) throw new Error("Slack accepted the message but did not say where it landed.");
|
|
4474
|
+
return { ts, channel: channel2 };
|
|
4475
|
+
}
|
|
4476
|
+
function permalink2(credential, channel2, ts) {
|
|
4477
|
+
const host = credential.teamUrl.replace(/\/$/, "");
|
|
4478
|
+
return `${host}/archives/${channel2}/p${ts.replace(".", "")}`;
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
// src/connectors/slack/runtime.ts
|
|
4482
|
+
function asAsked(value) {
|
|
4483
|
+
const record = value;
|
|
4484
|
+
if (!record || typeof record.channel !== "string" || typeof record.ts !== "string") {
|
|
4485
|
+
throw new Error("This task has lost the message it was about.");
|
|
4486
|
+
}
|
|
4487
|
+
return {
|
|
4488
|
+
channel: record.channel,
|
|
4489
|
+
ts: record.ts,
|
|
4490
|
+
user: typeof record.user === "string" ? record.user : void 0,
|
|
4491
|
+
text: typeof record.text === "string" ? record.text : "",
|
|
4492
|
+
threadTs: typeof record.threadTs === "string" ? record.threadTs : record.ts
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4495
|
+
function asCursor(value) {
|
|
4496
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
4497
|
+
const out = {};
|
|
4498
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4499
|
+
if (typeof entry === "string") out[key] = entry;
|
|
4500
|
+
}
|
|
4501
|
+
return out;
|
|
4502
|
+
}
|
|
4503
|
+
function summarise2(text3) {
|
|
4504
|
+
const line = text3.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
4505
|
+
if (!line) return "Slack message";
|
|
4506
|
+
return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
4507
|
+
}
|
|
4508
|
+
function isAsk(input) {
|
|
4509
|
+
if (input.user === input.botUserId || input.messageBotId && input.messageBotId === input.botId) {
|
|
4510
|
+
return false;
|
|
4511
|
+
}
|
|
4512
|
+
if (input.isIm) return Boolean(input.text.trim());
|
|
4513
|
+
return input.text.includes(`<@${input.botUserId}>`);
|
|
4514
|
+
}
|
|
4515
|
+
function destination(target, source) {
|
|
4516
|
+
if (typeof target.channel === "string" && target.channel.trim()) {
|
|
4517
|
+
return { channel: target.channel.trim() };
|
|
4518
|
+
}
|
|
4519
|
+
if (source.connectorId !== "slack") {
|
|
4520
|
+
throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
|
|
4521
|
+
}
|
|
4522
|
+
const carry = source.carry ?? {};
|
|
4523
|
+
if (!carry.channel) throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
|
|
4524
|
+
return { channel: carry.channel, threadTs: carry.ts };
|
|
4525
|
+
}
|
|
4526
|
+
function accountOf2(credential) {
|
|
4527
|
+
return {
|
|
4528
|
+
id: credential.teamId,
|
|
4529
|
+
label: credential.team || credential.teamId,
|
|
4530
|
+
url: credential.teamUrl || void 0
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
var slackRuntime = {
|
|
4534
|
+
async readiness() {
|
|
4535
|
+
return { ready: true };
|
|
4536
|
+
},
|
|
4537
|
+
async poll({ workflowId, credential, cursor }) {
|
|
4538
|
+
if (workflowId !== "slack.ask") {
|
|
4539
|
+
throw new Error(`Slack cannot watch for ${workflowId}.`);
|
|
4540
|
+
}
|
|
4541
|
+
const account2 = credential;
|
|
4542
|
+
const previous = asCursor(cursor);
|
|
4543
|
+
const channels = await listConversations(account2);
|
|
4544
|
+
const next = { ...previous };
|
|
4545
|
+
const signals2 = [];
|
|
4546
|
+
for (const channel2 of channels) {
|
|
4547
|
+
const oldest = previous[channel2.id];
|
|
4548
|
+
const messages = await history(account2, channel2.id, oldest);
|
|
4549
|
+
if (messages.length === 0) {
|
|
4550
|
+
if (!next[channel2.id]) next[channel2.id] = String(Date.now() / 1e3);
|
|
4551
|
+
continue;
|
|
4552
|
+
}
|
|
4553
|
+
next[channel2.id] = messages[messages.length - 1].ts;
|
|
4554
|
+
if (!oldest) continue;
|
|
4555
|
+
for (const message of messages) {
|
|
4556
|
+
if (!isAsk({
|
|
4557
|
+
text: message.text,
|
|
4558
|
+
user: message.user,
|
|
4559
|
+
messageBotId: message.botId,
|
|
4560
|
+
isIm: channel2.isIm,
|
|
4561
|
+
botUserId: account2.botUserId,
|
|
4562
|
+
botId: account2.botId
|
|
4563
|
+
})) {
|
|
4564
|
+
continue;
|
|
4565
|
+
}
|
|
4566
|
+
const asked = {
|
|
4567
|
+
channel: channel2.id,
|
|
4568
|
+
ts: message.ts,
|
|
4569
|
+
user: message.user,
|
|
4570
|
+
text: message.text,
|
|
4571
|
+
threadTs: message.threadTs ?? message.ts
|
|
4572
|
+
};
|
|
4573
|
+
signals2.push({
|
|
4574
|
+
key: `${channel2.id}:${message.ts}`,
|
|
4575
|
+
kind: "message",
|
|
4576
|
+
ref: `${channel2.id}:${message.ts}`,
|
|
4577
|
+
title: summarise2(message.text),
|
|
4578
|
+
url: permalink2(account2, channel2.id, message.ts),
|
|
4579
|
+
payload: asked
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
return { signals: signals2, cursor: next };
|
|
4584
|
+
},
|
|
4585
|
+
async resolveWorkItem({ payload }) {
|
|
4586
|
+
const asked = asAsked(payload);
|
|
4587
|
+
return {
|
|
4588
|
+
kind: "message",
|
|
4589
|
+
ref: `${asked.channel}:${asked.ts}`,
|
|
4590
|
+
title: summarise2(asked.text),
|
|
4591
|
+
url: "",
|
|
4592
|
+
carry: { channel: asked.channel, ts: asked.threadTs },
|
|
4593
|
+
context: [
|
|
4594
|
+
{
|
|
4595
|
+
name: "MESSAGE.md",
|
|
4596
|
+
body: [`# What you were asked`, ``, asked.text, ``].join("\n")
|
|
4597
|
+
}
|
|
4598
|
+
]
|
|
4599
|
+
};
|
|
4600
|
+
},
|
|
4601
|
+
async applyAction({ actionId, target, source, body, credential }) {
|
|
4602
|
+
if (actionId !== "slack.reply") throw new Error(`Slack cannot ${actionId}.`);
|
|
4603
|
+
const account2 = credential;
|
|
4604
|
+
const to = destination(target, source);
|
|
4605
|
+
const posted = await postMessage(account2, {
|
|
4606
|
+
channel: to.channel,
|
|
4607
|
+
text: body.trim(),
|
|
4608
|
+
threadTs: to.threadTs
|
|
4609
|
+
});
|
|
4610
|
+
return { url: permalink2(account2, posted.channel, posted.ts) };
|
|
4611
|
+
},
|
|
4612
|
+
auth: {
|
|
4613
|
+
async connectWithFields(fields) {
|
|
4614
|
+
const botToken = fields.botToken?.trim() ?? "";
|
|
4615
|
+
if (!botToken.startsWith("xoxb-")) {
|
|
4616
|
+
throw new Error("Paste the Bot User OAuth Token from your Slack app. It starts with xoxb-.");
|
|
4617
|
+
}
|
|
4618
|
+
const credential = await authTest(botToken);
|
|
4619
|
+
return { credential, account: accountOf2(credential) };
|
|
4620
|
+
},
|
|
4621
|
+
async identity(credential) {
|
|
4622
|
+
const stored = credential;
|
|
4623
|
+
const fresh = await authTest(stored.botToken);
|
|
4624
|
+
return { account: accountOf2(fresh) };
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
};
|
|
4628
|
+
|
|
4629
|
+
// src/connectors/feishu/api.ts
|
|
4630
|
+
var API2 = {
|
|
4631
|
+
feishu: "https://open.feishu.cn/open-apis",
|
|
4632
|
+
lark: "https://open.larksuite.com/open-apis"
|
|
4633
|
+
};
|
|
4634
|
+
var APPLINK = {
|
|
4635
|
+
feishu: "https://applink.feishu.cn/client/chat/open",
|
|
4636
|
+
lark: "https://applink.larksuite.com/client/chat/open"
|
|
4637
|
+
};
|
|
4638
|
+
function parseHost(value) {
|
|
4639
|
+
const raw = (value ?? "feishu").trim().toLowerCase();
|
|
4640
|
+
if (!raw || raw === "feishu" || raw === "feishu.cn" || raw.includes("feishu.cn")) return "feishu";
|
|
4641
|
+
if (raw === "lark" || raw === "larksuite" || raw.includes("larksuite.com") || raw.includes("larkoffice.com")) {
|
|
4642
|
+
return "lark";
|
|
4643
|
+
}
|
|
4644
|
+
throw new Error("Open platform must be feishu (China) or lark (international).");
|
|
4645
|
+
}
|
|
4646
|
+
function refuse2(code, msg) {
|
|
4647
|
+
if (code === 10003 || code === 10012 || code === 10014) {
|
|
4648
|
+
throw new Error("Feishu did not accept those app credentials.");
|
|
4649
|
+
}
|
|
4650
|
+
throw new Error(msg ? `Feishu refused: ${msg}` : "Feishu refused the request.");
|
|
4651
|
+
}
|
|
4652
|
+
async function call(host, path, init = {}) {
|
|
4653
|
+
const url = new URL(`${API2[host]}${path}`);
|
|
4654
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
4655
|
+
url.searchParams.set(key, value);
|
|
4656
|
+
}
|
|
4657
|
+
let response;
|
|
4658
|
+
try {
|
|
4659
|
+
response = await fetch(url.href, {
|
|
4660
|
+
method: init.method ?? "GET",
|
|
4661
|
+
headers: {
|
|
4662
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
4663
|
+
...init.token ? { Authorization: `Bearer ${init.token}` } : {}
|
|
4664
|
+
},
|
|
4665
|
+
body: init.body ? JSON.stringify(init.body) : void 0
|
|
4666
|
+
});
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
throw new TransientError(`Feishu is unreachable: ${error.message}`);
|
|
4669
|
+
}
|
|
4670
|
+
if (response.status >= 500 || response.status === 429) {
|
|
4671
|
+
throw new TransientError(`Feishu answered ${response.status}`);
|
|
4672
|
+
}
|
|
4673
|
+
let body;
|
|
4674
|
+
try {
|
|
4675
|
+
body = await response.json();
|
|
4676
|
+
} catch {
|
|
4677
|
+
throw new Error(`Feishu answered ${response.status}`);
|
|
4678
|
+
}
|
|
4679
|
+
return body;
|
|
4680
|
+
}
|
|
4681
|
+
function dataOf(body) {
|
|
4682
|
+
if (body.code && body.code !== 0) refuse2(body.code, body.msg);
|
|
4683
|
+
return body.data ?? {};
|
|
4684
|
+
}
|
|
4685
|
+
async function tenantAccessToken(host, appId, appSecret) {
|
|
4686
|
+
const body = await call(host, "/auth/v3/tenant_access_token/internal", {
|
|
4687
|
+
method: "POST",
|
|
4688
|
+
body: { app_id: appId, app_secret: appSecret }
|
|
4689
|
+
});
|
|
4690
|
+
if (body.code && body.code !== 0) refuse2(body.code, body.msg);
|
|
4691
|
+
const token = body.tenant_access_token ?? "";
|
|
4692
|
+
const expire = typeof body.expire === "number" ? body.expire : 7200;
|
|
4693
|
+
if (!token) throw new Error("Feishu did not return a tenant token.");
|
|
4694
|
+
return { token, expiresAt: Date.now() + expire * 1e3 - 6e4 };
|
|
4695
|
+
}
|
|
4696
|
+
async function botInfo(credential) {
|
|
4697
|
+
const body = await call(credential.host, "/bot/v3/info", { token: credential.tenantAccessToken });
|
|
4698
|
+
if (body.code && body.code !== 0) refuse2(body.code, body.msg);
|
|
4699
|
+
const bot = body.bot ?? {};
|
|
4700
|
+
const openId = typeof bot.open_id === "string" ? bot.open_id : "";
|
|
4701
|
+
const name = typeof bot.app_name === "string" ? bot.app_name : "";
|
|
4702
|
+
const avatarUrl = typeof bot.avatar_url === "string" ? bot.avatar_url : void 0;
|
|
4703
|
+
if (!openId) throw new Error("Feishu did not return which bot this app is.");
|
|
4704
|
+
return { openId, name, ...avatarUrl ? { avatarUrl } : {} };
|
|
4705
|
+
}
|
|
4706
|
+
async function listChats(credential) {
|
|
4707
|
+
const chats = [];
|
|
4708
|
+
let pageToken = "";
|
|
4709
|
+
do {
|
|
4710
|
+
const query2 = { page_size: "100" };
|
|
4711
|
+
if (pageToken) query2.page_token = pageToken;
|
|
4712
|
+
const body = await call(credential.host, "/im/v1/chats", {
|
|
4713
|
+
token: credential.tenantAccessToken,
|
|
4714
|
+
query: query2
|
|
4715
|
+
});
|
|
4716
|
+
const data = dataOf(body);
|
|
4717
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
4718
|
+
for (const raw of items) {
|
|
4719
|
+
const chat2 = raw;
|
|
4720
|
+
if (!chat2.chat_id) continue;
|
|
4721
|
+
chats.push({ id: chat2.chat_id, name: chat2.name ?? "" });
|
|
4722
|
+
}
|
|
4723
|
+
pageToken = data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
|
|
4724
|
+
} while (pageToken);
|
|
4725
|
+
return chats;
|
|
4726
|
+
}
|
|
4727
|
+
function textOf(content) {
|
|
4728
|
+
if (!content) return "";
|
|
4729
|
+
try {
|
|
4730
|
+
const parsed = JSON.parse(content);
|
|
4731
|
+
return typeof parsed.text === "string" ? parsed.text : content;
|
|
4732
|
+
} catch {
|
|
4733
|
+
return content;
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
function mentionsOf(raw) {
|
|
4737
|
+
if (!Array.isArray(raw)) return [];
|
|
4738
|
+
return raw.map((entry) => {
|
|
4739
|
+
const mention = entry;
|
|
4740
|
+
return typeof mention.id === "string" ? mention.id : "";
|
|
4741
|
+
}).filter(Boolean);
|
|
4742
|
+
}
|
|
4743
|
+
async function history2(credential, chatId, oldestMs) {
|
|
4744
|
+
const parsed = [];
|
|
4745
|
+
let pageToken = "";
|
|
4746
|
+
do {
|
|
4747
|
+
const query2 = {
|
|
4748
|
+
container_id_type: "chat",
|
|
4749
|
+
container_id: chatId,
|
|
4750
|
+
sort_type: oldestMs ? "ByCreateTimeAsc" : "ByCreateTimeDesc",
|
|
4751
|
+
page_size: oldestMs ? "50" : "1"
|
|
4752
|
+
};
|
|
4753
|
+
if (oldestMs) query2.start_time = String(Math.floor(Number(oldestMs) / 1e3));
|
|
4754
|
+
if (pageToken) query2.page_token = pageToken;
|
|
4755
|
+
const body = await call(credential.host, "/im/v1/messages", {
|
|
4756
|
+
token: credential.tenantAccessToken,
|
|
4757
|
+
query: query2
|
|
4758
|
+
});
|
|
4759
|
+
if (body.code && body.code !== 0) {
|
|
4760
|
+
if (body.code === 230002 || body.code === 231203) return parsed;
|
|
4761
|
+
refuse2(body.code, body.msg);
|
|
4762
|
+
}
|
|
4763
|
+
const data = body.data ?? {};
|
|
4764
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
4765
|
+
for (const raw of items) {
|
|
4766
|
+
const message = raw;
|
|
4767
|
+
if (!message.message_id || message.deleted) continue;
|
|
4768
|
+
parsed.push({
|
|
4769
|
+
id: message.message_id,
|
|
4770
|
+
chatId: message.chat_id ?? chatId,
|
|
4771
|
+
createTime: message.create_time ?? "",
|
|
4772
|
+
text: textOf(message.body?.content),
|
|
4773
|
+
...message.sender?.id ? { senderId: message.sender.id } : {},
|
|
4774
|
+
...message.sender?.sender_type ? { senderType: message.sender.sender_type } : {},
|
|
4775
|
+
mentionIds: mentionsOf(message.mentions)
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
pageToken = oldestMs && data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
|
|
4779
|
+
} while (pageToken);
|
|
4780
|
+
return parsed.sort((a, b) => Number(a.createTime) - Number(b.createTime));
|
|
4781
|
+
}
|
|
4782
|
+
async function replyMessage(credential, messageId, text3) {
|
|
4783
|
+
const body = await call(credential.host, `/im/v1/messages/${messageId}/reply`, {
|
|
4784
|
+
token: credential.tenantAccessToken,
|
|
4785
|
+
method: "POST",
|
|
4786
|
+
body: { msg_type: "text", content: JSON.stringify({ text: text3 }) }
|
|
4787
|
+
});
|
|
4788
|
+
const data = dataOf(body);
|
|
4789
|
+
const id = typeof data.message_id === "string" ? data.message_id : "";
|
|
4790
|
+
const chatId = typeof data.chat_id === "string" ? data.chat_id : "";
|
|
4791
|
+
if (!id) throw new Error("Feishu accepted the reply but did not say where it landed.");
|
|
4792
|
+
return { id, chatId };
|
|
4793
|
+
}
|
|
4794
|
+
async function sendMessage(credential, chatId, text3) {
|
|
4795
|
+
const body = await call(credential.host, "/im/v1/messages", {
|
|
4796
|
+
token: credential.tenantAccessToken,
|
|
4797
|
+
method: "POST",
|
|
4798
|
+
query: { receive_id_type: "chat_id" },
|
|
4799
|
+
body: { receive_id: chatId, msg_type: "text", content: JSON.stringify({ text: text3 }) }
|
|
4800
|
+
});
|
|
4801
|
+
const data = dataOf(body);
|
|
4802
|
+
const id = typeof data.message_id === "string" ? data.message_id : "";
|
|
4803
|
+
const landed = typeof data.chat_id === "string" ? data.chat_id : chatId;
|
|
4804
|
+
if (!id) throw new Error("Feishu accepted the message but did not say where it landed.");
|
|
4805
|
+
return { id, chatId: landed };
|
|
4806
|
+
}
|
|
4807
|
+
function permalink3(host, chatId) {
|
|
4808
|
+
return `${APPLINK[host]}?openChatId=${chatId}`;
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
// src/connectors/feishu/runtime.ts
|
|
4812
|
+
function asAsked2(value) {
|
|
4813
|
+
const record = value;
|
|
4814
|
+
if (!record || typeof record.chatId !== "string" || typeof record.messageId !== "string") {
|
|
4815
|
+
throw new Error("This task has lost the message it was about.");
|
|
4816
|
+
}
|
|
4817
|
+
return {
|
|
4818
|
+
chatId: record.chatId,
|
|
4819
|
+
messageId: record.messageId,
|
|
4820
|
+
user: typeof record.user === "string" ? record.user : void 0,
|
|
4821
|
+
text: typeof record.text === "string" ? record.text : ""
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
function asCursor2(value) {
|
|
4825
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
4826
|
+
const out = {};
|
|
4827
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4828
|
+
if (typeof entry === "string") out[key] = entry;
|
|
4829
|
+
}
|
|
4830
|
+
return out;
|
|
4831
|
+
}
|
|
4832
|
+
function summarise3(text3) {
|
|
4833
|
+
const line = text3.replace(/@_user_\d+/g, "").replace(/\s+/g, " ").trim();
|
|
4834
|
+
if (!line) return "Feishu message";
|
|
4835
|
+
return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
4836
|
+
}
|
|
4837
|
+
function isAsk2(input) {
|
|
4838
|
+
if (input.senderType === "app" || input.senderId === input.botOpenId) return false;
|
|
4839
|
+
return input.mentionIds.includes(input.botOpenId);
|
|
4840
|
+
}
|
|
4841
|
+
function destination2(target, source) {
|
|
4842
|
+
if (typeof target.chat === "string" && target.chat.trim()) {
|
|
4843
|
+
return { chatId: target.chat.trim() };
|
|
4844
|
+
}
|
|
4845
|
+
if (source.connectorId !== "feishu") {
|
|
4846
|
+
throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
|
|
4847
|
+
}
|
|
4848
|
+
const carry = source.carry ?? {};
|
|
4849
|
+
if (!carry.chatId && !carry.messageId) {
|
|
4850
|
+
throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
|
|
4851
|
+
}
|
|
4852
|
+
return { chatId: carry.chatId, messageId: carry.messageId };
|
|
4853
|
+
}
|
|
4854
|
+
function accountOf3(credential) {
|
|
4855
|
+
return {
|
|
4856
|
+
id: credential.botOpenId,
|
|
4857
|
+
label: credential.botName || credential.appId,
|
|
4858
|
+
...credential.avatarUrl ? { avatarUrl: credential.avatarUrl } : {}
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
async function usable(credential) {
|
|
4862
|
+
if (Date.now() < credential.expiresAt) return { credential };
|
|
4863
|
+
const minted = await tenantAccessToken(credential.host, credential.appId, credential.appSecret);
|
|
4864
|
+
const refreshed = {
|
|
4865
|
+
...credential,
|
|
4866
|
+
tenantAccessToken: minted.token,
|
|
4867
|
+
expiresAt: minted.expiresAt
|
|
4868
|
+
};
|
|
4869
|
+
return { credential: refreshed, refreshed };
|
|
4870
|
+
}
|
|
4871
|
+
var feishuRuntime = {
|
|
4872
|
+
async readiness() {
|
|
4873
|
+
return { ready: true };
|
|
4874
|
+
},
|
|
4875
|
+
async poll({ workflowId, credential, cursor }) {
|
|
4876
|
+
if (workflowId !== "feishu.ask") {
|
|
4877
|
+
throw new Error(`Feishu cannot watch for ${workflowId}.`);
|
|
4878
|
+
}
|
|
4879
|
+
const { credential: account2 } = await usable(credential);
|
|
4880
|
+
const previous = asCursor2(cursor);
|
|
4881
|
+
const chats = await listChats(account2);
|
|
4882
|
+
const next = { ...previous };
|
|
4883
|
+
const signals2 = [];
|
|
4884
|
+
for (const chat2 of chats) {
|
|
4885
|
+
const oldest = previous[chat2.id];
|
|
4886
|
+
const messages = await history2(account2, chat2.id, oldest);
|
|
4887
|
+
if (messages.length === 0) {
|
|
4888
|
+
if (!next[chat2.id]) next[chat2.id] = String(Date.now());
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4891
|
+
next[chat2.id] = messages[messages.length - 1].createTime;
|
|
4892
|
+
if (!oldest) continue;
|
|
4893
|
+
for (const message of messages) {
|
|
4894
|
+
if (oldest && Number(message.createTime) <= Number(oldest)) continue;
|
|
4895
|
+
if (!isAsk2({
|
|
4896
|
+
senderId: message.senderId,
|
|
4897
|
+
senderType: message.senderType,
|
|
4898
|
+
mentionIds: message.mentionIds,
|
|
4899
|
+
botOpenId: account2.botOpenId
|
|
4900
|
+
})) {
|
|
4901
|
+
continue;
|
|
4902
|
+
}
|
|
4903
|
+
const asked = {
|
|
4904
|
+
chatId: message.chatId,
|
|
4905
|
+
messageId: message.id,
|
|
4906
|
+
user: message.senderId,
|
|
4907
|
+
text: message.text
|
|
4908
|
+
};
|
|
4909
|
+
signals2.push({
|
|
4910
|
+
key: `${message.chatId}:${message.id}`,
|
|
4911
|
+
kind: "message",
|
|
4912
|
+
ref: message.id,
|
|
4913
|
+
title: summarise3(message.text),
|
|
4914
|
+
url: permalink3(account2.host, message.chatId),
|
|
4915
|
+
payload: asked
|
|
4916
|
+
});
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
return { signals: signals2, cursor: next };
|
|
4920
|
+
},
|
|
4921
|
+
async resolveWorkItem({ payload }) {
|
|
4922
|
+
const asked = asAsked2(payload);
|
|
4923
|
+
return {
|
|
4924
|
+
kind: "message",
|
|
4925
|
+
ref: asked.messageId,
|
|
4926
|
+
title: summarise3(asked.text),
|
|
4927
|
+
url: "",
|
|
4928
|
+
carry: { chatId: asked.chatId, messageId: asked.messageId },
|
|
4929
|
+
context: [
|
|
4930
|
+
{
|
|
4931
|
+
name: "MESSAGE.md",
|
|
4932
|
+
body: [`# What you were asked`, ``, asked.text, ``].join("\n")
|
|
4933
|
+
}
|
|
4934
|
+
]
|
|
4935
|
+
};
|
|
4936
|
+
},
|
|
4937
|
+
async applyAction({ actionId, target, source, body, credential }) {
|
|
4938
|
+
if (actionId !== "feishu.reply") throw new Error(`Feishu cannot ${actionId}.`);
|
|
4939
|
+
const { credential: account2 } = await usable(credential);
|
|
4940
|
+
const to = destination2(target, source);
|
|
4941
|
+
const text3 = body.trim();
|
|
4942
|
+
const posted = to.messageId && source.connectorId === "feishu" ? await replyMessage(account2, to.messageId, text3) : await sendMessage(account2, to.chatId ?? "", text3);
|
|
4943
|
+
return { url: permalink3(account2.host, posted.chatId || to.chatId || "") };
|
|
4944
|
+
},
|
|
4945
|
+
auth: {
|
|
4946
|
+
async connectWithFields(fields) {
|
|
4947
|
+
const appId = fields.appId?.trim() ?? "";
|
|
4948
|
+
const appSecret = fields.appSecret?.trim() ?? "";
|
|
4949
|
+
if (!appId) throw new Error("Paste the App ID from your Feishu or Lark app.");
|
|
4950
|
+
if (!appSecret) throw new Error("Paste the App Secret from your Feishu or Lark app.");
|
|
4951
|
+
const host = parseHost(fields.host);
|
|
4952
|
+
const minted = await tenantAccessToken(host, appId, appSecret);
|
|
4953
|
+
const bot = await botInfo({ host, tenantAccessToken: minted.token });
|
|
4954
|
+
const stored = {
|
|
4955
|
+
appId,
|
|
4956
|
+
appSecret,
|
|
4957
|
+
host,
|
|
4958
|
+
botOpenId: bot.openId,
|
|
4959
|
+
botName: bot.name,
|
|
4960
|
+
...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {},
|
|
4961
|
+
tenantAccessToken: minted.token,
|
|
4962
|
+
expiresAt: minted.expiresAt
|
|
4963
|
+
};
|
|
4964
|
+
return { credential: stored, account: accountOf3(stored) };
|
|
4965
|
+
},
|
|
4966
|
+
async identity(credential) {
|
|
4967
|
+
const stored = credential;
|
|
4968
|
+
const { credential: fresh, refreshed } = await usable(stored);
|
|
4969
|
+
const bot = await botInfo(fresh);
|
|
4970
|
+
const updated = {
|
|
4971
|
+
...fresh,
|
|
4972
|
+
botOpenId: bot.openId,
|
|
4973
|
+
botName: bot.name,
|
|
4974
|
+
...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {}
|
|
4975
|
+
};
|
|
4976
|
+
return {
|
|
4977
|
+
account: accountOf3(updated),
|
|
4978
|
+
...refreshed ? { renewedCredential: updated } : {}
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
};
|
|
4983
|
+
|
|
4206
4984
|
// src/connectors/wechat/runtime.ts
|
|
4207
4985
|
import { randomUUID } from "node:crypto";
|
|
4208
4986
|
|
|
@@ -4227,7 +5005,7 @@ function postHeaders(token) {
|
|
|
4227
5005
|
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
4228
5006
|
};
|
|
4229
5007
|
}
|
|
4230
|
-
async function
|
|
5008
|
+
async function call2(url, init) {
|
|
4231
5009
|
let response;
|
|
4232
5010
|
try {
|
|
4233
5011
|
response = await fetch(url, init);
|
|
@@ -4241,7 +5019,7 @@ async function call(url, init) {
|
|
|
4241
5019
|
return await response.json();
|
|
4242
5020
|
}
|
|
4243
5021
|
async function requestQrCode() {
|
|
4244
|
-
const body = await
|
|
5022
|
+
const body = await call2(
|
|
4245
5023
|
`${WECHAT_API}/ilink/bot/get_bot_qrcode?bot_type=3`,
|
|
4246
5024
|
{
|
|
4247
5025
|
method: "POST",
|
|
@@ -4261,7 +5039,7 @@ function absolute(host) {
|
|
|
4261
5039
|
async function qrCodeStatus(qrcode, host) {
|
|
4262
5040
|
let body;
|
|
4263
5041
|
try {
|
|
4264
|
-
body = await
|
|
5042
|
+
body = await call2(
|
|
4265
5043
|
`${absolute(host)}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
|
|
4266
5044
|
{ headers: APP_HEADERS }
|
|
4267
5045
|
);
|
|
@@ -4314,7 +5092,7 @@ function refusal(body) {
|
|
|
4314
5092
|
return body.errmsg || `WeChat refused the request (${bad}).`;
|
|
4315
5093
|
}
|
|
4316
5094
|
async function notifyStart(credential) {
|
|
4317
|
-
const body = await
|
|
5095
|
+
const body = await call2(`${credential.baseUrl}/ilink/bot/msg/notifystart`, {
|
|
4318
5096
|
method: "POST",
|
|
4319
5097
|
headers: postHeaders(credential.botToken),
|
|
4320
5098
|
body: JSON.stringify({ base_info: BASE_INFO })
|
|
@@ -4324,7 +5102,7 @@ async function notifyStart(credential) {
|
|
|
4324
5102
|
}
|
|
4325
5103
|
var FROM_USER = 1;
|
|
4326
5104
|
var TEXT_ITEM = 1;
|
|
4327
|
-
function
|
|
5105
|
+
function textOf2(message) {
|
|
4328
5106
|
return (message.item_list ?? []).filter((item) => item.type === TEXT_ITEM).map((item) => item.text_item?.text?.trim()).filter((text3) => Boolean(text3)).join("\n").trim();
|
|
4329
5107
|
}
|
|
4330
5108
|
function isFromPerson(message) {
|
|
@@ -4336,7 +5114,7 @@ async function getUpdates(credential, cursor) {
|
|
|
4336
5114
|
const timer = setTimeout(() => abort.abort(), WAIT_MS);
|
|
4337
5115
|
let body;
|
|
4338
5116
|
try {
|
|
4339
|
-
body = await
|
|
5117
|
+
body = await call2(`${credential.baseUrl}/ilink/bot/getupdates`, {
|
|
4340
5118
|
method: "POST",
|
|
4341
5119
|
headers: postHeaders(credential.botToken),
|
|
4342
5120
|
body: JSON.stringify({ get_updates_buf: cursor, base_info: BASE_INFO }),
|
|
@@ -4352,8 +5130,8 @@ async function getUpdates(credential, cursor) {
|
|
|
4352
5130
|
if (refused) throw new Error(refused);
|
|
4353
5131
|
return { messages: body.msgs ?? [], cursor: body.get_updates_buf || cursor };
|
|
4354
5132
|
}
|
|
4355
|
-
async function
|
|
4356
|
-
const body = await
|
|
5133
|
+
async function sendMessage2(credential, input) {
|
|
5134
|
+
const body = await call2(`${credential.baseUrl}/ilink/bot/sendmessage`, {
|
|
4357
5135
|
method: "POST",
|
|
4358
5136
|
headers: postHeaders(credential.botToken),
|
|
4359
5137
|
body: JSON.stringify({
|
|
@@ -4388,7 +5166,7 @@ function asAttempt(value) {
|
|
|
4388
5166
|
}
|
|
4389
5167
|
return { qrcode: record.qrcode, host: record.host };
|
|
4390
5168
|
}
|
|
4391
|
-
function
|
|
5169
|
+
function asAsked3(value) {
|
|
4392
5170
|
const record = value;
|
|
4393
5171
|
if (!record || typeof record.fromUserId !== "string" || typeof record.text !== "string") {
|
|
4394
5172
|
throw new Error("This task has lost the message it was about.");
|
|
@@ -4400,7 +5178,7 @@ function asAsked(value) {
|
|
|
4400
5178
|
text: record.text
|
|
4401
5179
|
};
|
|
4402
5180
|
}
|
|
4403
|
-
function
|
|
5181
|
+
function summarise4(text3) {
|
|
4404
5182
|
const line = text3.split("\n")[0].trim();
|
|
4405
5183
|
return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
4406
5184
|
}
|
|
@@ -4427,7 +5205,7 @@ function recipientFor(target, source, account2) {
|
|
|
4427
5205
|
if (!carry.toUserId) throw new Error("There is nobody to reply to on this task.");
|
|
4428
5206
|
return { userId: carry.toUserId, contextToken: carry.contextToken };
|
|
4429
5207
|
}
|
|
4430
|
-
function
|
|
5208
|
+
function accountOf4(credential) {
|
|
4431
5209
|
return {
|
|
4432
5210
|
id: credential.botId || credential.userId,
|
|
4433
5211
|
label: credential.botId ? `WeChat bot ${credential.botId}` : "WeChat bot"
|
|
@@ -4455,7 +5233,7 @@ var wechatRuntime = {
|
|
|
4455
5233
|
const signals2 = [];
|
|
4456
5234
|
for (const message of messages) {
|
|
4457
5235
|
if (!isFromPerson(message)) continue;
|
|
4458
|
-
const text3 =
|
|
5236
|
+
const text3 = textOf2(message);
|
|
4459
5237
|
if (!text3) continue;
|
|
4460
5238
|
const fromUserId = message.from_user_id ?? "";
|
|
4461
5239
|
const asked = {
|
|
@@ -4468,7 +5246,7 @@ var wechatRuntime = {
|
|
|
4468
5246
|
key: `msg#${asked.messageId}`,
|
|
4469
5247
|
kind: "message",
|
|
4470
5248
|
ref: `msg#${asked.messageId}`,
|
|
4471
|
-
title:
|
|
5249
|
+
title: summarise4(text3),
|
|
4472
5250
|
// A message in a chat is not somewhere a browser can be sent.
|
|
4473
5251
|
url: "",
|
|
4474
5252
|
// Held rather than dropped, so a message that was sent and never
|
|
@@ -4481,11 +5259,11 @@ var wechatRuntime = {
|
|
|
4481
5259
|
return { signals: signals2, cursor: next };
|
|
4482
5260
|
},
|
|
4483
5261
|
async resolveWorkItem({ payload }) {
|
|
4484
|
-
const asked =
|
|
5262
|
+
const asked = asAsked3(payload);
|
|
4485
5263
|
return {
|
|
4486
5264
|
kind: "message",
|
|
4487
5265
|
ref: `msg#${asked.messageId}`,
|
|
4488
|
-
title:
|
|
5266
|
+
title: summarise4(asked.text),
|
|
4489
5267
|
url: "",
|
|
4490
5268
|
carry: { toUserId: asked.fromUserId, contextToken: asked.contextToken },
|
|
4491
5269
|
context: [
|
|
@@ -4501,7 +5279,7 @@ var wechatRuntime = {
|
|
|
4501
5279
|
const account2 = credential;
|
|
4502
5280
|
const to = recipientFor(target, source, account2);
|
|
4503
5281
|
for (const part of chunked(body)) {
|
|
4504
|
-
await
|
|
5282
|
+
await sendMessage2(account2, {
|
|
4505
5283
|
toUserId: to.userId,
|
|
4506
5284
|
contextToken: to.contextToken,
|
|
4507
5285
|
// Unique per piece, so a retry cannot post half an answer twice.
|
|
@@ -4527,7 +5305,7 @@ var wechatRuntime = {
|
|
|
4527
5305
|
case "confirmed":
|
|
4528
5306
|
return {
|
|
4529
5307
|
state: "confirmed",
|
|
4530
|
-
result: { credential: status.credential, account:
|
|
5308
|
+
result: { credential: status.credential, account: accountOf4(status.credential) }
|
|
4531
5309
|
};
|
|
4532
5310
|
case "expired":
|
|
4533
5311
|
return { state: "expired", reason: status.reason };
|
|
@@ -4542,7 +5320,7 @@ var wechatRuntime = {
|
|
|
4542
5320
|
async identity(credential) {
|
|
4543
5321
|
const stored = credential;
|
|
4544
5322
|
await notifyStart(stored);
|
|
4545
|
-
return { account:
|
|
5323
|
+
return { account: accountOf4(stored) };
|
|
4546
5324
|
}
|
|
4547
5325
|
}
|
|
4548
5326
|
};
|
|
@@ -4550,6 +5328,8 @@ var wechatRuntime = {
|
|
|
4550
5328
|
// src/connectors/runtimes.ts
|
|
4551
5329
|
var RUNTIMES = {
|
|
4552
5330
|
github: githubRuntime,
|
|
5331
|
+
slack: slackRuntime,
|
|
5332
|
+
feishu: feishuRuntime,
|
|
4553
5333
|
gmail: gmailRuntime,
|
|
4554
5334
|
wechat: wechatRuntime,
|
|
4555
5335
|
schedule: scheduleRuntime
|
|
@@ -8629,13 +9409,13 @@ function migrationsFolder() {
|
|
|
8629
9409
|
join3(here, "../../../drizzle"),
|
|
8630
9410
|
join3(process.cwd(), "drizzle")
|
|
8631
9411
|
];
|
|
8632
|
-
return candidates2.find((
|
|
9412
|
+
return candidates2.find((folder5) => existsSync2(folder5)) ?? join3(process.cwd(), "drizzle");
|
|
8633
9413
|
}
|
|
8634
9414
|
async function migrateIfNeeded() {
|
|
8635
9415
|
if (process.env.LOOPABLE_SKIP_MIGRATE === "1") return;
|
|
8636
|
-
const
|
|
8637
|
-
if (!existsSync2(
|
|
8638
|
-
await migrate(db(), { migrationsFolder:
|
|
9416
|
+
const folder5 = migrationsFolder();
|
|
9417
|
+
if (!existsSync2(folder5)) return;
|
|
9418
|
+
await migrate(db(), { migrationsFolder: folder5 });
|
|
8639
9419
|
}
|
|
8640
9420
|
function rowsChanged(result) {
|
|
8641
9421
|
return Number(result.rowsAffected ?? result.changes ?? 0);
|