@rynx-ai/plugin-channel-lark 0.1.10 → 0.1.11-beta.10
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/.rynx-plugin/plugin.json +25 -0
- package/dist/cli-mirror/mirror-index.d.ts +4 -2
- package/dist/cli-mirror/mirror-index.js +10 -2
- package/dist/host-adapters.d.ts +26 -13
- package/dist/host-adapters.js +146 -44
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/lark-config-store.d.ts +30 -0
- package/dist/lark-config-store.js +135 -0
- package/dist/lark-runtime-state-store.d.ts +18 -0
- package/dist/lark-runtime-state-store.js +95 -0
- package/dist/lark-setup.d.ts +27 -5
- package/dist/lark-setup.js +24 -7
- package/dist/lark.d.ts +9 -2
- package/dist/lark.js +45 -80
- package/dist/runtime.d.ts +5 -8
- package/dist/runtime.js +131312 -141
- package/dist/settings.d.ts +7 -0
- package/dist/{runtime.mjs → settings.js} +6631 -10535
- package/package.json +27 -10
- package/ui/assets/index-CGUVAMsv.css +1 -0
- package/ui/assets/index-D8Amoe6a.js +201 -0
- package/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/ui/index.html +13 -0
- package/rynx-plugin.json +0 -42
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const INSTANCE_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
|
|
5
|
+
export class LarkConfigStore {
|
|
6
|
+
file;
|
|
7
|
+
constructor(dataDir) {
|
|
8
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
9
|
+
this.file = path.join(dataDir, "lark-config.json");
|
|
10
|
+
}
|
|
11
|
+
list() {
|
|
12
|
+
return structuredClone(this.read().instances);
|
|
13
|
+
}
|
|
14
|
+
get(id) {
|
|
15
|
+
return this.list().find((instance) => instance.id === id);
|
|
16
|
+
}
|
|
17
|
+
upsert(input) {
|
|
18
|
+
const state = this.read();
|
|
19
|
+
const id = input.id ?? `instance-${randomUUID().slice(0, 8)}`;
|
|
20
|
+
if (!INSTANCE_ID_PATTERN.test(id))
|
|
21
|
+
throw new Error("Lark instance id is invalid");
|
|
22
|
+
const existing = state.instances.find((instance) => instance.id === id);
|
|
23
|
+
const secret = input.appSecret?.trim() || existing?.appSecret;
|
|
24
|
+
if (!secret)
|
|
25
|
+
throw new Error("App Secret is required");
|
|
26
|
+
const appId = input.appId.trim();
|
|
27
|
+
const name = input.name.trim();
|
|
28
|
+
if (!appId)
|
|
29
|
+
throw new Error("App ID is required");
|
|
30
|
+
if (!name)
|
|
31
|
+
throw new Error("instance name is required");
|
|
32
|
+
const defaultAgent = input.defaultAgent === undefined
|
|
33
|
+
? existing?.defaultAgent
|
|
34
|
+
: input.defaultAgent?.trim() || undefined;
|
|
35
|
+
const roots = input.allowedRoots ?? existing?.executionDefaults.allowedRoots ?? [];
|
|
36
|
+
if (!Array.isArray(roots) || roots.some((root) => typeof root !== "string" || !path.isAbsolute(root))) {
|
|
37
|
+
throw new Error("allowed roots must be absolute paths");
|
|
38
|
+
}
|
|
39
|
+
const instance = {
|
|
40
|
+
id,
|
|
41
|
+
name,
|
|
42
|
+
enabled: input.enabled,
|
|
43
|
+
appId,
|
|
44
|
+
appSecret: secret,
|
|
45
|
+
...(defaultAgent ? { defaultAgent } : {}),
|
|
46
|
+
executionDefaults: {
|
|
47
|
+
runtime: input.runtime ?? existing?.executionDefaults.runtime ?? "codex",
|
|
48
|
+
allowedRoots: [...new Set(roots.map((root) => path.resolve(root)))],
|
|
49
|
+
...(defaultAgent ? { defaultAgent } : {}),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const index = state.instances.findIndex((item) => item.id === id);
|
|
53
|
+
if (index >= 0)
|
|
54
|
+
state.instances[index] = instance;
|
|
55
|
+
else
|
|
56
|
+
state.instances.push(instance);
|
|
57
|
+
this.write(state);
|
|
58
|
+
return structuredClone(instance);
|
|
59
|
+
}
|
|
60
|
+
remove(id) {
|
|
61
|
+
const state = this.read();
|
|
62
|
+
const filtered = state.instances.filter((instance) => instance.id !== id);
|
|
63
|
+
if (filtered.length === state.instances.length)
|
|
64
|
+
return false;
|
|
65
|
+
this.write({ ...state, instances: filtered });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
setEnabled(id, enabled) {
|
|
69
|
+
const state = this.read();
|
|
70
|
+
const instance = state.instances.find((item) => item.id === id);
|
|
71
|
+
if (!instance)
|
|
72
|
+
throw new Error(`Lark instance "${id}" not found`);
|
|
73
|
+
instance.enabled = enabled;
|
|
74
|
+
this.write(state);
|
|
75
|
+
return structuredClone(instance);
|
|
76
|
+
}
|
|
77
|
+
read() {
|
|
78
|
+
if (!existsSync(this.file))
|
|
79
|
+
return { schemaVersion: 1, instances: [] };
|
|
80
|
+
const info = lstatSync(this.file);
|
|
81
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) {
|
|
82
|
+
throw new Error("Lark plugin configuration is invalid");
|
|
83
|
+
}
|
|
84
|
+
return parseState(JSON.parse(readFileSync(this.file, "utf8")));
|
|
85
|
+
}
|
|
86
|
+
write(state) {
|
|
87
|
+
const temporary = `${this.file}.tmp-${randomUUID()}`;
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
mode: 0o600,
|
|
92
|
+
flag: "wx",
|
|
93
|
+
});
|
|
94
|
+
chmodSync(temporary, 0o600);
|
|
95
|
+
renameSync(temporary, this.file);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
rmSync(temporary, { force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function parseState(value) {
|
|
103
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
104
|
+
throw new Error("Lark plugin configuration must be an object");
|
|
105
|
+
}
|
|
106
|
+
const record = value;
|
|
107
|
+
if (record.schemaVersion !== 1 || !Array.isArray(record.instances)) {
|
|
108
|
+
throw new Error("Lark plugin configuration schema is invalid");
|
|
109
|
+
}
|
|
110
|
+
const ids = new Set();
|
|
111
|
+
const instances = record.instances.map((value) => {
|
|
112
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
113
|
+
throw new Error("Lark instance configuration is invalid");
|
|
114
|
+
}
|
|
115
|
+
const instance = value;
|
|
116
|
+
if (!INSTANCE_ID_PATTERN.test(instance.id) ||
|
|
117
|
+
typeof instance.name !== "string" || !instance.name ||
|
|
118
|
+
typeof instance.enabled !== "boolean" ||
|
|
119
|
+
typeof instance.appId !== "string" || !instance.appId ||
|
|
120
|
+
typeof instance.appSecret !== "string" || !instance.appSecret ||
|
|
121
|
+
(instance.defaultAgent !== undefined &&
|
|
122
|
+
(typeof instance.defaultAgent !== "string" || !instance.defaultAgent.trim())) ||
|
|
123
|
+
!instance.executionDefaults ||
|
|
124
|
+
!["codex", "traex", "claude"].includes(instance.executionDefaults.runtime) ||
|
|
125
|
+
!Array.isArray(instance.executionDefaults.allowedRoots) ||
|
|
126
|
+
!instance.executionDefaults.allowedRoots.every((root) => typeof root === "string" && path.isAbsolute(root))) {
|
|
127
|
+
throw new Error("Lark instance configuration is invalid");
|
|
128
|
+
}
|
|
129
|
+
if (ids.has(instance.id))
|
|
130
|
+
throw new Error(`duplicate Lark instance id "${instance.id}"`);
|
|
131
|
+
ids.add(instance.id);
|
|
132
|
+
return structuredClone(instance);
|
|
133
|
+
});
|
|
134
|
+
return { schemaVersion: 1, instances };
|
|
135
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PluginHealth } from "@rynx-ai/plugin-sdk";
|
|
2
|
+
export interface LarkRuntimeInstanceState {
|
|
3
|
+
state: "starting" | "running" | "failed" | "stopping" | "stopped";
|
|
4
|
+
connected: boolean;
|
|
5
|
+
lastError: string | null;
|
|
6
|
+
lastEventAt: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface LarkRuntimeState {
|
|
9
|
+
active: boolean;
|
|
10
|
+
health: PluginHealth;
|
|
11
|
+
instances: Record<string, LarkRuntimeInstanceState>;
|
|
12
|
+
}
|
|
13
|
+
export declare class LarkRuntimeStateStore {
|
|
14
|
+
private readonly file;
|
|
15
|
+
constructor(dataDir: string);
|
|
16
|
+
read(): LarkRuntimeState;
|
|
17
|
+
write(state: LarkRuntimeState): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
export class LarkRuntimeStateStore {
|
|
5
|
+
file;
|
|
6
|
+
constructor(dataDir) {
|
|
7
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
8
|
+
this.file = path.join(dataDir, "lark-runtime-state.json");
|
|
9
|
+
}
|
|
10
|
+
read() {
|
|
11
|
+
if (!existsSync(this.file)) {
|
|
12
|
+
return { active: false, health: { state: "running" }, instances: {} };
|
|
13
|
+
}
|
|
14
|
+
const info = lstatSync(this.file);
|
|
15
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) {
|
|
16
|
+
return {
|
|
17
|
+
active: false,
|
|
18
|
+
health: { state: "degraded", message: "Runtime state is unavailable" },
|
|
19
|
+
instances: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return parseState(JSON.parse(readFileSync(this.file, "utf8")));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {
|
|
27
|
+
active: false,
|
|
28
|
+
health: { state: "degraded", message: "Runtime state is unavailable" },
|
|
29
|
+
instances: {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
write(state) {
|
|
34
|
+
const temporary = `${this.file}.tmp-${randomUUID()}`;
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(temporary, `${JSON.stringify(state)}\n`, {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
mode: 0o600,
|
|
39
|
+
flag: "wx",
|
|
40
|
+
});
|
|
41
|
+
chmodSync(temporary, 0o600);
|
|
42
|
+
renameSync(temporary, this.file);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rmSync(temporary, { force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseState(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
51
|
+
throw new Error("invalid runtime state");
|
|
52
|
+
}
|
|
53
|
+
const record = value;
|
|
54
|
+
if (typeof record.active !== "boolean")
|
|
55
|
+
throw new Error("invalid runtime activity");
|
|
56
|
+
const health = parseHealth(record.health);
|
|
57
|
+
if (typeof record.instances !== "object" || record.instances === null ||
|
|
58
|
+
Array.isArray(record.instances)) {
|
|
59
|
+
throw new Error("invalid runtime instances");
|
|
60
|
+
}
|
|
61
|
+
const instances = {};
|
|
62
|
+
for (const [id, raw] of Object.entries(record.instances)) {
|
|
63
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
64
|
+
throw new Error("invalid runtime instance");
|
|
65
|
+
}
|
|
66
|
+
const item = raw;
|
|
67
|
+
if (!["starting", "running", "failed", "stopping", "stopped"].includes(String(item.state)) ||
|
|
68
|
+
typeof item.connected !== "boolean" ||
|
|
69
|
+
(item.lastError !== null && typeof item.lastError !== "string") ||
|
|
70
|
+
(item.lastEventAt !== null && typeof item.lastEventAt !== "string")) {
|
|
71
|
+
throw new Error("invalid runtime instance");
|
|
72
|
+
}
|
|
73
|
+
instances[id] = {
|
|
74
|
+
state: item.state,
|
|
75
|
+
connected: item.connected,
|
|
76
|
+
lastError: item.lastError,
|
|
77
|
+
lastEventAt: item.lastEventAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { active: record.active, health, instances };
|
|
81
|
+
}
|
|
82
|
+
function parseHealth(value) {
|
|
83
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
84
|
+
throw new Error("invalid runtime health");
|
|
85
|
+
}
|
|
86
|
+
const record = value;
|
|
87
|
+
if ((record.state !== "running" && record.state !== "degraded") ||
|
|
88
|
+
(record.message !== undefined && typeof record.message !== "string")) {
|
|
89
|
+
throw new Error("invalid runtime health");
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
state: record.state,
|
|
93
|
+
...(typeof record.message === "string" ? { message: record.message } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/lark-setup.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
export interface LarkSetupField {
|
|
2
|
+
key: "LARK_APP_ID" | "LARK_APP_SECRET";
|
|
3
|
+
label: string;
|
|
4
|
+
type: "string" | "secret";
|
|
5
|
+
required: boolean;
|
|
6
|
+
help?: string;
|
|
7
|
+
}
|
|
8
|
+
export type LarkAuthorizationMaterial = {
|
|
9
|
+
kind: "qr";
|
|
10
|
+
url: string;
|
|
11
|
+
expireIn?: number;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "link";
|
|
14
|
+
url: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "status";
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
export interface LarkAuthorizationCredentials {
|
|
21
|
+
LARK_APP_ID: string;
|
|
22
|
+
LARK_APP_SECRET: string;
|
|
23
|
+
botName?: string;
|
|
24
|
+
}
|
|
2
25
|
/**
|
|
3
26
|
* Minimal tenant (app-identity) scopes the Lark channel needs: receive/send
|
|
4
27
|
* messages, add reactions, read chat + members, upload/download resources, and
|
|
@@ -8,10 +31,9 @@ export declare const LARK_BOT_SCOPES: string[];
|
|
|
8
31
|
/** The bot only needs to be told when a message arrives. */
|
|
9
32
|
export declare const LARK_BOT_EVENTS: string[];
|
|
10
33
|
/** Config mode 1: paste an existing app's credentials. */
|
|
11
|
-
export declare const larkConfigSchema:
|
|
34
|
+
export declare const larkConfigSchema: LarkSetupField[];
|
|
12
35
|
/**
|
|
13
|
-
*
|
|
14
|
-
* host renders as a QR and/or a clickable link) and `status` updates, then
|
|
36
|
+
* Scan-to-create/authorize. Emits a `qr` material and `status` updates, then
|
|
15
37
|
* resolves with the App ID / App Secret to persist. Aborts via `signal`.
|
|
16
38
|
*/
|
|
17
|
-
export declare function authorizeLark(emit: (material:
|
|
39
|
+
export declare function authorizeLark(emit: (material: LarkAuthorizationMaterial) => void, signal: AbortSignal): Promise<LarkAuthorizationCredentials>;
|
package/dist/lark-setup.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lark (Feishu) authorization flow + its credential form schema.
|
|
3
3
|
*
|
|
4
|
-
* Two
|
|
4
|
+
* Two setup modes the Lark plugin can offer from its own UI or command:
|
|
5
5
|
* 1. **Form** ({@link larkConfigSchema}) — paste an existing app's App ID /
|
|
6
|
-
* App Secret.
|
|
6
|
+
* App Secret.
|
|
7
7
|
* 2. **Authorize** ({@link authorizeLark}) — the SDK's `registerApp` device
|
|
8
8
|
* flow (RFC 8628-shaped, Feishu-proprietary provisioning): the user scans a
|
|
9
9
|
* QR (or opens the link) in Feishu, an app is created/authorized with the
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* App ID / App Secret.
|
|
12
12
|
*
|
|
13
13
|
* `authorizeLark` is transport-neutral: it translates the SDK's callbacks into
|
|
14
|
-
* the
|
|
15
|
-
*
|
|
14
|
+
* the plugin's own {@link LarkAuthorizationMaterial} stream and resolves with
|
|
15
|
+
* the values to persist.
|
|
16
16
|
*/
|
|
17
17
|
import { registerApp } from "@larksuiteoapi/node-sdk";
|
|
18
|
+
import { createLarkSdkClient } from "./lark-sdk/client.js";
|
|
18
19
|
/**
|
|
19
20
|
* Minimal tenant (app-identity) scopes the Lark channel needs: receive/send
|
|
20
21
|
* messages, add reactions, read chat + members, upload/download resources, and
|
|
@@ -50,8 +51,7 @@ const STATUS_TEXT = {
|
|
|
50
51
|
domain_switched: "识别到国际版租户,已切换到 larksuite.com 继续。",
|
|
51
52
|
};
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
-
* host renders as a QR and/or a clickable link) and `status` updates, then
|
|
54
|
+
* Scan-to-create/authorize. Emits a `qr` material and `status` updates, then
|
|
55
55
|
* resolves with the App ID / App Secret to persist. Aborts via `signal`.
|
|
56
56
|
*/
|
|
57
57
|
export async function authorizeLark(emit, signal) {
|
|
@@ -75,7 +75,24 @@ export async function authorizeLark(emit, signal) {
|
|
|
75
75
|
if (!result.client_id || !result.client_secret) {
|
|
76
76
|
throw new Error("扫码返回的凭据为空,请改用手动填写 App ID / App Secret。");
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
const botName = await resolveBotName(result.client_id, result.client_secret);
|
|
79
|
+
return {
|
|
80
|
+
LARK_APP_ID: result.client_id,
|
|
81
|
+
LARK_APP_SECRET: result.client_secret,
|
|
82
|
+
...(botName ? { botName } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function resolveBotName(appId, appSecret) {
|
|
86
|
+
try {
|
|
87
|
+
const response = await createLarkSdkClient({ appId, appSecret }).request({
|
|
88
|
+
method: "GET",
|
|
89
|
+
url: "/open-apis/bot/v3/info",
|
|
90
|
+
});
|
|
91
|
+
return response?.bot?.app_name?.trim() || undefined;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
79
96
|
}
|
|
80
97
|
function messageOf(err) {
|
|
81
98
|
return err instanceof Error ? err.message : String(err);
|
package/dist/lark.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { chunkLarkReplyText, extractLarkTextPrompt, isLarkNewSessionCommand, isL
|
|
|
4
4
|
import { type LarkConversationDirectory } from "./lark-conversation-directory.js";
|
|
5
5
|
import { LarkClientProvider, LarkSdkMessageSender } from "./lark-sender.js";
|
|
6
6
|
import { type LarkActiveSessionStore } from "./lark-session-store.js";
|
|
7
|
-
import type { ConversationProvider,
|
|
7
|
+
import type { ConversationProvider, ConversationStreamRequest, StartedLiveExecution } from "@rynx-ai/core";
|
|
8
8
|
export { chunkLarkReplyText, extractLarkTextPrompt, isLarkNewSessionCommand, isLarkStopCommand, };
|
|
9
9
|
export { LarkClientProvider, LarkSdkMessageSender };
|
|
10
10
|
export interface LarkUserIdentifier {
|
|
@@ -234,7 +234,12 @@ export interface LarkWsClientProvider {
|
|
|
234
234
|
start(options: LarkWsClientStartOptions): Promise<void>;
|
|
235
235
|
stop(): Promise<void>;
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
/** Plugin-facing run contract. The daemon-backed adapter sends only the
|
|
238
|
+
* provisioned Session id and message over Host RPC; the daemon loads that
|
|
239
|
+
* Session's immutable workspace/execution snapshots. */
|
|
240
|
+
export interface LarkConversationRuntime {
|
|
241
|
+
runLive(input: ConversationStreamRequest): Promise<StartedLiveExecution>;
|
|
242
|
+
}
|
|
238
243
|
interface LarkMessageIngestorOptions {
|
|
239
244
|
conversationRuntime: LarkConversationRuntime;
|
|
240
245
|
messageSender: LarkMessageSender;
|
|
@@ -278,6 +283,7 @@ interface LarkMessageIngestorOptions {
|
|
|
278
283
|
sessionProvisioner?: (input: {
|
|
279
284
|
sessionKey: string;
|
|
280
285
|
fromSessionId: string;
|
|
286
|
+
title?: string;
|
|
281
287
|
}) => Promise<string>;
|
|
282
288
|
}
|
|
283
289
|
export interface LarkAgentCatalog {
|
|
@@ -484,6 +490,7 @@ interface LocalLarkBotRuntimeOptions {
|
|
|
484
490
|
sessionProvisioner?: (input: {
|
|
485
491
|
sessionKey: string;
|
|
486
492
|
fromSessionId: string;
|
|
493
|
+
title?: string;
|
|
487
494
|
}) => Promise<string>;
|
|
488
495
|
}
|
|
489
496
|
export declare class LocalLarkBotRuntime implements LarkBotRuntime {
|
package/dist/lark.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { mkdir } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { promisify } from "node:util";
|
|
6
5
|
import { CodexRuntimeError, collectLiveRun, } from "@rynx-ai/core";
|
|
@@ -16,7 +15,6 @@ import { buildSettingsCard, buildSettingsCardJson, buildSettingsResultCard, SETT
|
|
|
16
15
|
import { getRuntimeProfile, isAgentRuntimeId, RUNTIME_PROFILES, } from "@rynx-ai/core";
|
|
17
16
|
import { SteerController } from "@rynx-ai/core";
|
|
18
17
|
import { toErrorMessage } from "@rynx-ai/core";
|
|
19
|
-
import { findInnermostAllowedRoot } from "@rynx-ai/core";
|
|
20
18
|
export { chunkLarkReplyText, extractLarkTextPrompt, isLarkNewSessionCommand, isLarkStopCommand, };
|
|
21
19
|
export { LarkClientProvider, LarkSdkMessageSender };
|
|
22
20
|
const DEFAULT_UNSUPPORTED_MESSAGE = "暂时只支持文本消息,请发送文本内容。";
|
|
@@ -38,7 +36,7 @@ const LARK_COMMANDS = [
|
|
|
38
36
|
{ name: "models", help: "/models — 列出当前运行时可用模型", category: "info" },
|
|
39
37
|
{ name: "resume", help: "/resume [短id] — 列出最近会话/跳转或转发到指定会话", category: "takeover" },
|
|
40
38
|
{ name: "fork", help: "/fork — 把当前会话 fork 到新群/新话题(带上下文+历史记录)", category: "takeover" },
|
|
41
|
-
{ name: "cwd", help: "/cwd
|
|
39
|
+
{ name: "cwd", help: "/cwd — 工作目录已改由 Rynx Project 管理", category: "other" },
|
|
42
40
|
{ name: "goal", help: "/goal [目标|clear] — 查看/设置/清除当前会话目标", category: "action" },
|
|
43
41
|
{ name: "diff", help: "/diff — 显示当前工作区 git 改动(含未跟踪文件)", category: "action" },
|
|
44
42
|
{ name: "help", help: "/help — 显示本帮助", category: "info" },
|
|
@@ -64,8 +62,7 @@ const defaultLogger = {
|
|
|
64
62
|
}));
|
|
65
63
|
},
|
|
66
64
|
};
|
|
67
|
-
/** Route SDK diagnostics through the
|
|
68
|
-
* reserved for the NDJSON RPC transport in an out-of-process plugin. */
|
|
65
|
+
/** Route SDK diagnostics through the host logger; the process transport owns stdout. */
|
|
69
66
|
function larkSdkLogger(logger) {
|
|
70
67
|
const write = (level, messages) => {
|
|
71
68
|
const text = messages
|
|
@@ -502,6 +499,14 @@ export class LarkMessageIngestor {
|
|
|
502
499
|
// → fall through to the queue so the message runs as the next turn.
|
|
503
500
|
}
|
|
504
501
|
}
|
|
502
|
+
// /new must execute immediately after interrupting the active turn. If it
|
|
503
|
+
// joins the per-session queue, it waits behind the very turn it just
|
|
504
|
+
// aborted; a backend that is slow to close its stream can otherwise leave
|
|
505
|
+
// the command with no acknowledgement and no rotated session.
|
|
506
|
+
if (isLarkNewSessionCommand(prompt)) {
|
|
507
|
+
await this.handleNewSessionCommand(event, sessionKey);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
505
510
|
// Informational commands (/help, /models, /search) answer instantly and
|
|
506
511
|
// also bypass the queue so they respond even while a turn is running.
|
|
507
512
|
if (command && LARK_INFO_COMMANDS.has(command.name)) {
|
|
@@ -524,9 +529,9 @@ export class LarkMessageIngestor {
|
|
|
524
529
|
await this.handleForkCommand(event, sessionKey);
|
|
525
530
|
return;
|
|
526
531
|
}
|
|
527
|
-
// /cwd
|
|
532
|
+
// /cwd is retained only as a migration notice; Project owns workspace selection.
|
|
528
533
|
if (command && command.name === "cwd") {
|
|
529
|
-
await this.handleCwdCommand(event
|
|
534
|
+
await this.handleCwdCommand(event);
|
|
530
535
|
return;
|
|
531
536
|
}
|
|
532
537
|
// /runtime and /model open the interactive settings card; bypass queue.
|
|
@@ -540,10 +545,6 @@ export class LarkMessageIngestor {
|
|
|
540
545
|
return;
|
|
541
546
|
}
|
|
542
547
|
await this.queue.enqueue(sessionKey, async () => {
|
|
543
|
-
if (isLarkNewSessionCommand(prompt)) {
|
|
544
|
-
await this.handleNewSessionCommand(event, sessionKey);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
548
|
// Best-effort, off the turn's critical path: the directory only feeds
|
|
548
549
|
// /resume, so a lagging or failed write must never delay the reply.
|
|
549
550
|
void this.recordConversation(event, sessionKey, prompt);
|
|
@@ -559,11 +560,10 @@ export class LarkMessageIngestor {
|
|
|
559
560
|
session_id: session.sessionId,
|
|
560
561
|
provider: this.provider,
|
|
561
562
|
});
|
|
563
|
+
// A session title is initialized from its first ordinary message and
|
|
564
|
+
// remains stable for the rest of that session.
|
|
565
|
+
this.registerSession(session.sessionId, prompt);
|
|
562
566
|
}
|
|
563
|
-
// Register the session's identity row (idempotent): the first turn fixes
|
|
564
|
-
// its source + title; rotated (`/new`) and forked sessions land here on
|
|
565
|
-
// their first turn too, so the control plane lists every Lark session.
|
|
566
|
-
this.registerSession(session.sessionId, prompt);
|
|
567
567
|
this.logMessageReceived(event, sessionKey, session.sessionId);
|
|
568
568
|
const startedReactionId = await this.tryCreateReaction(event, sessionKey, LARK_WORK_STARTED_REACTION, session.sessionId);
|
|
569
569
|
const reply = await this.buildReply(event, sessionKey, session.sessionId, prompt, session.cwd ?? this.cwd, await this.resolveTurnExecution(sessionKey, session));
|
|
@@ -810,39 +810,8 @@ export class LarkMessageIngestor {
|
|
|
810
810
|
});
|
|
811
811
|
}
|
|
812
812
|
}
|
|
813
|
-
async handleCwdCommand(event
|
|
814
|
-
|
|
815
|
-
const currentCwd = session?.cwd ?? this.cwd;
|
|
816
|
-
const arg = command.args.trim();
|
|
817
|
-
if (!arg) {
|
|
818
|
-
await this.sendReplyText(event, currentCwd ? `当前工作目录:${currentCwd}` : "当前使用默认工作目录。");
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
const base = currentCwd ?? this.allowedRoots[0] ?? process.cwd();
|
|
822
|
-
const resolved = path.isAbsolute(arg) ? path.resolve(arg) : path.resolve(base, arg);
|
|
823
|
-
if (!findInnermostAllowedRoot(resolved, this.allowedRoots)) {
|
|
824
|
-
await this.sendReplyText(event, `目录不在允许范围内(需位于 AGENT_ALLOWED_DIRS 内):${resolved}`);
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
try {
|
|
828
|
-
await mkdir(resolved, { recursive: true });
|
|
829
|
-
}
|
|
830
|
-
catch (error) {
|
|
831
|
-
await this.sendReplyText(event, `创建目录失败:${toErrorMessage(error)}`);
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
await this.activeSessionStore.bindCwd(sessionKey, resolved);
|
|
835
|
-
await this.sendReplyText(event, `已绑定工作目录:${resolved},并开启新会话。`);
|
|
836
|
-
this.logger.log({
|
|
837
|
-
event: "command.handled",
|
|
838
|
-
tenant_key: event.tenant_key ?? event.sender.tenant_key,
|
|
839
|
-
chat_id: event.message.chat_id,
|
|
840
|
-
thread_id: event.message.thread_id,
|
|
841
|
-
message_id: event.message.message_id,
|
|
842
|
-
session_key: sessionKey,
|
|
843
|
-
command: "cwd",
|
|
844
|
-
provider: this.provider,
|
|
845
|
-
});
|
|
813
|
+
async handleCwdCommand(event) {
|
|
814
|
+
await this.sendReplyText(event, "工作目录已改由 Rynx Project 管理;请在 Rynx 中创建或选择 Project。未选择时会使用该 Session 的独立临时目录。");
|
|
846
815
|
}
|
|
847
816
|
async handleResumeCommand(event, sessionKey, command) {
|
|
848
817
|
const arg = command.args.trim();
|
|
@@ -955,8 +924,7 @@ export class LarkMessageIngestor {
|
|
|
955
924
|
}
|
|
956
925
|
async handleForkCommand(event, sessionKey) {
|
|
957
926
|
const sender = this.messageSender;
|
|
958
|
-
if (!
|
|
959
|
-
!sender.sendText ||
|
|
927
|
+
if (!sender.sendText ||
|
|
960
928
|
!sender.listMessages ||
|
|
961
929
|
!sender.listChatMembers) {
|
|
962
930
|
await this.sendReplyText(event, "当前不支持 /fork(仅 app_server 后端 + 飞书机器人能力)。");
|
|
@@ -1006,6 +974,22 @@ export class LarkMessageIngestor {
|
|
|
1006
974
|
}
|
|
1007
975
|
const header = `📋 原会话「${baseName}」记录(fork):`;
|
|
1008
976
|
const body = transcript ? `${header}\n${transcript}` : header;
|
|
977
|
+
if (!this.sessionProvisioner) {
|
|
978
|
+
await this.sendReplyText(event, LARK_BACKEND_UNSUPPORTED);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
let newLocalThreadId;
|
|
982
|
+
try {
|
|
983
|
+
newLocalThreadId = await this.sessionProvisioner({
|
|
984
|
+
sessionKey: `fork:${tenantKey}:${event.message.message_id}`,
|
|
985
|
+
fromSessionId: session.sessionId,
|
|
986
|
+
title: `${baseName} - fork`,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
await this.sendReplyText(event, `fork 会话创建失败:${toErrorMessage(error)}`);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
1009
993
|
let newSessionKey;
|
|
1010
994
|
let newChatId = null;
|
|
1011
995
|
try {
|
|
@@ -1041,31 +1025,16 @@ export class LarkMessageIngestor {
|
|
|
1041
1025
|
await this.sendReplyText(event, `fork 失败:${toErrorMessage(error)}`);
|
|
1042
1026
|
return;
|
|
1043
1027
|
}
|
|
1044
|
-
// Bind the new conversation to
|
|
1045
|
-
//
|
|
1046
|
-
// The session id is opaque (no routing baked in); newSessionKey is the
|
|
1047
|
-
// routing key the store maps to it.
|
|
1048
|
-
if (!this.sessionProvisioner) {
|
|
1049
|
-
await this.sendReplyText(event, LARK_BACKEND_UNSUPPORTED);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
let newLocalThreadId;
|
|
1053
|
-
let forkResult;
|
|
1028
|
+
// Bind the new external conversation to the already-committed Machine
|
|
1029
|
+
// Session fork. Feishu group/topic creation remains channel-owned.
|
|
1054
1030
|
try {
|
|
1055
|
-
newLocalThreadId = await this.sessionProvisioner({
|
|
1056
|
-
sessionKey: newSessionKey,
|
|
1057
|
-
fromSessionId: session.sessionId,
|
|
1058
|
-
});
|
|
1059
1031
|
await this.activeSessionStore.set(newSessionKey, newLocalThreadId, session.cwd);
|
|
1060
|
-
forkResult = await this.capabilities.forkSession(session.sessionId, newLocalThreadId);
|
|
1061
1032
|
}
|
|
1062
1033
|
catch (error) {
|
|
1063
|
-
await this.sendReplyText(event, `fork
|
|
1034
|
+
await this.sendReplyText(event, `fork 已创建,但飞书会话绑定失败:${toErrorMessage(error)}`);
|
|
1064
1035
|
return;
|
|
1065
1036
|
}
|
|
1066
|
-
const contextNote =
|
|
1067
|
-
? "已带上当前会话上下文。"
|
|
1068
|
-
: `但未能带上上下文:${this.capabilityErrorText(forkResult)}`;
|
|
1037
|
+
const contextNote = "已带上当前会话上下文。";
|
|
1069
1038
|
// Surface a one-tap entry into the new conversation in the current chat.
|
|
1070
1039
|
if (newChatId) {
|
|
1071
1040
|
const entryCardJson = buildOpenChatCardJson(newChatId, `${baseName} - fork`);
|
|
@@ -1082,9 +1051,6 @@ export class LarkMessageIngestor {
|
|
|
1082
1051
|
if (!sentCard) {
|
|
1083
1052
|
await this.sendReplyText(event, `已 fork 到新群「${baseName} - fork」,点此进入:${buildChatApplink(newChatId)}(${contextNote})`);
|
|
1084
1053
|
}
|
|
1085
|
-
else if (!forkResult.ok) {
|
|
1086
|
-
await this.sendReplyText(event, `提示:${contextNote}`);
|
|
1087
|
-
}
|
|
1088
1054
|
}
|
|
1089
1055
|
else {
|
|
1090
1056
|
// Topic thread: the root message is already visible in this group.
|
|
@@ -1119,6 +1085,7 @@ export class LarkMessageIngestor {
|
|
|
1119
1085
|
includeRuntime,
|
|
1120
1086
|
currentRuntime,
|
|
1121
1087
|
currentModel,
|
|
1088
|
+
currentEffort,
|
|
1122
1089
|
runtime: currentRuntime,
|
|
1123
1090
|
model: currentModel ?? SETTINGS_DEFAULT_VALUE,
|
|
1124
1091
|
effort: currentEffort ?? SETTINGS_DEFAULT_VALUE,
|
|
@@ -1333,7 +1300,6 @@ export class LarkMessageIngestor {
|
|
|
1333
1300
|
const runtime = pending.includeRuntime && isAgentRuntimeId(pending.runtime)
|
|
1334
1301
|
? pending.runtime
|
|
1335
1302
|
: pending.currentRuntime;
|
|
1336
|
-
const runtimeChanged = runtime !== pending.currentRuntime;
|
|
1337
1303
|
const model = pending.model === SETTINGS_DEFAULT_VALUE ? undefined : pending.model;
|
|
1338
1304
|
const selectedModel = pending.models.find((candidate) => candidate.value === pending.model);
|
|
1339
1305
|
const allowedEfforts = selectedModel?.supportedEfforts?.length
|
|
@@ -1342,10 +1308,9 @@ export class LarkMessageIngestor {
|
|
|
1342
1308
|
const effort = pending.effort !== SETTINGS_DEFAULT_VALUE && allowedEfforts.includes(pending.effort)
|
|
1343
1309
|
? pending.effort
|
|
1344
1310
|
: undefined;
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
|
|
1348
|
-
const rotate = runtimeChanged;
|
|
1311
|
+
// Provider selects the immutable runtime implementation. Model and
|
|
1312
|
+
// reasoning effort are Turn settings and update the same idle Session.
|
|
1313
|
+
const rotate = runtime !== pending.currentRuntime;
|
|
1349
1314
|
await this.activeSessionStore.applySettings(sessionKey, {
|
|
1350
1315
|
runtime,
|
|
1351
1316
|
model,
|
|
@@ -1748,7 +1713,7 @@ export class LarkMessageIngestor {
|
|
|
1748
1713
|
// A user-triggered cancel needs no redundant chat reply (the card carries it).
|
|
1749
1714
|
text: cancelled ? "" : `处理失败:${errorMessage}`,
|
|
1750
1715
|
provider: started.provider,
|
|
1751
|
-
model: started.model,
|
|
1716
|
+
...(started.model ? { model: started.model } : {}),
|
|
1752
1717
|
};
|
|
1753
1718
|
}
|
|
1754
1719
|
// The card carries the answer; only emit a plain-text reply when no card
|
|
@@ -1756,7 +1721,7 @@ export class LarkMessageIngestor {
|
|
|
1756
1721
|
return {
|
|
1757
1722
|
text: orchestrator.cardStarted ? "" : orchestrator.fallbackText.trim() || DEFAULT_EMPTY_RESPONSE,
|
|
1758
1723
|
provider: started.provider,
|
|
1759
|
-
model: started.model,
|
|
1724
|
+
...(started.model ? { model: started.model } : {}),
|
|
1760
1725
|
};
|
|
1761
1726
|
}
|
|
1762
1727
|
/**
|
|
@@ -1798,7 +1763,7 @@ export class LarkMessageIngestor {
|
|
|
1798
1763
|
return {
|
|
1799
1764
|
text: answer.trim() || DEFAULT_EMPTY_RESPONSE,
|
|
1800
1765
|
provider: started.provider,
|
|
1801
|
-
model: started.model,
|
|
1766
|
+
...(started.model ? { model: started.model } : {}),
|
|
1802
1767
|
};
|
|
1803
1768
|
}
|
|
1804
1769
|
catch (error) {
|