dsh-lark-agent 0.4.0 → 0.5.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/README.md +7 -3
- package/dist/bridge.d.ts +6 -1
- package/dist/bridge.js +92 -16
- package/dist/client/AgentSettings.d.ts +12 -0
- package/dist/client/ConnectionCard.d.ts +5 -1
- package/dist/client/ManagerPanel.d.ts +3 -0
- package/dist/client/api.d.ts +14 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/manager-styles.d.ts +2 -0
- package/dist/client/use-connection.d.ts +2 -2
- package/dist/client.cjs +727 -258
- package/dist/config.d.ts +4 -0
- package/dist/config.js +8 -0
- package/dist/dsh-runtime.d.ts +18 -4
- package/dist/dsh-runtime.js +199 -61
- package/dist/lark-cli.d.ts +7 -0
- package/dist/lark-cli.js +105 -1
- package/dist/media-input.d.ts +28 -0
- package/dist/media-input.js +127 -0
- package/dist/media-scopes.d.ts +5 -0
- package/dist/media-scopes.js +6 -0
- package/dist/message.d.ts +7 -0
- package/dist/message.js +27 -3
- package/dist/onboarding/agents.d.ts +9 -0
- package/dist/onboarding/agents.js +100 -0
- package/dist/onboarding/controller.d.ts +15 -2
- package/dist/onboarding/controller.js +155 -14
- package/dist/onboarding/host.d.ts +14 -2
- package/dist/onboarding/host.js +45 -16
- package/dist/onboarding/lark-platform.d.ts +6 -0
- package/dist/onboarding/lark-platform.js +15 -4
- package/dist/onboarding/manager.d.ts +32 -0
- package/dist/onboarding/manager.js +129 -0
- package/dist/onboarding/protocol.d.ts +51 -1
- package/dist/onboarding/settings-store.d.ts +11 -0
- package/dist/onboarding/settings-store.js +67 -0
- package/dist/session-store.d.ts +5 -1
- package/dist/session-store.js +32 -4
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export class MediaInputError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export const MAX_INPUT_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
5
|
+
export const MAX_INPUT_AUDIO_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
export const MAX_INPUT_IMAGES = 4;
|
|
7
|
+
export const MAX_INPUT_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
8
|
+
const MAX_PCM_BYTES = 60 * 16_000 * 2;
|
|
9
|
+
/** Fixed local codec invocation; no user/model arguments, URLs, paths or stderr reach the process. */
|
|
10
|
+
export function decodeVoice(data, signal) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
const child = spawn('ffmpeg', ['-hide_banner', '-loglevel', 'error', '-protocol_whitelist', 'pipe', '-i', 'pipe:0',
|
|
14
|
+
'-vn', '-ac', '1', '-ar', '16000', '-f', 's16le', 'pipe:1'], { shell: false, windowsHide: true, stdio: 'pipe' });
|
|
15
|
+
let failure;
|
|
16
|
+
let size = 0;
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let kill;
|
|
19
|
+
const stop = (error) => {
|
|
20
|
+
failure ??= error;
|
|
21
|
+
child.kill('SIGTERM');
|
|
22
|
+
kill ??= setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
23
|
+
};
|
|
24
|
+
const abort = () => stop(new MediaInputError('语音处理已取消。'));
|
|
25
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
26
|
+
const timeout = setTimeout(() => stop(new MediaInputError('语音转码超时,请改发文字。')), 30_000);
|
|
27
|
+
child.stdout.on('data', (chunk) => {
|
|
28
|
+
size += chunk.length;
|
|
29
|
+
if (size > MAX_PCM_BYTES) {
|
|
30
|
+
stop(new MediaInputError('语音最长支持 60 秒,请分段发送。'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!failure)
|
|
34
|
+
chunks.push(Buffer.from(chunk));
|
|
35
|
+
});
|
|
36
|
+
child.stderr.resume();
|
|
37
|
+
child.stdin.on('error', () => stop(new MediaInputError('语音格式无法读取,请改发文字。')));
|
|
38
|
+
child.on('error', () => { failure = new MediaInputError('语音输入需要宿主设备安装 FFmpeg。'); });
|
|
39
|
+
child.on('close', code => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
clearTimeout(kill);
|
|
42
|
+
signal.removeEventListener('abort', abort);
|
|
43
|
+
if (failure)
|
|
44
|
+
reject(failure);
|
|
45
|
+
else if (code !== 0 || !size)
|
|
46
|
+
reject(new MediaInputError('这段语音无法读取,请改发文字。'));
|
|
47
|
+
else
|
|
48
|
+
resolve(Buffer.concat(chunks));
|
|
49
|
+
});
|
|
50
|
+
child.stdin.end(data);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Only binary discovery, never a download or installation. */
|
|
54
|
+
export function ffmpegAvailable() {
|
|
55
|
+
return new Promise(resolve => {
|
|
56
|
+
const child = spawn('ffmpeg', ['-version'], { shell: false, windowsHide: true, stdio: 'ignore' });
|
|
57
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); resolve(false); }, 3000);
|
|
58
|
+
child.on('error', () => { clearTimeout(timer); resolve(false); });
|
|
59
|
+
child.on('close', code => { clearTimeout(timer); resolve(code === 0); });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function imageType(data) {
|
|
63
|
+
const bytes = Buffer.from(data);
|
|
64
|
+
if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
65
|
+
return 'image/png';
|
|
66
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255)
|
|
67
|
+
return 'image/jpeg';
|
|
68
|
+
if (['GIF87a', 'GIF89a'].includes(bytes.subarray(0, 6).toString('ascii')))
|
|
69
|
+
return 'image/gif';
|
|
70
|
+
if (bytes.subarray(0, 4).toString('ascii') === 'RIFF' && bytes.subarray(8, 12).toString('ascii') === 'WEBP')
|
|
71
|
+
return 'image/webp';
|
|
72
|
+
throw new MediaInputError('图片格式不支持,请发送 PNG、JPEG、WebP 或 GIF。');
|
|
73
|
+
}
|
|
74
|
+
/** Called inside an admitted lane, after identity, durable claim and current chat access checks. */
|
|
75
|
+
export async function prepareMedia(message, transport, signal, options, decode = decodeVoice) {
|
|
76
|
+
const resources = message.resources ?? [];
|
|
77
|
+
if (!resources.length)
|
|
78
|
+
return { text: message.text };
|
|
79
|
+
if (message.chatType !== 'p2p')
|
|
80
|
+
throw new MediaInputError('图片和语音输入仅用于本人私聊,请改发文字。');
|
|
81
|
+
if (resources.length > MAX_INPUT_IMAGES)
|
|
82
|
+
throw new MediaInputError(`每条消息最多支持 ${MAX_INPUT_IMAGES} 张图片,请分开发送。`);
|
|
83
|
+
if (resources.some(resource => resource.kind === 'image') && !options.images)
|
|
84
|
+
throw new MediaInputError('这个机器人的图片输入已关闭,请在设置中开启。');
|
|
85
|
+
if (resources.some(resource => resource.kind === 'audio') && !options.voice)
|
|
86
|
+
throw new MediaInputError('这个机器人的语音输入未启用,请在设置中检查 FFmpeg 和飞书语音识别权限,或改发文字。');
|
|
87
|
+
if (!transport.readResource)
|
|
88
|
+
throw new MediaInputError('当前消息通道无法下载附件,请更新插件。');
|
|
89
|
+
const images = [];
|
|
90
|
+
let total = 0, text = message.text;
|
|
91
|
+
for (const resource of resources) {
|
|
92
|
+
signal.throwIfAborted();
|
|
93
|
+
if (resource.kind === 'audio' && resource.duration !== undefined && resource.duration > 60)
|
|
94
|
+
throw new MediaInputError('语音最长支持 60 秒,请分段发送。');
|
|
95
|
+
let data;
|
|
96
|
+
try {
|
|
97
|
+
data = await transport.readResource(resource, signal);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new MediaInputError('附件未能下载,请检查机器人的 im:message:readonly 权限和消息是否仍可访问。');
|
|
101
|
+
}
|
|
102
|
+
signal.throwIfAborted();
|
|
103
|
+
total += data.byteLength;
|
|
104
|
+
if (!data.byteLength || total > MAX_INPUT_TOTAL_BYTES || data.byteLength > (resource.kind === 'image' ? MAX_INPUT_IMAGE_BYTES : MAX_INPUT_AUDIO_BYTES)) {
|
|
105
|
+
throw new MediaInputError('附件超出大小限制:单张图片 10 MB、单段语音 5 MB,每条消息合计 20 MB。');
|
|
106
|
+
}
|
|
107
|
+
if (resource.kind === 'image')
|
|
108
|
+
images.push({ data, mediaType: imageType(data) });
|
|
109
|
+
else {
|
|
110
|
+
if (!transport.recognizeSpeech)
|
|
111
|
+
throw new MediaInputError('当前消息通道没有语音识别能力,请改发文字。');
|
|
112
|
+
const pcm = await decode(data, signal);
|
|
113
|
+
try {
|
|
114
|
+
text = (await transport.recognizeSpeech(pcm, signal)).trim();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new MediaInputError('语音识别失败,请检查 speech_to_text:speech 权限及租户套餐,或改发文字。');
|
|
118
|
+
}
|
|
119
|
+
if (!text || text.length > options.maxText)
|
|
120
|
+
throw new MediaInputError('没有得到可用的语音文字,请缩短语音后重试。');
|
|
121
|
+
// A transcript is conversation input. It must never become a slash command or card action.
|
|
122
|
+
text = `用户发来的语音转写(可能有识别误差):\n${text}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { text, ...(images.length ? { images } : {}) };
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=media-input.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Fixed bot scopes. UI callers choose a feature, never supply arbitrary permissions. */
|
|
2
|
+
export type MediaInputKind = 'images' | 'voice';
|
|
3
|
+
export declare const MEDIA_RESOURCE_SCOPE = "im:message:readonly";
|
|
4
|
+
export declare const SPEECH_SCOPE = "speech_to_text:speech";
|
|
5
|
+
export declare function mediaScopes(kind: MediaInputKind): readonly string[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const MEDIA_RESOURCE_SCOPE = 'im:message:readonly';
|
|
2
|
+
export const SPEECH_SCOPE = 'speech_to_text:speech';
|
|
3
|
+
export function mediaScopes(kind) {
|
|
4
|
+
return kind === 'voice' ? [MEDIA_RESOURCE_SCOPE, SPEECH_SCOPE] : [MEDIA_RESOURCE_SCOPE];
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=media-scopes.js.map
|
package/dist/message.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export interface IncomingMessage {
|
|
|
6
6
|
chatType: 'p2p' | 'group';
|
|
7
7
|
threadId: string;
|
|
8
8
|
text: string;
|
|
9
|
+
resources?: IncomingResource[];
|
|
10
|
+
}
|
|
11
|
+
export interface IncomingResource {
|
|
12
|
+
kind: 'image' | 'audio';
|
|
13
|
+
key: string;
|
|
14
|
+
messageId: string;
|
|
15
|
+
duration?: number;
|
|
9
16
|
}
|
|
10
17
|
/** Accept only the verified, compact Lark CLI event contract. Fail closed on raw/unknown events. */
|
|
11
18
|
export declare function parseMessage(raw: unknown, botOpenId: string): IncomingMessage | undefined;
|
package/dist/message.js
CHANGED
|
@@ -6,7 +6,7 @@ export function parseMessage(raw, botOpenId) {
|
|
|
6
6
|
const data = object(raw);
|
|
7
7
|
if (!data || data.type !== 'im.message.receive_v1' || data.sender_type !== 'user')
|
|
8
8
|
return;
|
|
9
|
-
if (
|
|
9
|
+
if (!['text', 'post', 'image', 'audio'].includes(String(data.message_type)))
|
|
10
10
|
return;
|
|
11
11
|
if (!validId(data.message_id, 'om') || !validId(data.chat_id, 'oc') || !validId(data.sender_id, 'ou'))
|
|
12
12
|
return;
|
|
@@ -21,6 +21,27 @@ export function parseMessage(raw, botOpenId) {
|
|
|
21
21
|
// Text events render @name; rich posts can render <at user_id="...">.
|
|
22
22
|
// Authorization always uses the structured open_id, never a display name.
|
|
23
23
|
let text = data.content;
|
|
24
|
+
const resources = [];
|
|
25
|
+
// The CLI compact contract renders media markers, not the original Lark JSON.
|
|
26
|
+
// Keys stay bound to this real event's message ID; no arbitrary URL or path is accepted.
|
|
27
|
+
if (data.message_type === 'image' || data.message_type === 'post') {
|
|
28
|
+
text = text.replace(/\[Image: (img_[A-Za-z0-9_-]{1,200})\]|!\[Image\]\((img_[A-Za-z0-9_-]{1,200})\)/g, (_tag, marker, inline) => {
|
|
29
|
+
const key = (marker ?? inline);
|
|
30
|
+
if (!resources.some(resource => resource.key === key))
|
|
31
|
+
resources.push({ kind: 'image', key, messageId: data.message_id });
|
|
32
|
+
return '';
|
|
33
|
+
});
|
|
34
|
+
if (data.message_type === 'image' && (!resources.length || text.trim()))
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (data.message_type === 'audio') {
|
|
38
|
+
const audio = /^<audio key="(file_[A-Za-z0-9_-]{1,200})"(?: duration="([0-9]{1,6})s")?\s*\/>$/.exec(text.trim());
|
|
39
|
+
if (!audio)
|
|
40
|
+
return;
|
|
41
|
+
resources.push({ kind: 'audio', key: audio[1], messageId: data.message_id,
|
|
42
|
+
...(audio[2] === undefined ? {} : { duration: Number(audio[2]) }) });
|
|
43
|
+
text = '';
|
|
44
|
+
}
|
|
24
45
|
for (const mention of ours) {
|
|
25
46
|
if (typeof mention.key === 'string' && mention.key.length > 0)
|
|
26
47
|
text = text.split(mention.key).join('');
|
|
@@ -32,12 +53,15 @@ export function parseMessage(raw, botOpenId) {
|
|
|
32
53
|
text = text.slice(rendered.length).trimStart();
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
|
-
if (!text)
|
|
56
|
+
if (!text && !resources.length)
|
|
36
57
|
return;
|
|
58
|
+
if (!text)
|
|
59
|
+
text = data.message_type === 'audio' ? '请处理这段语音。' : '请查看这些图片。';
|
|
37
60
|
// Only a real Lark thread is its own conversation. A quote reply also carries
|
|
38
61
|
// root_id, but it continues the chat's current conversation.
|
|
39
62
|
const threadId = typeof data.thread_id === 'string' && /^(om|omt)_[a-zA-Z0-9_-]+$/.test(data.thread_id) ? data.thread_id : '';
|
|
40
|
-
return { messageId: data.message_id, chatId: data.chat_id, senderId: data.sender_id, chatType: data.chat_type, threadId, text
|
|
63
|
+
return { messageId: data.message_id, chatId: data.chat_id, senderId: data.sender_id, chatType: data.chat_type, threadId, text,
|
|
64
|
+
...(resources.length ? { resources } : {}) };
|
|
41
65
|
}
|
|
42
66
|
export function authorize(message, config) {
|
|
43
67
|
const policy = config.users.find(user => user.openId === message.senderId);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { AgentCatalog, AgentSettings } from './protocol.js';
|
|
3
|
+
import { SettingsStore } from './settings-store.js';
|
|
4
|
+
export declare const defaultAgentSettings: AgentSettings;
|
|
5
|
+
export declare function parseAgentSettings(raw: unknown): AgentSettings;
|
|
6
|
+
export declare function agentSettingsStore(directory: string): SettingsStore<AgentSettings>;
|
|
7
|
+
/** A metadata-only catalog; no plugin definitions, paths, provider endpoints or secrets. */
|
|
8
|
+
export declare function agentCatalog(ctx: Context): Promise<AgentCatalog>;
|
|
9
|
+
export declare function validateAgentSettings(ctx: Context, settings: AgentSettings): Promise<AgentSettings>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { SetupError } from './controller.js';
|
|
4
|
+
import { SettingsStore } from './settings-store.js';
|
|
5
|
+
import { ffmpegAvailable } from '../media-input.js';
|
|
6
|
+
const controls = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u;
|
|
7
|
+
export const defaultAgentSettings = { label: '', agentPreset: '', workspace: '', imageInput: true, voiceInput: false };
|
|
8
|
+
export function parseAgentSettings(raw) {
|
|
9
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
10
|
+
throw new Error('Invalid agent settings');
|
|
11
|
+
const data = raw;
|
|
12
|
+
for (const [key, limit] of [['label', 80], ['agentPreset', 200], ['workspace', 4096]]) {
|
|
13
|
+
const value = data[key];
|
|
14
|
+
if (typeof value !== 'string' || value.length > limit || value !== value.trim() || controls.test(value))
|
|
15
|
+
throw new Error('Invalid agent settings');
|
|
16
|
+
}
|
|
17
|
+
if (typeof data.imageInput !== 'boolean' || typeof data.voiceInput !== 'boolean'
|
|
18
|
+
|| (data.workspace && !isAbsolute(data.workspace)))
|
|
19
|
+
throw new Error('Invalid agent settings');
|
|
20
|
+
return { label: data.label, agentPreset: data.agentPreset, workspace: data.workspace,
|
|
21
|
+
imageInput: data.imageInput, voiceInput: data.voiceInput };
|
|
22
|
+
}
|
|
23
|
+
export function agentSettingsStore(directory) {
|
|
24
|
+
return new SettingsStore(directory, 'agent.json', parseAgentSettings);
|
|
25
|
+
}
|
|
26
|
+
/** A metadata-only catalog; no plugin definitions, paths, provider endpoints or secrets. */
|
|
27
|
+
export async function agentCatalog(ctx) {
|
|
28
|
+
const service = ctx.get('agentPresets');
|
|
29
|
+
const available = typeof service?.list === 'function' && typeof service.resolve === 'function' && typeof service.mount === 'function';
|
|
30
|
+
const catalog = { presets: [], presetsAvailable: available, model: '跟随宿主模型',
|
|
31
|
+
imagesAvailable: typeof ctx.get('attachments')?.saveImages === 'function',
|
|
32
|
+
voiceAvailable: await ffmpegAvailable() };
|
|
33
|
+
const display = (value, limit) => typeof value === 'string' && !controls.test(value) ? value.slice(0, limit) : '';
|
|
34
|
+
if (available) {
|
|
35
|
+
const rows = await service.list();
|
|
36
|
+
if (!Array.isArray(rows) || rows.length > 200)
|
|
37
|
+
throw new SetupError('Agent 预设列表暂时不可用,请检查宿主设置。');
|
|
38
|
+
catalog.presets = rows.filter(row => typeof row.id === 'string' && row.id.length > 0 && row.id.length <= 200 && !controls.test(row.id))
|
|
39
|
+
.map(row => ({ id: row.id, name: display(row.name, 200) || row.id,
|
|
40
|
+
...(display(row.description, 600) ? { description: display(row.description, 600) } : {}), available: !row.broken }));
|
|
41
|
+
try {
|
|
42
|
+
catalog.defaultPreset = (await service.resolve()).id;
|
|
43
|
+
}
|
|
44
|
+
catch { /* An explicit working preset is still selectable. */ }
|
|
45
|
+
}
|
|
46
|
+
const selection = ctx.get('agentDefaultModel')?.currentSelection?.();
|
|
47
|
+
if (display(selection?.model, 200))
|
|
48
|
+
catalog.model = display(selection?.model, 200);
|
|
49
|
+
catalog.modelImages = 'unknown';
|
|
50
|
+
const llm = ctx.get('llm');
|
|
51
|
+
if (selection?.provider && selection.model && typeof llm?.resolveModelInfo === 'function') {
|
|
52
|
+
const aborter = new AbortController();
|
|
53
|
+
let timer;
|
|
54
|
+
try {
|
|
55
|
+
// Adapter metadata only. No test message, Agent creation or tool execution.
|
|
56
|
+
const info = await Promise.race([
|
|
57
|
+
llm.resolveModelInfo(selection.provider, selection.model, aborter.signal),
|
|
58
|
+
new Promise(resolve => { timer = setTimeout(() => { aborter.abort(); resolve(undefined); }, 3000); }),
|
|
59
|
+
]);
|
|
60
|
+
if (Array.isArray(info?.inputModalities))
|
|
61
|
+
catalog.modelImages = info.inputModalities.includes('image') ? 'supported' : 'unsupported';
|
|
62
|
+
}
|
|
63
|
+
catch { /* Missing metadata is shown as unknown, and checked again for the actual chat model at admission. */ }
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
aborter.abort();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return catalog;
|
|
70
|
+
}
|
|
71
|
+
export async function validateAgentSettings(ctx, settings) {
|
|
72
|
+
const next = parseAgentSettings(settings);
|
|
73
|
+
if (next.agentPreset) {
|
|
74
|
+
const presets = ctx.get('agentPresets');
|
|
75
|
+
if (typeof presets?.resolve !== 'function' || typeof presets.mount !== 'function')
|
|
76
|
+
throw new SetupError('当前宿主不支持选择 Agent 预设。');
|
|
77
|
+
try {
|
|
78
|
+
const resolved = await presets.resolve(next.agentPreset);
|
|
79
|
+
if (resolved.id !== next.agentPreset || resolved.broken)
|
|
80
|
+
throw new Error();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new SetupError('这个 Agent 预设已不可用,请刷新后重新选择。');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (next.workspace) {
|
|
87
|
+
try {
|
|
88
|
+
next.workspace = await realpath(next.workspace);
|
|
89
|
+
if (!(await stat(next.workspace)).isDirectory())
|
|
90
|
+
throw new Error();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new SetupError('工作区必须是这台设备上已有的文件夹。');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (next.voiceInput && !await ffmpegAvailable())
|
|
97
|
+
throw new SetupError('请先在宿主设备安装 FFmpeg,再启用语音输入。');
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -3,9 +3,9 @@ import { type BotService } from '../bot-service.js';
|
|
|
3
3
|
import { LarkPlatform } from './lark-platform.js';
|
|
4
4
|
import { ConnectionStore } from './store.js';
|
|
5
5
|
import { waitForPairing } from './pairing.js';
|
|
6
|
-
import type { ConnectionEndpoint, ConnectionProfile, ConnectionSnapshot } from './protocol.js';
|
|
6
|
+
import type { AgentSettings, ConnectionEndpoint, ConnectionProfile, ConnectionSnapshot } from './protocol.js';
|
|
7
7
|
export interface OnboardingDependencies {
|
|
8
|
-
platform: Pick<LarkPlatform, 'listProfiles' | 'registerNew' | 'saveNewProfile' | 'inspect' | 'configureAndPublish'> & Partial<Pick<LarkPlatform, 'enableDocuments'>>;
|
|
8
|
+
platform: Pick<LarkPlatform, 'listProfiles' | 'registerNew' | 'saveNewProfile' | 'inspect' | 'configureAndPublish'> & Partial<Pick<LarkPlatform, 'enableDocuments' | 'enableInput'>>;
|
|
9
9
|
store: ConnectionStore;
|
|
10
10
|
checkRuntime(): void;
|
|
11
11
|
start(config: BotConfig, signal: AbortSignal, onHealth: (ready: boolean) => void, pairedChat?: {
|
|
@@ -19,6 +19,10 @@ export interface OnboardingDependencies {
|
|
|
19
19
|
executable?: string;
|
|
20
20
|
/** Delay before automatic restore attempt `attempt` (0-based) of an enabled connection. */
|
|
21
21
|
retryDelay?: (attempt: number) => number;
|
|
22
|
+
validateAgent?: (settings: AgentSettings) => Promise<AgentSettings>;
|
|
23
|
+
reserveApp?: (appId: string) => void;
|
|
24
|
+
/** Release former reservations only after the replacement record is durable. */
|
|
25
|
+
commitApp?: (appId: string) => void;
|
|
22
26
|
}
|
|
23
27
|
export declare class SetupError extends Error {
|
|
24
28
|
readonly retryable: boolean;
|
|
@@ -39,20 +43,27 @@ export declare class ConnectionController {
|
|
|
39
43
|
private unsafeStop;
|
|
40
44
|
private menuReady;
|
|
41
45
|
private documentsReady;
|
|
46
|
+
private imagesReady;
|
|
47
|
+
private voiceReady;
|
|
42
48
|
/** Menu entries the published app lacks; the owner adds them in the developer console. */
|
|
43
49
|
private menuMissing;
|
|
44
50
|
private documentSetup?;
|
|
45
51
|
private documentJob?;
|
|
52
|
+
private inputSetup?;
|
|
46
53
|
private documentAborter?;
|
|
47
54
|
private candidate?;
|
|
48
55
|
private commands;
|
|
49
56
|
private retryTimer?;
|
|
50
57
|
private retryAttempts;
|
|
58
|
+
private settings;
|
|
59
|
+
private readonly settingsStore;
|
|
60
|
+
private settingsFailed;
|
|
51
61
|
constructor(deps: OnboardingDependencies);
|
|
52
62
|
snapshot(): ConnectionSnapshot;
|
|
53
63
|
private phase;
|
|
54
64
|
private action;
|
|
55
65
|
private consoleUrl;
|
|
66
|
+
private inspectCapabilities;
|
|
56
67
|
private assertCurrent;
|
|
57
68
|
private save;
|
|
58
69
|
private launch;
|
|
@@ -68,6 +79,8 @@ export declare class ConnectionController {
|
|
|
68
79
|
command(endpoint: Exclude<ConnectionEndpoint, 'profiles' | 'status'>, payload: unknown): Promise<ConnectionSnapshot>;
|
|
69
80
|
private create;
|
|
70
81
|
private check;
|
|
82
|
+
private botDirectory;
|
|
83
|
+
private defaultWorkspace;
|
|
71
84
|
private config;
|
|
72
85
|
private bind;
|
|
73
86
|
private start;
|