@rynx-ai/daemon 0.1.10 → 0.1.11-beta.2
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/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
- package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +37 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +54 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +504 -0
- package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/index.js +7 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
- package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
- package/bundled-plugins/plugins/lark/dist/lark.js +1977 -0
- package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/runtime.js +131316 -0
- package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +126958 -0
- package/bundled-plugins/plugins/lark/package.json +57 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +1 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-D9N-AkLX.js +201 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/index.html +13 -0
- package/dist/bundled-plugins.d.ts +15 -0
- package/dist/bundled-plugins.js +134 -0
- package/dist/cli.js +26 -208
- package/dist/control-deps.js +2 -79
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +167 -57
- package/dist/db.js +145 -124
- package/dist/index-daemon.js +25 -4
- package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
- package/dist/migrations/cleanup-legacy-sessions.js +33 -15
- package/dist/official-plugins.d.ts +2 -0
- package/dist/official-plugins.js +2 -0
- package/dist/plugin-cli.d.ts +1 -3
- package/dist/plugin-cli.js +56 -45
- package/dist/plugin-host-leases.d.ts +4 -0
- package/dist/plugin-host-leases.js +7 -0
- package/dist/plugin-host-rpc.d.ts +42 -4
- package/dist/plugin-host-rpc.js +144 -34
- package/dist/plugin-installer.d.ts +24 -55
- package/dist/plugin-installer.js +202 -415
- package/dist/plugin-management-service.d.ts +30 -23
- package/dist/plugin-management-service.js +290 -105
- package/dist/plugin-marketplace.d.ts +35 -0
- package/dist/plugin-marketplace.js +299 -0
- package/dist/plugin-package.d.ts +22 -24
- package/dist/plugin-package.js +520 -571
- package/dist/plugin-skill-context.d.ts +4 -0
- package/dist/plugin-skill-context.js +35 -0
- package/dist/plugin-store.d.ts +36 -37
- package/dist/plugin-store.js +140 -92
- package/dist/plugin-supervisor.d.ts +45 -125
- package/dist/plugin-supervisor.js +516 -1053
- package/dist/pm2.js +15 -10
- package/dist/runtime-project-store.d.ts +9 -0
- package/dist/runtime-project-store.js +83 -0
- package/dist/session-fallback-workspace.d.ts +22 -0
- package/dist/session-fallback-workspace.js +120 -0
- package/dist/session-fork-store.d.ts +41 -0
- package/dist/session-fork-store.js +288 -0
- package/dist/session-launch-operations.d.ts +14 -2
- package/dist/session-launch-operations.js +34 -2
- package/dist/session-meta-store.d.ts +7 -9
- package/dist/session-meta-store.js +28 -49
- package/dist/setup.js +26 -14
- package/package.json +14 -11
- package/dist/channel-store.d.ts +0 -22
- package/dist/channel-store.js +0 -65
- package/dist/instance-store.d.ts +0 -38
- package/dist/instance-store.js +0 -85
- package/dist/plugin-install-restart.d.ts +0 -13
- package/dist/plugin-install-restart.js +0 -24
- package/dist/plugin-runtime-control.d.ts +0 -2
- package/dist/plugin-runtime-control.js +0 -37
- package/dist/registry.d.ts +0 -10
- package/dist/registry.js +0 -52
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PluginExecutionDefaults } from "@rynx-ai/plugin-sdk";
|
|
2
|
+
export interface LarkInstanceConfig {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
appId: string;
|
|
7
|
+
appSecret: string;
|
|
8
|
+
defaultAgent?: string;
|
|
9
|
+
executionDefaults: PluginExecutionDefaults;
|
|
10
|
+
}
|
|
11
|
+
export declare class LarkConfigStore {
|
|
12
|
+
private readonly file;
|
|
13
|
+
constructor(dataDir: string);
|
|
14
|
+
list(): LarkInstanceConfig[];
|
|
15
|
+
get(id: string): LarkInstanceConfig | undefined;
|
|
16
|
+
upsert(input: {
|
|
17
|
+
id?: string;
|
|
18
|
+
name: string;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
appId: string;
|
|
21
|
+
appSecret?: string;
|
|
22
|
+
defaultAgent?: string | null;
|
|
23
|
+
runtime?: PluginExecutionDefaults["runtime"];
|
|
24
|
+
allowedRoots?: string[];
|
|
25
|
+
}): LarkInstanceConfig;
|
|
26
|
+
remove(id: string): boolean;
|
|
27
|
+
setEnabled(id: string, enabled: boolean): LarkInstanceConfig;
|
|
28
|
+
private read;
|
|
29
|
+
private write;
|
|
30
|
+
}
|
|
@@ -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,30 @@
|
|
|
1
|
+
import type { LarkMessageReceiveEvent } from "./lark.js";
|
|
2
|
+
export declare function extractLarkTextPrompt(event: LarkMessageReceiveEvent): string;
|
|
3
|
+
export declare function isLarkNewSessionCommand(prompt: string): boolean;
|
|
4
|
+
export declare function isLarkStopCommand(prompt: string): boolean;
|
|
5
|
+
export interface LarkCommand {
|
|
6
|
+
/** Lower-cased command name without the leading slash, e.g. `search`. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Trimmed argument string after the command name (may be empty). */
|
|
9
|
+
args: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse a `/command [args...]` message into `{ name, args }`. Returns null for
|
|
13
|
+
* non-command text. The command name must start with a letter so bare `/` or
|
|
14
|
+
* `/123` is not treated as a command. Used for the informational slash-command
|
|
15
|
+
* registry (`/help`, `/models`, `/search`); `/new` and `/stop` keep their
|
|
16
|
+
* exact-match predicates above for backward compatibility.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseLarkCommand(prompt: string): LarkCommand | null;
|
|
19
|
+
export declare function chunkLarkReplyText(text: string, maxBytes?: number): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Wrap a turn that replies to a Lark message card so the agent receives the
|
|
22
|
+
* full card content as context. Intentionally generic: it makes no assumption
|
|
23
|
+
* about the card's purpose and names no specific skill. The agent inspects the
|
|
24
|
+
* card content and decides how to handle the request (and which skill, if any,
|
|
25
|
+
* matches).
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildLarkCardReplyPrompt({ userPrompt, cardContent, }: {
|
|
28
|
+
userPrompt: string;
|
|
29
|
+
cardContent: string;
|
|
30
|
+
}): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
const LARK_TEXT_MAX_BYTES = 120 * 1024;
|
|
3
|
+
export function extractLarkTextPrompt(event) {
|
|
4
|
+
try {
|
|
5
|
+
const parsed = JSON.parse(event.message.content);
|
|
6
|
+
let text = "";
|
|
7
|
+
if (event.message.message_type === "text") {
|
|
8
|
+
text = typeof parsed.text === "string" ? parsed.text : "";
|
|
9
|
+
}
|
|
10
|
+
if (event.message.message_type === "post") {
|
|
11
|
+
text = extractLarkPostPrompt(parsed, event.message.mentions ?? []);
|
|
12
|
+
}
|
|
13
|
+
for (const mention of event.message.mentions ?? []) {
|
|
14
|
+
text = text.replaceAll(mention.key, `@${mention.name}`);
|
|
15
|
+
}
|
|
16
|
+
return text.replace(/^(\s*@\S+\s*)+/, "").trim();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function isLarkNewSessionCommand(prompt) {
|
|
23
|
+
return prompt === "/new";
|
|
24
|
+
}
|
|
25
|
+
export function isLarkStopCommand(prompt) {
|
|
26
|
+
const normalized = prompt.trim();
|
|
27
|
+
return normalized === "/stop" || normalized === "/cancel";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse a `/command [args...]` message into `{ name, args }`. Returns null for
|
|
31
|
+
* non-command text. The command name must start with a letter so bare `/` or
|
|
32
|
+
* `/123` is not treated as a command. Used for the informational slash-command
|
|
33
|
+
* registry (`/help`, `/models`, `/search`); `/new` and `/stop` keep their
|
|
34
|
+
* exact-match predicates above for backward compatibility.
|
|
35
|
+
*/
|
|
36
|
+
export function parseLarkCommand(prompt) {
|
|
37
|
+
const trimmed = prompt.trim();
|
|
38
|
+
if (!trimmed.startsWith("/")) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const match = /^\/([a-zA-Z][\w-]*)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
42
|
+
if (!match) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return { name: match[1].toLowerCase(), args: (match[2] ?? "").trim() };
|
|
46
|
+
}
|
|
47
|
+
export function chunkLarkReplyText(text, maxBytes = LARK_TEXT_MAX_BYTES) {
|
|
48
|
+
if (!text) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) {
|
|
52
|
+
return [text];
|
|
53
|
+
}
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let current = "";
|
|
56
|
+
for (const character of text) {
|
|
57
|
+
const next = `${current}${character}`;
|
|
58
|
+
if (current && Buffer.byteLength(next, "utf8") > maxBytes) {
|
|
59
|
+
chunks.push(current);
|
|
60
|
+
current = character;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
current = next;
|
|
64
|
+
}
|
|
65
|
+
if (current) {
|
|
66
|
+
chunks.push(current);
|
|
67
|
+
}
|
|
68
|
+
return chunks;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Wrap a turn that replies to a Lark message card so the agent receives the
|
|
72
|
+
* full card content as context. Intentionally generic: it makes no assumption
|
|
73
|
+
* about the card's purpose and names no specific skill. The agent inspects the
|
|
74
|
+
* card content and decides how to handle the request (and which skill, if any,
|
|
75
|
+
* matches).
|
|
76
|
+
*/
|
|
77
|
+
export function buildLarkCardReplyPrompt({ userPrompt, cardContent, }) {
|
|
78
|
+
const trimmedPrompt = userPrompt.trim();
|
|
79
|
+
return [
|
|
80
|
+
"The user replied to a Lark message card. The full card content is included below — use it as context to handle the user's request, and if one of your skills matches this kind of card, use that skill.",
|
|
81
|
+
trimmedPrompt ? `User reply: ${trimmedPrompt}` : "User reply: (empty)",
|
|
82
|
+
"Lark card content:",
|
|
83
|
+
"```json",
|
|
84
|
+
cardContent,
|
|
85
|
+
"```",
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
function extractLarkPostPrompt(parsed, mentions) {
|
|
89
|
+
const locale = resolveLarkPostLocale(parsed);
|
|
90
|
+
if (!locale) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
const lines = [];
|
|
94
|
+
if (typeof locale.title === "string" && locale.title.trim()) {
|
|
95
|
+
lines.push(locale.title.trim());
|
|
96
|
+
}
|
|
97
|
+
for (const block of locale.content ?? []) {
|
|
98
|
+
const line = block
|
|
99
|
+
.map((item) => renderLarkPostItem(item, mentions))
|
|
100
|
+
.join("")
|
|
101
|
+
.trim();
|
|
102
|
+
if (line) {
|
|
103
|
+
lines.push(line);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
function resolveLarkPostLocale(parsed) {
|
|
109
|
+
if (Array.isArray(parsed.content)) {
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
for (const value of Object.values(parsed)) {
|
|
113
|
+
if (value &&
|
|
114
|
+
typeof value === "object" &&
|
|
115
|
+
Array.isArray(value.content)) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function renderLarkPostItem(item, mentions) {
|
|
122
|
+
if (item.tag === "at") {
|
|
123
|
+
return `@${item.user_name ?? resolveMentionName(item, mentions)}`;
|
|
124
|
+
}
|
|
125
|
+
if (typeof item.text === "string") {
|
|
126
|
+
return item.text;
|
|
127
|
+
}
|
|
128
|
+
if (item.tag === "a" && typeof item.href === "string") {
|
|
129
|
+
return item.href;
|
|
130
|
+
}
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
function resolveMentionName(item, mentions) {
|
|
134
|
+
if (item.user_name) {
|
|
135
|
+
return item.user_name;
|
|
136
|
+
}
|
|
137
|
+
return mentions.at(0)?.name ?? "用户";
|
|
138
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark adapter over the channel-agnostic @rynx-ai/core conversation directory.
|
|
3
|
+
* Keeps the Lark-facing record shape + API (so the resume card, mirror index,
|
|
4
|
+
* and ingestor are unchanged) while delegating storage + indexing to core's
|
|
5
|
+
* generic FileConversationDirectory, parameterized over Lark's routing payload
|
|
6
|
+
* (chat/thread ids). The plugin runtime supplies one explicit file per instance.
|
|
7
|
+
*/
|
|
8
|
+
import { deriveConversationShortId as coreDeriveConversationShortId } from "@rynx-ai/core";
|
|
9
|
+
/** Conversation kind, derived from the inbound message. */
|
|
10
|
+
export type LarkConversationKind = "p2p" | "group" | "thread";
|
|
11
|
+
export interface LarkConversationRecord {
|
|
12
|
+
/** Stable conversation key (= the bot's sessionKey), e.g. `lark:t:chat:oc_x`. */
|
|
13
|
+
conversationKey: string;
|
|
14
|
+
/** Stable short id (6 hex chars derived from the key) for `/resume <id>`. */
|
|
15
|
+
shortId: string;
|
|
16
|
+
chatId: string;
|
|
17
|
+
/** Present only for topic-thread conversations. */
|
|
18
|
+
threadId?: string;
|
|
19
|
+
kind: LarkConversationKind;
|
|
20
|
+
/** Last user message text (truncated), used as the display title. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** open_ids that have sent a message in this conversation. */
|
|
23
|
+
participants: string[];
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
}
|
|
26
|
+
export interface LarkRecordConversationInput {
|
|
27
|
+
conversationKey: string;
|
|
28
|
+
chatId: string;
|
|
29
|
+
threadId?: string;
|
|
30
|
+
kind: LarkConversationKind;
|
|
31
|
+
title: string;
|
|
32
|
+
participantOpenId?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface LarkConversationDirectory {
|
|
35
|
+
readonly filePath: string;
|
|
36
|
+
record(input: LarkRecordConversationInput): Promise<LarkConversationRecord>;
|
|
37
|
+
/** Conversations the given user has participated in, newest first. */
|
|
38
|
+
listForUser(openId: string, limit?: number): Promise<LarkConversationRecord[]>;
|
|
39
|
+
getByShortId(shortId: string): Promise<LarkConversationRecord | null>;
|
|
40
|
+
get(conversationKey: string): Promise<LarkConversationRecord | null>;
|
|
41
|
+
}
|
|
42
|
+
/** Re-exported from @rynx-ai/core (kept here for existing importers). */
|
|
43
|
+
export declare const deriveConversationShortId: typeof coreDeriveConversationShortId;
|
|
44
|
+
export declare class FileLarkConversationDirectory implements LarkConversationDirectory {
|
|
45
|
+
private readonly core;
|
|
46
|
+
constructor({ filePath, now, }: {
|
|
47
|
+
filePath: string;
|
|
48
|
+
now?: () => string;
|
|
49
|
+
});
|
|
50
|
+
get filePath(): string;
|
|
51
|
+
record(input: LarkRecordConversationInput): Promise<LarkConversationRecord>;
|
|
52
|
+
listForUser(openId: string, limit?: number): Promise<LarkConversationRecord[]>;
|
|
53
|
+
getByShortId(shortId: string): Promise<LarkConversationRecord | null>;
|
|
54
|
+
get(conversationKey: string): Promise<LarkConversationRecord | null>;
|
|
55
|
+
}
|
|
56
|
+
/** Non-persistent test/direct-construction helper. Production uses the file
|
|
57
|
+
* implementation with an explicit context.dataDir-scoped path. */
|
|
58
|
+
export declare class MemoryLarkConversationDirectory implements LarkConversationDirectory {
|
|
59
|
+
readonly filePath = "memory://lark-conversations";
|
|
60
|
+
private readonly records;
|
|
61
|
+
record(input: LarkRecordConversationInput): Promise<LarkConversationRecord>;
|
|
62
|
+
listForUser(openId: string, limit?: number): Promise<LarkConversationRecord[]>;
|
|
63
|
+
getByShortId(shortId: string): Promise<LarkConversationRecord | null>;
|
|
64
|
+
get(conversationKey: string): Promise<LarkConversationRecord | null>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark adapter over the channel-agnostic @rynx-ai/core conversation directory.
|
|
3
|
+
* Keeps the Lark-facing record shape + API (so the resume card, mirror index,
|
|
4
|
+
* and ingestor are unchanged) while delegating storage + indexing to core's
|
|
5
|
+
* generic FileConversationDirectory, parameterized over Lark's routing payload
|
|
6
|
+
* (chat/thread ids). The plugin runtime supplies one explicit file per instance.
|
|
7
|
+
*/
|
|
8
|
+
import { deriveConversationShortId as coreDeriveConversationShortId, FileConversationDirectory, } from "@rynx-ai/core";
|
|
9
|
+
const TITLE_MAX_CHARS = 40;
|
|
10
|
+
/** Re-exported from @rynx-ai/core (kept here for existing importers). */
|
|
11
|
+
export const deriveConversationShortId = coreDeriveConversationShortId;
|
|
12
|
+
function truncateTitle(title) {
|
|
13
|
+
const normalized = title.replace(/\s+/g, " ").trim();
|
|
14
|
+
if (normalized.length <= TITLE_MAX_CHARS) {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
return `${normalized.slice(0, TITLE_MAX_CHARS - 1)}…`;
|
|
18
|
+
}
|
|
19
|
+
function toLarkRecord(entry) {
|
|
20
|
+
return {
|
|
21
|
+
conversationKey: entry.key,
|
|
22
|
+
shortId: entry.shortId,
|
|
23
|
+
chatId: entry.meta.chatId,
|
|
24
|
+
threadId: entry.meta.threadId,
|
|
25
|
+
kind: entry.meta.kind,
|
|
26
|
+
title: entry.title || "(无标题)",
|
|
27
|
+
participants: entry.participants,
|
|
28
|
+
updatedAt: entry.updatedAt,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export class FileLarkConversationDirectory {
|
|
32
|
+
core;
|
|
33
|
+
constructor({ filePath, now, }) {
|
|
34
|
+
this.core = new FileConversationDirectory({ filePath, now });
|
|
35
|
+
}
|
|
36
|
+
get filePath() {
|
|
37
|
+
return this.core.filePath;
|
|
38
|
+
}
|
|
39
|
+
async record(input) {
|
|
40
|
+
const entry = await this.core.record({
|
|
41
|
+
key: input.conversationKey,
|
|
42
|
+
title: truncateTitle(input.title),
|
|
43
|
+
participant: input.participantOpenId,
|
|
44
|
+
// Keep a previously-recorded threadId when this message didn't carry one.
|
|
45
|
+
meta: (previous) => ({
|
|
46
|
+
chatId: input.chatId,
|
|
47
|
+
threadId: input.threadId ?? previous?.threadId,
|
|
48
|
+
kind: input.kind,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
return toLarkRecord(entry);
|
|
52
|
+
}
|
|
53
|
+
async listForUser(openId, limit = 5) {
|
|
54
|
+
const entries = await this.core.listRecent({
|
|
55
|
+
limit,
|
|
56
|
+
where: (entry) => entry.participants.includes(openId),
|
|
57
|
+
});
|
|
58
|
+
return entries.map(toLarkRecord);
|
|
59
|
+
}
|
|
60
|
+
async getByShortId(shortId) {
|
|
61
|
+
const entry = await this.core.getByShortId(shortId);
|
|
62
|
+
return entry ? toLarkRecord(entry) : null;
|
|
63
|
+
}
|
|
64
|
+
async get(conversationKey) {
|
|
65
|
+
const entry = await this.core.get(conversationKey);
|
|
66
|
+
return entry ? toLarkRecord(entry) : null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Non-persistent test/direct-construction helper. Production uses the file
|
|
70
|
+
* implementation with an explicit context.dataDir-scoped path. */
|
|
71
|
+
export class MemoryLarkConversationDirectory {
|
|
72
|
+
filePath = "memory://lark-conversations";
|
|
73
|
+
records = new Map();
|
|
74
|
+
async record(input) {
|
|
75
|
+
const previous = this.records.get(input.conversationKey);
|
|
76
|
+
const participants = new Set(previous?.participants ?? []);
|
|
77
|
+
if (input.participantOpenId)
|
|
78
|
+
participants.add(input.participantOpenId);
|
|
79
|
+
const record = {
|
|
80
|
+
conversationKey: input.conversationKey,
|
|
81
|
+
shortId: deriveConversationShortId(input.conversationKey),
|
|
82
|
+
chatId: input.chatId,
|
|
83
|
+
...(input.threadId ?? previous?.threadId
|
|
84
|
+
? { threadId: input.threadId ?? previous?.threadId }
|
|
85
|
+
: {}),
|
|
86
|
+
kind: input.kind,
|
|
87
|
+
title: truncateTitle(input.title) || "(无标题)",
|
|
88
|
+
participants: [...participants],
|
|
89
|
+
updatedAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
this.records.set(input.conversationKey, record);
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
async listForUser(openId, limit = 5) {
|
|
95
|
+
return [...this.records.values()]
|
|
96
|
+
.filter((record) => record.participants.includes(openId))
|
|
97
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
98
|
+
.slice(0, limit);
|
|
99
|
+
}
|
|
100
|
+
async getByShortId(shortId) {
|
|
101
|
+
return [...this.records.values()].find((record) => record.shortId === shortId) ?? null;
|
|
102
|
+
}
|
|
103
|
+
async get(conversationKey) {
|
|
104
|
+
return this.records.get(conversationKey) ?? null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Per-file change stats from `git diff --numstat` (plus untracked files). */
|
|
2
|
+
export interface DiffFileStat {
|
|
3
|
+
path: string;
|
|
4
|
+
/** Added lines, or `null` for binary files (numstat reports `-`). */
|
|
5
|
+
additions: number | null;
|
|
6
|
+
/** Removed lines, or `null` for binary files. */
|
|
7
|
+
deletions: number | null;
|
|
8
|
+
/** True for files surfaced by `git ls-files --others` (not yet staged). */
|
|
9
|
+
untracked?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface DiffCardInput {
|
|
12
|
+
files: DiffFileStat[];
|
|
13
|
+
totalAdditions: number;
|
|
14
|
+
totalDeletions: number;
|
|
15
|
+
/** Unified diff body to embed (already capped by the caller). */
|
|
16
|
+
diffBody: string;
|
|
17
|
+
/** Whether `diffBody` was truncated. */
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Render `/diff` as a CardKit card: a summary line, a per-file change list, and
|
|
22
|
+
* the full unified diff inside a collapsed panel (a ```diff fenced block so
|
|
23
|
+
* Feishu colors the +/- lines). Far more readable than a raw text dump.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildDiffCardJson(input: DiffCardInput): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderDiffMarkdown } from "./lark-card-stream.js";
|
|
2
|
+
/** Cap the per-file list so a huge change set doesn't blow up the card. */
|
|
3
|
+
const FILE_LIST_LIMIT = 40;
|
|
4
|
+
function renderFileLine(file) {
|
|
5
|
+
if (file.untracked) {
|
|
6
|
+
return `\`${file.path}\` <font color='grey'>未跟踪</font>`;
|
|
7
|
+
}
|
|
8
|
+
if (file.additions === null || file.deletions === null) {
|
|
9
|
+
return `\`${file.path}\` <font color='grey'>二进制</font>`;
|
|
10
|
+
}
|
|
11
|
+
return `\`${file.path}\` <font color='green'>+${file.additions}</font> <font color='red'>-${file.deletions}</font>`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Render `/diff` as a CardKit card: a summary line, a per-file change list, and
|
|
15
|
+
* the full unified diff inside a collapsed panel (a ```diff fenced block so
|
|
16
|
+
* Feishu colors the +/- lines). Far more readable than a raw text dump.
|
|
17
|
+
*/
|
|
18
|
+
export function buildDiffCardJson(input) {
|
|
19
|
+
const { files, totalAdditions, totalDeletions, diffBody, truncated } = input;
|
|
20
|
+
const summary = `共 **${files.length}** 个文件 ` +
|
|
21
|
+
`<font color='green'>+${totalAdditions}</font> ` +
|
|
22
|
+
`<font color='red'>-${totalDeletions}</font>`;
|
|
23
|
+
const shown = files.slice(0, FILE_LIST_LIMIT);
|
|
24
|
+
const fileLines = shown.map(renderFileLine);
|
|
25
|
+
const hidden = files.length - shown.length;
|
|
26
|
+
if (hidden > 0) {
|
|
27
|
+
fileLines.push(`<font color='grey'>… 还有 ${hidden} 个文件</font>`);
|
|
28
|
+
}
|
|
29
|
+
const elements = [
|
|
30
|
+
{ tag: "markdown", content: summary },
|
|
31
|
+
{ tag: "hr" },
|
|
32
|
+
{ tag: "markdown", content: fileLines.join("\n") },
|
|
33
|
+
];
|
|
34
|
+
if (diffBody.trim()) {
|
|
35
|
+
elements.push({
|
|
36
|
+
tag: "collapsible_panel",
|
|
37
|
+
expanded: false,
|
|
38
|
+
header: {
|
|
39
|
+
title: {
|
|
40
|
+
tag: "plain_text",
|
|
41
|
+
content: truncated ? "完整 diff(已截断)" : "完整 diff",
|
|
42
|
+
},
|
|
43
|
+
vertical_align: "center",
|
|
44
|
+
icon: { tag: "standard_icon", token: "down-small-ccm_outlined" },
|
|
45
|
+
},
|
|
46
|
+
elements: [{ tag: "markdown", content: renderDiffMarkdown(diffBody) }],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const card = {
|
|
50
|
+
schema: "2.0",
|
|
51
|
+
header: {
|
|
52
|
+
title: { tag: "plain_text", content: "📝 工作区改动" },
|
|
53
|
+
template: "blue",
|
|
54
|
+
},
|
|
55
|
+
body: { elements },
|
|
56
|
+
};
|
|
57
|
+
return JSON.stringify(card);
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ForkTranscriptMessage {
|
|
2
|
+
msgType: string;
|
|
3
|
+
senderId?: string;
|
|
4
|
+
senderType?: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
/** Extract a human-readable line from a Feishu message body `content` (a JSON
|
|
8
|
+
* string whose shape depends on `msgType`). Non-text types become a short
|
|
9
|
+
* placeholder so the transcript stays readable. */
|
|
10
|
+
export declare function extractMessageText(msgType: string, content: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Render a conversation history as a transcript: one `名字:内容` line per
|
|
13
|
+
* message. `nameById` maps a user open_id to a display name; messages from the
|
|
14
|
+
* bot (sender_type !== "user") are labelled 机器人. Bounded to a sane size.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildForkTranscript(messages: ForkTranscriptMessage[], nameById: Map<string, string>): string;
|