@rynx-ai/plugin-channel-lark 0.1.9
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/cli-mirror/mirror-index.d.ts +46 -0
- package/dist/cli-mirror/mirror-index.js +48 -0
- package/dist/cli-mirror/rollout-mirror.d.ts +85 -0
- package/dist/cli-mirror/rollout-mirror.js +333 -0
- package/dist/cli-mirror/rollout-watcher.d.ts +109 -0
- package/dist/cli-mirror/rollout-watcher.js +347 -0
- package/dist/host-adapters.d.ts +82 -0
- package/dist/host-adapters.js +404 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/lark-card-stream.d.ts +363 -0
- package/dist/lark-card-stream.js +1335 -0
- package/dist/lark-content.d.ts +30 -0
- package/dist/lark-content.js +138 -0
- package/dist/lark-conversation-directory.d.ts +65 -0
- package/dist/lark-conversation-directory.js +106 -0
- package/dist/lark-diff-card.d.ts +25 -0
- package/dist/lark-diff-card.js +58 -0
- package/dist/lark-fork-transcript.d.ts +16 -0
- package/dist/lark-fork-transcript.js +141 -0
- package/dist/lark-resume-card.d.ts +31 -0
- package/dist/lark-resume-card.js +105 -0
- package/dist/lark-sdk/client.d.ts +8 -0
- package/dist/lark-sdk/client.js +32 -0
- package/dist/lark-sdk/index.d.ts +1 -0
- package/dist/lark-sdk/index.js +1 -0
- package/dist/lark-sender.d.ts +114 -0
- package/dist/lark-sender.js +323 -0
- package/dist/lark-session-store.d.ts +13 -0
- package/dist/lark-session-store.js +14 -0
- package/dist/lark-settings-card.d.ts +55 -0
- package/dist/lark-settings-card.js +158 -0
- package/dist/lark-setup.d.ts +17 -0
- package/dist/lark-setup.js +86 -0
- package/dist/lark.d.ts +500 -0
- package/dist/lark.js +2010 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.js +157 -0
- package/dist/runtime.mjs +130863 -0
- package/package.json +35 -0
- package/rynx-plugin.json +42 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Sentinel option value meaning "use the runtime/model default". */
|
|
2
|
+
export const SETTINGS_DEFAULT_VALUE = "__default__";
|
|
3
|
+
export const SETTINGS_EFFORT_VALUES = [
|
|
4
|
+
"none",
|
|
5
|
+
"minimal",
|
|
6
|
+
"low",
|
|
7
|
+
"medium",
|
|
8
|
+
"high",
|
|
9
|
+
"xhigh",
|
|
10
|
+
"max",
|
|
11
|
+
"ultra",
|
|
12
|
+
];
|
|
13
|
+
const EFFORT_LABELS = {
|
|
14
|
+
minimal: "minimal · 最小",
|
|
15
|
+
low: "low · 低",
|
|
16
|
+
medium: "medium · 中",
|
|
17
|
+
high: "high · 高",
|
|
18
|
+
xhigh: "xhigh · 极高",
|
|
19
|
+
none: "none · 关闭",
|
|
20
|
+
max: "max · 最大",
|
|
21
|
+
ultra: "ultra · 自动委派",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Effort options for the currently-selected model: the model's own
|
|
25
|
+
* `supportedEfforts` when it advertises them, otherwise the full effort set.
|
|
26
|
+
* Always prefixed with the "follow the model default" sentinel.
|
|
27
|
+
*/
|
|
28
|
+
function effortOptionsFor(models, selectedModel) {
|
|
29
|
+
const model = models.find((m) => m.value === selectedModel);
|
|
30
|
+
const supported = Array.from(new Set(model?.supportedEfforts ?? []));
|
|
31
|
+
const values = supported.length > 0 ? supported : SETTINGS_EFFORT_VALUES;
|
|
32
|
+
return [
|
|
33
|
+
{ text: plainText("默认(跟随模型)"), value: SETTINGS_DEFAULT_VALUE },
|
|
34
|
+
...values.map((value) => ({ text: plainText(EFFORT_LABELS[value] ?? value), value })),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function plainText(content) {
|
|
38
|
+
return { tag: "plain_text", content };
|
|
39
|
+
}
|
|
40
|
+
function buildSelect(field, sessionKey, includeRuntime, placeholder, options, initialValue) {
|
|
41
|
+
return {
|
|
42
|
+
tag: "select_static",
|
|
43
|
+
name: field,
|
|
44
|
+
placeholder: plainText(placeholder),
|
|
45
|
+
initial_option: initialValue,
|
|
46
|
+
options,
|
|
47
|
+
behaviors: [
|
|
48
|
+
{
|
|
49
|
+
type: "callback",
|
|
50
|
+
value: { kind: "settings_select", field, sessionKey, includeRuntime },
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function labelMarkdown(content) {
|
|
56
|
+
return { tag: "markdown", content };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the `/runtime` / `/model` settings card object: dropdowns for runtime
|
|
60
|
+
* (optional), model and reasoning effort, plus confirm / cancel buttons.
|
|
61
|
+
* Selections round-trip via `card.action.trigger` callbacks (`settings_select`
|
|
62
|
+
* per change — runtime/model changes re-render this card; `settings_apply` /
|
|
63
|
+
* `settings_cancel` on the buttons). The effort options reflect the currently
|
|
64
|
+
* selected model's supported efforts.
|
|
65
|
+
*/
|
|
66
|
+
export function buildSettingsCard(input) {
|
|
67
|
+
const { sessionKey, includeRuntime, models, selectedRuntime, selectedModel, selectedEffort } = input;
|
|
68
|
+
const elements = [];
|
|
69
|
+
elements.push(labelMarkdown(includeRuntime
|
|
70
|
+
? "选择**运行时 / 模型 / 思考强度**,点击确认后对新会话生效。"
|
|
71
|
+
: "为当前运行时选择**模型 / 思考强度**,点击确认后对新会话生效。"));
|
|
72
|
+
if (includeRuntime) {
|
|
73
|
+
const runtimeOptions = input.runtimeOptions.map((option) => ({
|
|
74
|
+
text: plainText(option.label),
|
|
75
|
+
value: option.value,
|
|
76
|
+
}));
|
|
77
|
+
elements.push(labelMarkdown("**运行时**"));
|
|
78
|
+
elements.push(buildSelect("runtime", sessionKey, includeRuntime, "选择运行时", runtimeOptions, selectedRuntime));
|
|
79
|
+
}
|
|
80
|
+
const modelOptions = [
|
|
81
|
+
{ text: plainText("跟随运行时默认"), value: SETTINGS_DEFAULT_VALUE },
|
|
82
|
+
...models.map((model) => ({
|
|
83
|
+
text: plainText(`${model.label}${model.isDefault ? " ⭐️" : ""}`),
|
|
84
|
+
value: model.value,
|
|
85
|
+
})),
|
|
86
|
+
];
|
|
87
|
+
elements.push(labelMarkdown("**模型**"));
|
|
88
|
+
elements.push(buildSelect("model", sessionKey, includeRuntime, "选择模型", modelOptions, selectedModel));
|
|
89
|
+
elements.push(labelMarkdown("**思考强度**"));
|
|
90
|
+
elements.push(buildSelect("effort", sessionKey, includeRuntime, "选择思考强度", effortOptionsFor(models, selectedModel), selectedEffort));
|
|
91
|
+
const confirmButton = {
|
|
92
|
+
tag: "button",
|
|
93
|
+
text: plainText("确认"),
|
|
94
|
+
type: "primary",
|
|
95
|
+
size: "medium",
|
|
96
|
+
behaviors: [
|
|
97
|
+
{
|
|
98
|
+
type: "callback",
|
|
99
|
+
value: { kind: "settings_apply", sessionKey, includeRuntime },
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const cancelButton = {
|
|
104
|
+
tag: "button",
|
|
105
|
+
text: plainText("取消"),
|
|
106
|
+
type: "default",
|
|
107
|
+
size: "medium",
|
|
108
|
+
behaviors: [
|
|
109
|
+
{
|
|
110
|
+
type: "callback",
|
|
111
|
+
value: { kind: "settings_cancel", sessionKey, includeRuntime },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
// `flex_mode: "none"` + auto-width columns keep the two buttons sized to
|
|
116
|
+
// their content and sitting adjacent on the left (weighted columns would
|
|
117
|
+
// stretch each button across half the card, pushing 取消 to the far right).
|
|
118
|
+
elements.push({
|
|
119
|
+
tag: "column_set",
|
|
120
|
+
flex_mode: "none",
|
|
121
|
+
horizontal_spacing: "8px",
|
|
122
|
+
columns: [
|
|
123
|
+
{ tag: "column", width: "auto", elements: [confirmButton] },
|
|
124
|
+
{ tag: "column", width: "auto", elements: [cancelButton] },
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
schema: "2.0",
|
|
129
|
+
header: {
|
|
130
|
+
title: plainText(includeRuntime ? "⚙️ 运行设置" : "⚙️ 模型设置"),
|
|
131
|
+
template: "blue",
|
|
132
|
+
},
|
|
133
|
+
body: { elements },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** String form of {@link buildSettingsCard} (for the initial card send). */
|
|
137
|
+
export function buildSettingsCardJson(input) {
|
|
138
|
+
return JSON.stringify(buildSettingsCard(input));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build a static result card shown after the user confirms or cancels the
|
|
142
|
+
* settings card — replaces the interactive form with a plain conclusion.
|
|
143
|
+
* Returned as a plain object so it can be embedded in a card-action response.
|
|
144
|
+
*/
|
|
145
|
+
export function buildSettingsResultCard(input) {
|
|
146
|
+
return {
|
|
147
|
+
schema: "2.0",
|
|
148
|
+
header: {
|
|
149
|
+
title: plainText(input.includeRuntime ? "⚙️ 运行设置" : "⚙️ 模型设置"),
|
|
150
|
+
template: input.template,
|
|
151
|
+
},
|
|
152
|
+
body: { elements: [{ tag: "markdown", content: input.content }] },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** String form of {@link buildSettingsResultCard}. */
|
|
156
|
+
export function buildSettingsResultCardJson(input) {
|
|
157
|
+
return JSON.stringify(buildSettingsResultCard(input));
|
|
158
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AuthMaterial, ConfigField } from "@rynx-ai/core";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal tenant (app-identity) scopes the Lark channel needs: receive/send
|
|
4
|
+
* messages, add reactions, read chat + members, upload/download resources, and
|
|
5
|
+
* resolve user display info. Pre-filled into the scan-confirm page; editable.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LARK_BOT_SCOPES: string[];
|
|
8
|
+
/** The bot only needs to be told when a message arrives. */
|
|
9
|
+
export declare const LARK_BOT_EVENTS: string[];
|
|
10
|
+
/** Config mode 1: paste an existing app's credentials. */
|
|
11
|
+
export declare const larkConfigSchema: ConfigField[];
|
|
12
|
+
/**
|
|
13
|
+
* Config mode 2: scan-to-create/authorize. Emits a `qr` material (the URL the
|
|
14
|
+
* host renders as a QR and/or a clickable link) and `status` updates, then
|
|
15
|
+
* resolves with the App ID / App Secret to persist. Aborts via `signal`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function authorizeLark(emit: (material: AuthMaterial) => void, signal: AbortSignal): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark (Feishu) authorization flow + its credential form schema.
|
|
3
|
+
*
|
|
4
|
+
* Two config modes the host can offer for this channel type:
|
|
5
|
+
* 1. **Form** ({@link larkConfigSchema}) — paste an existing app's App ID /
|
|
6
|
+
* App Secret. The host renders these fields generically.
|
|
7
|
+
* 2. **Authorize** ({@link authorizeLark}) — the SDK's `registerApp` device
|
|
8
|
+
* flow (RFC 8628-shaped, Feishu-proprietary provisioning): the user scans a
|
|
9
|
+
* QR (or opens the link) in Feishu, an app is created/authorized with the
|
|
10
|
+
* bot scopes + message-receive event pre-filled, and the SDK returns the
|
|
11
|
+
* App ID / App Secret.
|
|
12
|
+
*
|
|
13
|
+
* `authorizeLark` is transport-neutral: it translates the SDK's callbacks into
|
|
14
|
+
* the host's {@link AuthMaterial} stream (`qr` / `status`) and resolves with the
|
|
15
|
+
* options to persist. The terminal vs web rendering is the host's concern.
|
|
16
|
+
*/
|
|
17
|
+
import { registerApp } from "@larksuiteoapi/node-sdk";
|
|
18
|
+
/**
|
|
19
|
+
* Minimal tenant (app-identity) scopes the Lark channel needs: receive/send
|
|
20
|
+
* messages, add reactions, read chat + members, upload/download resources, and
|
|
21
|
+
* resolve user display info. Pre-filled into the scan-confirm page; editable.
|
|
22
|
+
*/
|
|
23
|
+
export const LARK_BOT_SCOPES = [
|
|
24
|
+
"im:message",
|
|
25
|
+
"im:message:send_as_bot",
|
|
26
|
+
"im:message.group_at_msg:readonly",
|
|
27
|
+
"im:message.p2p_msg:readonly",
|
|
28
|
+
"im:message.reactions:write_only",
|
|
29
|
+
"im:chat:read",
|
|
30
|
+
"im:chat.members:read",
|
|
31
|
+
"im:resource",
|
|
32
|
+
"contact:user.base:readonly",
|
|
33
|
+
];
|
|
34
|
+
/** The bot only needs to be told when a message arrives. */
|
|
35
|
+
export const LARK_BOT_EVENTS = ["im.message.receive_v1"];
|
|
36
|
+
/** Config mode 1: paste an existing app's credentials. */
|
|
37
|
+
export const larkConfigSchema = [
|
|
38
|
+
{
|
|
39
|
+
key: "LARK_APP_ID",
|
|
40
|
+
label: "App ID",
|
|
41
|
+
type: "string",
|
|
42
|
+
required: true,
|
|
43
|
+
help: "形如 cli_xxx,在开放平台「凭证与基础信息」获取",
|
|
44
|
+
},
|
|
45
|
+
{ key: "LARK_APP_SECRET", label: "App Secret", type: "secret", required: true },
|
|
46
|
+
];
|
|
47
|
+
const STATUS_TEXT = {
|
|
48
|
+
polling: "等待在飞书中扫码确认…",
|
|
49
|
+
slow_down: "服务器繁忙,正在放慢轮询…",
|
|
50
|
+
domain_switched: "识别到国际版租户,已切换到 larksuite.com 继续。",
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Config mode 2: scan-to-create/authorize. Emits a `qr` material (the URL the
|
|
54
|
+
* host renders as a QR and/or a clickable link) and `status` updates, then
|
|
55
|
+
* resolves with the App ID / App Secret to persist. Aborts via `signal`.
|
|
56
|
+
*/
|
|
57
|
+
export async function authorizeLark(emit, signal) {
|
|
58
|
+
let result;
|
|
59
|
+
try {
|
|
60
|
+
result = await registerApp({
|
|
61
|
+
source: "rynx",
|
|
62
|
+
signal,
|
|
63
|
+
// One-scan provisioning: pre-fill the scopes + event this channel needs.
|
|
64
|
+
addons: {
|
|
65
|
+
scopes: { tenant: LARK_BOT_SCOPES },
|
|
66
|
+
events: { items: { tenant: LARK_BOT_EVENTS } },
|
|
67
|
+
},
|
|
68
|
+
onQRCodeReady: ({ url, expireIn }) => emit({ kind: "qr", url, expireIn }),
|
|
69
|
+
onStatusChange: ({ status }) => emit({ kind: "status", text: STATUS_TEXT[status] ?? status }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new Error(scrubSecret(messageOf(error)));
|
|
74
|
+
}
|
|
75
|
+
if (!result.client_id || !result.client_secret) {
|
|
76
|
+
throw new Error("扫码返回的凭据为空,请改用手动填写 App ID / App Secret。");
|
|
77
|
+
}
|
|
78
|
+
return { LARK_APP_ID: result.client_id, LARK_APP_SECRET: result.client_secret };
|
|
79
|
+
}
|
|
80
|
+
function messageOf(err) {
|
|
81
|
+
return err instanceof Error ? err.message : String(err);
|
|
82
|
+
}
|
|
83
|
+
/** Defensively mask any long token-like substring so a secret can't leak. */
|
|
84
|
+
function scrubSecret(text) {
|
|
85
|
+
return text.replace(/[A-Za-z0-9_-]{30,}/g, "***");
|
|
86
|
+
}
|