dsh-lark-agent 0.3.3 → 0.5.0
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 +10 -5
- package/dist/bot-service.js +4 -2
- package/dist/bridge.d.ts +27 -1
- package/dist/bridge.js +333 -66
- package/dist/card-actions.js +3 -1
- 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 +3 -0
- package/dist/dsh-runtime.js +46 -4
- package/dist/lark-cli.d.ts +20 -1
- package/dist/lark-cli.js +168 -2
- 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 +49 -32
- 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/model-check.d.ts +5 -0
- package/dist/onboarding/model-check.js +51 -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/replies.d.ts +16 -0
- package/dist/replies.js +37 -11
- package/dist/session-controls.d.ts +34 -0
- package/dist/session-controls.js +66 -0
- package/dist/session-store.d.ts +7 -1
- package/dist/session-store.js +44 -11
- package/dist/topic-cards.d.ts +3 -0
- package/dist/topic-cards.js +8 -0
- package/dist/topic-store.d.ts +40 -0
- package/dist/topic-store.js +192 -0
- package/package.json +1 -1
package/dist/onboarding/host.js
CHANGED
|
@@ -4,14 +4,18 @@ import { constants, readFileSync } from 'node:fs';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { delimiter, join, resolve } from 'node:path';
|
|
6
6
|
import { createBotService, BotServiceStartError } from '../bot-service.js';
|
|
7
|
-
import { checkRuntimeCompatibility
|
|
7
|
+
import { checkRuntimeCompatibility } from '../dsh-runtime.js';
|
|
8
8
|
import { acquireLock, LockHeldError } from '../lock.js';
|
|
9
|
-
import {
|
|
10
|
-
import { ConnectionStore } from './store.js';
|
|
9
|
+
import { SetupError } from './controller.js';
|
|
11
10
|
import { LarkPlatform } from './lark-platform.js';
|
|
11
|
+
import { ConnectionManager, validConnectionId } from './manager.js';
|
|
12
|
+
import { agentCatalog, parseAgentSettings, validateAgentSettings } from './agents.js';
|
|
12
13
|
import { npmLatest, UpdateCheck } from './updates.js';
|
|
14
|
+
import { checkConnectionModel } from './model-check.js';
|
|
13
15
|
const endpoints = new Set(['status', 'profiles', 'create', 'use-profile', 'recheck', 'cancel',
|
|
14
|
-
'renew-pairing', 'confirm-owner', 'pause', 'resume', 'disconnect', 'enable-documents', 'cancel-documents']);
|
|
16
|
+
'renew-pairing', 'confirm-owner', 'pause', 'resume', 'disconnect', 'enable-documents', 'cancel-documents', 'enable-input', 'cancel-input', 'update-agent']);
|
|
17
|
+
const managerEndpoints = ['connections', 'add-connection', 'remove-connection', 'agent-catalog'];
|
|
18
|
+
const allEndpoints = [...endpoints, ...managerEndpoints];
|
|
15
19
|
const failure = (message) => ({ ok: false, error: { code: 'internal', message, details: {} } });
|
|
16
20
|
class ConnectionRegistrationCleanupError extends Error {
|
|
17
21
|
}
|
|
@@ -29,24 +33,48 @@ export function registerConnectionRpc(rpc, controller, ownerRoutes) {
|
|
|
29
33
|
if (!legacy && rpc.handle.length !== 2)
|
|
30
34
|
throw new SetupError('此 dsh 版本的连接接口尚未验证,请更新到支持的版本。');
|
|
31
35
|
const dispatch = async (endpoint, payload, signal) => {
|
|
32
|
-
if (!
|
|
36
|
+
if (!allEndpoints.includes(endpoint))
|
|
33
37
|
return failure('不支持的连接操作。');
|
|
34
38
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
35
39
|
return failure('连接请求格式不正确。');
|
|
36
|
-
const
|
|
40
|
+
const manager = managerEndpoints.includes(endpoint);
|
|
41
|
+
const allowed = [...(manager && endpoint !== 'remove-connection' ? [] : ['connectionId']), ...(endpoint === 'create' ? ['cards', 'loading'] : endpoint === 'use-profile' ? ['name'] : endpoint === 'confirm-owner' ? ['id']
|
|
42
|
+
: endpoint === 'update-agent' ? ['label', 'agentPreset', 'workspace', 'imageInput', 'voiceInput'] : endpoint === 'enable-input' ? ['kind'] : [])];
|
|
37
43
|
const data = payload;
|
|
38
|
-
if (Object.keys(data).some(key => !allowed.includes(key)) || Object.values(data).some(value => !['boolean', 'string'].includes(typeof value) || (typeof value === 'string' && value.length > 200))
|
|
44
|
+
if (Object.keys(data).some(key => !allowed.includes(key)) || Object.values(data).some(value => !['boolean', 'string'].includes(typeof value)) || Object.entries(data).some(([key, value]) => typeof value === 'string' && value.length > (key === 'workspace' ? 4096 : 200))
|
|
45
|
+
|| (data.connectionId !== undefined && !validConnectionId(data.connectionId)))
|
|
39
46
|
return failure('连接请求格式不正确。');
|
|
40
47
|
if ((endpoint === 'create' && ['cards', 'loading'].some(key => data[key] !== undefined && typeof data[key] !== 'boolean'))
|
|
41
48
|
|| (endpoint === 'use-profile' && typeof data.name !== 'string')
|
|
42
|
-
|| (endpoint === 'confirm-owner' && typeof data.id !== 'string')
|
|
49
|
+
|| (endpoint === 'confirm-owner' && typeof data.id !== 'string')
|
|
50
|
+
|| (endpoint === 'enable-input' && data.kind !== 'images' && data.kind !== 'voice'))
|
|
43
51
|
return failure('连接请求格式不正确。');
|
|
52
|
+
if (endpoint === 'update-agent') {
|
|
53
|
+
try {
|
|
54
|
+
parseAgentSettings(data);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return failure('Agent 设置格式不正确。');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
44
60
|
try {
|
|
61
|
+
if (endpoint === 'connections' && controller.list)
|
|
62
|
+
return { ok: true, value: controller.list() };
|
|
63
|
+
if (endpoint === 'add-connection' && controller.add)
|
|
64
|
+
return { ok: true, value: await controller.add() };
|
|
65
|
+
if (endpoint === 'remove-connection' && controller.remove && typeof data.connectionId === 'string')
|
|
66
|
+
return { ok: true, value: await controller.remove(data.connectionId) };
|
|
67
|
+
if (endpoint === 'agent-catalog' && controller.catalog)
|
|
68
|
+
return { ok: true, value: await controller.catalog() };
|
|
69
|
+
if (manager)
|
|
70
|
+
return failure('当前连接服务不支持机器人管理,请检查插件版本。');
|
|
71
|
+
const { connectionId, ...fields } = data;
|
|
45
72
|
if (endpoint === 'status')
|
|
46
|
-
return { ok: true, value: controller.snapshot() };
|
|
73
|
+
return { ok: true, value: connectionId === undefined ? controller.snapshot() : controller.snapshot(connectionId) };
|
|
47
74
|
if (endpoint === 'profiles')
|
|
48
|
-
return { ok: true, value: await controller.profiles(signal) };
|
|
49
|
-
|
|
75
|
+
return { ok: true, value: await (connectionId === undefined ? controller.profiles(signal) : controller.profiles(signal, connectionId)) };
|
|
76
|
+
const command = endpoint;
|
|
77
|
+
return { ok: true, value: await (connectionId === undefined ? controller.command(command, fields) : controller.command(command, fields, connectionId)) };
|
|
50
78
|
}
|
|
51
79
|
catch (error) {
|
|
52
80
|
return failure(error instanceof SetupError ? error.message : '连接操作暂时失败,请检查本机服务后重试。');
|
|
@@ -72,7 +100,7 @@ export function registerConnectionRpc(rpc, controller, ownerRoutes) {
|
|
|
72
100
|
throw new ConnectionRegistrationCleanupError('Connection routes could not be removed.');
|
|
73
101
|
};
|
|
74
102
|
try {
|
|
75
|
-
for (const endpoint of
|
|
103
|
+
for (const endpoint of allEndpoints) {
|
|
76
104
|
disposers.push(ownerRoutes.register({ path: `/api/larkbot/${endpoint}`, methods: ['POST'], requestBody: 'buffered',
|
|
77
105
|
async fetch(request) {
|
|
78
106
|
const envelope = await readConnectionEnvelope(request, `larkbot/${endpoint}`);
|
|
@@ -227,23 +255,10 @@ export async function applyOnboarding(ctx, options = {}) {
|
|
|
227
255
|
return;
|
|
228
256
|
}
|
|
229
257
|
const executable = await findLarkExecutable(options.executable);
|
|
230
|
-
const controller = new
|
|
258
|
+
const controller = new ConnectionManager(directory, () => ({ platform: new LarkPlatform({ executable }), executable,
|
|
259
|
+
validateAgent: settings => validateAgentSettings(ctx, settings),
|
|
231
260
|
checkRuntime: () => requireRuntime(ctx),
|
|
232
|
-
preflight:
|
|
233
|
-
const policy = config.users[0];
|
|
234
|
-
const timeout = AbortSignal.timeout(45_000);
|
|
235
|
-
const combined = AbortSignal.any([signal, timeout]);
|
|
236
|
-
const session = await new DshRuntime(ctx).create({ ...policy, provider: config.provider, model: config.model });
|
|
237
|
-
try {
|
|
238
|
-
const result = await session.run('这是飞书连接测试,请只回复“连接成功”。', combined);
|
|
239
|
-
if (!(typeof result === 'string' ? result : result.text).trim())
|
|
240
|
-
throw new SetupError('模型没有返回内容,请检查模型设置后重试。');
|
|
241
|
-
await session.checkpoint();
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
await session.dispose();
|
|
245
|
-
}
|
|
246
|
-
},
|
|
261
|
+
preflight: (config, signal) => checkConnectionModel(ctx, config, signal),
|
|
247
262
|
start: async (config, signal, health, pairedChat) => {
|
|
248
263
|
signal.throwIfAborted();
|
|
249
264
|
const service = await createBotService(ctx, config, health, { directChat: pairedChat });
|
|
@@ -258,14 +273,16 @@ export async function applyOnboarding(ctx, options = {}) {
|
|
|
258
273
|
throw new BotServiceStartError(service);
|
|
259
274
|
throw error;
|
|
260
275
|
}
|
|
261
|
-
},
|
|
276
|
+
},
|
|
277
|
+
}), () => agentCatalog(ctx));
|
|
262
278
|
const installed = installedPackage();
|
|
263
279
|
const updates = new UpdateCheck(installed?.version, options.latestRelease
|
|
264
280
|
?? (signal => installed ? npmLatest(installed.name, signal) : Promise.resolve(undefined)));
|
|
265
281
|
const control = {
|
|
266
|
-
snapshot:
|
|
267
|
-
profiles: signal => controller.profiles(signal),
|
|
268
|
-
command: async (endpoint, payload) => withUpdate(await controller.command(endpoint, payload), updates),
|
|
282
|
+
snapshot: id => withUpdate(controller.snapshot(id), updates),
|
|
283
|
+
profiles: (signal, id) => controller.profiles(signal, id),
|
|
284
|
+
command: async (endpoint, payload, id) => withUpdate(await controller.command(endpoint, payload, id), updates),
|
|
285
|
+
list: () => controller.list(), add: () => controller.add(), remove: id => controller.remove(id), catalog: () => controller.catalog(),
|
|
269
286
|
};
|
|
270
287
|
let remove;
|
|
271
288
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { registerApp as sdkRegisterApp } from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import { type MediaInputKind } from '../media-scopes.js';
|
|
2
3
|
type RegisterAppOptions = Parameters<typeof sdkRegisterApp>[0];
|
|
3
4
|
type RegisterAppResult = Awaited<ReturnType<typeof sdkRegisterApp>>;
|
|
4
5
|
export type LarkBrand = 'feishu' | 'lark';
|
|
@@ -92,6 +93,11 @@ export declare class LarkPlatform {
|
|
|
92
93
|
url: string;
|
|
93
94
|
expireIn: number;
|
|
94
95
|
}) => void, signal: AbortSignal): Promise<void>;
|
|
96
|
+
enableInput(profile: string, kind: MediaInputKind, onQRCode: (info: {
|
|
97
|
+
url: string;
|
|
98
|
+
expireIn: number;
|
|
99
|
+
}) => void, signal: AbortSignal, expectedAppId: string): Promise<void>;
|
|
100
|
+
private authorizeScopes;
|
|
95
101
|
saveNewProfile(profile: string, app: RegisteredApp, signal?: AbortSignal): Promise<ProfileSummary>;
|
|
96
102
|
private profile;
|
|
97
103
|
private api;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { BOT_MENU_EVENT } from '../bot-menu.js';
|
|
3
|
-
import { DOCUMENT_SCOPES, STREAMING_SCOPE
|
|
3
|
+
import { DOCUMENT_SCOPES, STREAMING_SCOPE } from '../rich-scopes.js';
|
|
4
|
+
import { mediaScopes } from '../media-scopes.js';
|
|
4
5
|
export class LarkPlatformError extends Error {
|
|
5
6
|
code;
|
|
6
7
|
constructor(code) {
|
|
@@ -244,16 +245,26 @@ export class LarkPlatform {
|
|
|
244
245
|
}
|
|
245
246
|
/** Incremental, owner-confirmed authorization; returned credentials never leave this method. */
|
|
246
247
|
async enableDocuments(profile, onQRCode, signal) {
|
|
248
|
+
return this.authorizeScopes(profile, [...DOCUMENT_SCOPES, STREAMING_SCOPE], onQRCode, signal);
|
|
249
|
+
}
|
|
250
|
+
async enableInput(profile, kind, onQRCode, signal, expectedAppId) {
|
|
251
|
+
if ((kind !== 'images' && kind !== 'voice') || !validId(expectedAppId, 'cli'))
|
|
252
|
+
fail('INVALID_INPUT');
|
|
253
|
+
return this.authorizeScopes(profile, mediaScopes(kind), onQRCode, signal, expectedAppId);
|
|
254
|
+
}
|
|
255
|
+
async authorizeScopes(profile, scopes, onQRCode, signal, expectedAppId) {
|
|
247
256
|
const selected = await this.profile(profile, signal);
|
|
257
|
+
if (expectedAppId !== undefined && selected.appId !== expectedAppId)
|
|
258
|
+
fail('APP_MISMATCH');
|
|
248
259
|
try {
|
|
249
260
|
const current = await this.inspect(profile, signal);
|
|
250
261
|
if (current.appId !== selected.appId)
|
|
251
262
|
fail('APP_MISMATCH');
|
|
252
263
|
// Another host may already have completed the upgrade; never ask for another scan just to refresh UI state.
|
|
253
|
-
if (current.scopeStatus === 'verified' &&
|
|
264
|
+
if (current.scopeStatus === 'verified' && scopes.every(scope => current.grantedScopes.includes(scope)))
|
|
254
265
|
return;
|
|
255
266
|
const result = await this.registrar({ source: 'dsh-larkbot', appId: selected.appId, signal,
|
|
256
|
-
addons: { preset: false, scopes: { tenant: [...
|
|
267
|
+
addons: { preset: false, scopes: { tenant: [...scopes] } },
|
|
257
268
|
onQRCodeReady: info => {
|
|
258
269
|
checkSignal(signal);
|
|
259
270
|
let url;
|
|
@@ -274,7 +285,7 @@ export class LarkPlatform {
|
|
|
274
285
|
if (result.client_id !== selected.appId || (result.user_info?.tenant_brand && result.user_info.tenant_brand !== selected.brand))
|
|
275
286
|
fail('APP_MISMATCH');
|
|
276
287
|
const inspection = await this.inspect(profile, signal);
|
|
277
|
-
if (inspection.appId !== selected.appId || inspection.scopeStatus !== 'verified' || !
|
|
288
|
+
if (inspection.appId !== selected.appId || inspection.scopeStatus !== 'verified' || !scopes.every(scope => inspection.grantedScopes.includes(scope)))
|
|
278
289
|
fail('MISSING_SCOPE');
|
|
279
290
|
}
|
|
280
291
|
catch (error) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type OnboardingDependencies } from './controller.js';
|
|
2
|
+
import { ConnectionStore } from './store.js';
|
|
3
|
+
import type { AgentCatalog, ConnectionEndpoint, ConnectionList, ConnectionSnapshot } from './protocol.js';
|
|
4
|
+
export declare const MAX_CONNECTIONS = 10;
|
|
5
|
+
export declare const validConnectionId: (id: unknown) => id is string;
|
|
6
|
+
/** Owns independent controllers; the legacy controller and its native logs never move. */
|
|
7
|
+
export declare class ConnectionManager {
|
|
8
|
+
private readonly directory;
|
|
9
|
+
private readonly dependencies;
|
|
10
|
+
private readonly readCatalog;
|
|
11
|
+
private readonly controllers;
|
|
12
|
+
private readonly registry;
|
|
13
|
+
private readonly claims;
|
|
14
|
+
private tail;
|
|
15
|
+
private closed;
|
|
16
|
+
constructor(directory: string, dependencies: (store: ConnectionStore) => Omit<OnboardingDependencies, 'store'>, readCatalog: () => Promise<AgentCatalog>);
|
|
17
|
+
private make;
|
|
18
|
+
initialize(): Promise<void>;
|
|
19
|
+
private get;
|
|
20
|
+
snapshot(id?: string): ConnectionSnapshot;
|
|
21
|
+
list(): ConnectionList;
|
|
22
|
+
catalog(): Promise<AgentCatalog>;
|
|
23
|
+
profiles(signal?: AbortSignal, id?: string): Promise<import("./protocol.js").ConnectionProfile[]>;
|
|
24
|
+
command(endpoint: Exclude<ConnectionEndpoint, 'profiles' | 'status'>, payload: unknown, id?: string): Promise<ConnectionSnapshot>;
|
|
25
|
+
add(): Promise<ConnectionList & {
|
|
26
|
+
selectedId: string;
|
|
27
|
+
}>;
|
|
28
|
+
remove(id: string): Promise<ConnectionList & {
|
|
29
|
+
selectedId: string;
|
|
30
|
+
}>;
|
|
31
|
+
dispose(): Promise<boolean>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ConnectionController, SetupError } from './controller.js';
|
|
4
|
+
import { ConnectionStore } from './store.js';
|
|
5
|
+
import { SettingsStore } from './settings-store.js';
|
|
6
|
+
export const MAX_CONNECTIONS = 10;
|
|
7
|
+
export const validConnectionId = (id) => typeof id === 'string' && (id === 'default' || /^[a-f0-9]{32}$/.test(id));
|
|
8
|
+
function ids(raw) {
|
|
9
|
+
const value = raw;
|
|
10
|
+
if (value?.version !== 1 || !Array.isArray(value.ids) || !value.ids.length || value.ids.length > MAX_CONNECTIONS
|
|
11
|
+
|| value.ids[0] !== 'default' || !value.ids.every(validConnectionId) || new Set(value.ids).size !== value.ids.length)
|
|
12
|
+
throw new Error('Invalid connection registry');
|
|
13
|
+
return { version: 1, ids: [...value.ids] };
|
|
14
|
+
}
|
|
15
|
+
/** Owns independent controllers; the legacy controller and its native logs never move. */
|
|
16
|
+
export class ConnectionManager {
|
|
17
|
+
directory;
|
|
18
|
+
dependencies;
|
|
19
|
+
readCatalog;
|
|
20
|
+
controllers = new Map();
|
|
21
|
+
registry;
|
|
22
|
+
claims = new Map();
|
|
23
|
+
tail = Promise.resolve();
|
|
24
|
+
closed = false;
|
|
25
|
+
constructor(directory, dependencies, readCatalog) {
|
|
26
|
+
this.directory = directory;
|
|
27
|
+
this.dependencies = dependencies;
|
|
28
|
+
this.readCatalog = readCatalog;
|
|
29
|
+
this.registry = new SettingsStore(directory, 'connections.json', ids);
|
|
30
|
+
}
|
|
31
|
+
make(id) {
|
|
32
|
+
const directory = id === 'default' ? this.directory : join(this.directory, 'connections', id);
|
|
33
|
+
const store = new ConnectionStore(directory);
|
|
34
|
+
return new ConnectionController({ ...this.dependencies(store), store, reserveApp: appId => {
|
|
35
|
+
const owner = this.claims.get(appId);
|
|
36
|
+
if (owner && owner !== id)
|
|
37
|
+
throw new SetupError('这个机器人已在列表中,请直接打开对应的机器人。', false);
|
|
38
|
+
this.claims.set(appId, id);
|
|
39
|
+
}, commitApp: appId => {
|
|
40
|
+
for (const [reserved, owner] of this.claims)
|
|
41
|
+
if (owner === id && reserved !== appId)
|
|
42
|
+
this.claims.delete(reserved);
|
|
43
|
+
} });
|
|
44
|
+
}
|
|
45
|
+
async initialize() {
|
|
46
|
+
const registry = await this.registry.read() ?? { version: 1, ids: ['default'] };
|
|
47
|
+
// Reserve all saved identities before any enabled controller starts receiving.
|
|
48
|
+
for (const id of registry.ids) {
|
|
49
|
+
const store = new ConnectionStore(id === 'default' ? this.directory : join(this.directory, 'connections', id));
|
|
50
|
+
const record = await store.read();
|
|
51
|
+
if (record) {
|
|
52
|
+
if (this.claims.has(record.appId))
|
|
53
|
+
throw new SetupError('机器人列表有重复身份,请检查本地连接状态。', false);
|
|
54
|
+
this.claims.set(record.appId, id);
|
|
55
|
+
}
|
|
56
|
+
this.controllers.set(id, this.make(id));
|
|
57
|
+
}
|
|
58
|
+
for (const controller of this.controllers.values())
|
|
59
|
+
await controller.initialize();
|
|
60
|
+
}
|
|
61
|
+
get(id = 'default') {
|
|
62
|
+
if (this.closed || !validConnectionId(id))
|
|
63
|
+
throw new SetupError('机器人入口不可用,请刷新列表。');
|
|
64
|
+
const controller = this.controllers.get(id);
|
|
65
|
+
if (!controller)
|
|
66
|
+
throw new SetupError('没有找到这个机器人,请刷新列表。');
|
|
67
|
+
return controller;
|
|
68
|
+
}
|
|
69
|
+
snapshot(id) { return this.get(id).snapshot(); }
|
|
70
|
+
list() {
|
|
71
|
+
return { limit: MAX_CONNECTIONS, connections: [...this.controllers].map(([id, controller], index) => {
|
|
72
|
+
const snapshot = controller.snapshot();
|
|
73
|
+
return { id, label: snapshot.agent?.label || snapshot.bot?.name || `飞书助手 ${index + 1}`,
|
|
74
|
+
phase: snapshot.phase, busy: snapshot.busy, agentPreset: snapshot.agent?.agentPreset ?? '' };
|
|
75
|
+
}) };
|
|
76
|
+
}
|
|
77
|
+
catalog() { return this.readCatalog(); }
|
|
78
|
+
profiles(signal, id) { return this.get(id).profiles(signal); }
|
|
79
|
+
command(endpoint, payload, id) {
|
|
80
|
+
// Serialize roster changes with command admission, before a controller can acquire a service.
|
|
81
|
+
const result = this.tail.then(() => this.get(id).command(endpoint, payload));
|
|
82
|
+
this.tail = result.catch(() => undefined);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
add() {
|
|
86
|
+
const result = this.tail.then(async () => {
|
|
87
|
+
if (this.closed)
|
|
88
|
+
throw new SetupError('连接设置已关闭。');
|
|
89
|
+
if (this.controllers.size >= MAX_CONNECTIONS)
|
|
90
|
+
throw new SetupError(`最多管理 ${MAX_CONNECTIONS} 个机器人。`);
|
|
91
|
+
const id = randomUUID().replaceAll('-', '');
|
|
92
|
+
const controller = this.make(id);
|
|
93
|
+
await controller.initialize();
|
|
94
|
+
// Publish the new controller only after its roster entry is durable.
|
|
95
|
+
await this.registry.write({ version: 1, ids: [...this.controllers.keys(), id] });
|
|
96
|
+
this.controllers.set(id, controller);
|
|
97
|
+
return { ...this.list(), selectedId: id };
|
|
98
|
+
});
|
|
99
|
+
this.tail = result.catch(() => undefined);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
remove(id) {
|
|
103
|
+
const result = this.tail.then(async () => {
|
|
104
|
+
const controller = this.get(id), snapshot = controller.snapshot();
|
|
105
|
+
if (id === 'default')
|
|
106
|
+
throw new SetupError('首个机器人入口会保留,可以断开后复用。');
|
|
107
|
+
if (!snapshot.agent?.editable || snapshot.documentSetup || snapshot.inputSetup)
|
|
108
|
+
throw new SetupError('请先暂停或取消这个机器人的连接和授权,再从列表移除。');
|
|
109
|
+
if (!await controller.dispose())
|
|
110
|
+
throw new SetupError('连接尚未安全停止,机器人入口已保留。');
|
|
111
|
+
await this.registry.write({ version: 1, ids: [...this.controllers.keys()].filter(key => key !== id) });
|
|
112
|
+
this.controllers.delete(id);
|
|
113
|
+
for (const [appId, owner] of this.claims)
|
|
114
|
+
if (owner === id)
|
|
115
|
+
this.claims.delete(appId);
|
|
116
|
+
// Its durable folder and CLI credentials are retained for manual recovery.
|
|
117
|
+
return { ...this.list(), selectedId: 'default' };
|
|
118
|
+
});
|
|
119
|
+
this.tail = result.catch(() => undefined);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
async dispose() {
|
|
123
|
+
this.closed = true;
|
|
124
|
+
await this.tail;
|
|
125
|
+
const results = await Promise.allSettled([...this.controllers.values()].map(controller => controller.dispose()));
|
|
126
|
+
return results.every(result => result.status === 'fulfilled' && result.value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { BotConfig } from '../config.js';
|
|
3
|
+
export declare const CONNECTION_TEST_PROMPT = "\u8FD9\u662F\u98DE\u4E66\u8FDE\u63A5\u6D4B\u8BD5\uFF0C\u8BF7\u53EA\u56DE\u590D\u201C\u8FDE\u63A5\u6210\u529F\u201D\u3002";
|
|
4
|
+
/** One model call, without an Agent or a durable conversation to clean up. */
|
|
5
|
+
export declare function checkConnectionModel(ctx: Context, config: Pick<BotConfig, 'provider' | 'model'>, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { SetupError } from './controller.js';
|
|
3
|
+
export const CONNECTION_TEST_PROMPT = '这是飞书连接测试,请只回复“连接成功”。';
|
|
4
|
+
/** One model call, without an Agent or a durable conversation to clean up. */
|
|
5
|
+
export async function checkConnectionModel(ctx, config, signal) {
|
|
6
|
+
const timeout = AbortSignal.timeout(45_000);
|
|
7
|
+
const combined = AbortSignal.any([signal, timeout]);
|
|
8
|
+
try {
|
|
9
|
+
combined.throwIfAborted();
|
|
10
|
+
const selection = config.provider && config.model ? { provider: config.provider, model: config.model }
|
|
11
|
+
: ctx.get('agentDefaultModel')?.currentSelection?.();
|
|
12
|
+
const llm = ctx.get('llm');
|
|
13
|
+
if (!selection?.provider || !selection.model || typeof llm?.stream !== 'function') {
|
|
14
|
+
throw new SetupError('请先在 dsh 的模型设置中选择可用模型,然后回来继续连接飞书。');
|
|
15
|
+
}
|
|
16
|
+
let text = false, finished = false;
|
|
17
|
+
for await (const chunk of llm.stream({
|
|
18
|
+
provider: selection.provider, model: selection.model,
|
|
19
|
+
...(selection.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(selection.reasoningEffort) }),
|
|
20
|
+
messages: [createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: CONNECTION_TEST_PROMPT }] })],
|
|
21
|
+
tools: [], signal: combined,
|
|
22
|
+
})) {
|
|
23
|
+
combined.throwIfAborted();
|
|
24
|
+
if (chunk.type === 'text-delta')
|
|
25
|
+
text ||= Boolean(chunk.text.trim());
|
|
26
|
+
if (chunk.type === 'block-end' && chunk.block.type === 'text')
|
|
27
|
+
text ||= Boolean(chunk.block.text.trim());
|
|
28
|
+
if (chunk.type === 'finish') {
|
|
29
|
+
if (chunk.reason.kind === 'error' || chunk.reason.kind === 'aborted' || chunk.reason.kind === 'tool-calls') {
|
|
30
|
+
throw new SetupError('模型连接测试失败,请检查模型设置后重试。');
|
|
31
|
+
}
|
|
32
|
+
finished = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
combined.throwIfAborted();
|
|
36
|
+
if (!finished)
|
|
37
|
+
throw new SetupError('模型连接测试未完成,请检查模型设置后重试。');
|
|
38
|
+
if (!text)
|
|
39
|
+
throw new SetupError('模型没有返回内容,请检查模型设置后重试。');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (signal.aborted)
|
|
43
|
+
throw new SetupError('连接已取消。');
|
|
44
|
+
if (timeout.aborted)
|
|
45
|
+
throw new SetupError('模型连接测试超时,请检查模型设置后重试。');
|
|
46
|
+
if (error instanceof SetupError)
|
|
47
|
+
throw error;
|
|
48
|
+
throw new SetupError('模型连接测试失败,请检查模型设置后重试。');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=model-check.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MediaInputKind } from '../media-scopes.js';
|
|
1
2
|
/** Owner-only UI contract. Credentials and raw platform errors never belong here. */
|
|
2
3
|
export type ConnectionPhase = 'idle' | 'creating' | 'waiting_scan' | 'configuring' | 'action_required' | 'pairing' | 'confirming_owner' | 'starting' | 'connected' | 'paused' | 'error';
|
|
3
4
|
export interface ConnectionSnapshot {
|
|
@@ -30,6 +31,8 @@ export interface ConnectionSnapshot {
|
|
|
30
31
|
cards: boolean;
|
|
31
32
|
loading: boolean;
|
|
32
33
|
documents?: boolean;
|
|
34
|
+
images?: boolean;
|
|
35
|
+
voice?: boolean;
|
|
33
36
|
};
|
|
34
37
|
documentSetup?: {
|
|
35
38
|
phase: 'preparing' | 'scanning' | 'error';
|
|
@@ -40,6 +43,16 @@ export interface ConnectionSnapshot {
|
|
|
40
43
|
expiresAt: number;
|
|
41
44
|
};
|
|
42
45
|
};
|
|
46
|
+
inputSetup?: {
|
|
47
|
+
kind: MediaInputKind;
|
|
48
|
+
phase: 'preparing' | 'scanning' | 'error';
|
|
49
|
+
message: string;
|
|
50
|
+
qr?: {
|
|
51
|
+
url: string;
|
|
52
|
+
image: string;
|
|
53
|
+
expiresAt: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
43
56
|
/** Bottom-menu entries the published app lacks, and where its owner adds them. */
|
|
44
57
|
menu?: {
|
|
45
58
|
missing: Array<{
|
|
@@ -54,13 +67,50 @@ export interface ConnectionSnapshot {
|
|
|
54
67
|
current: string;
|
|
55
68
|
latest: string;
|
|
56
69
|
};
|
|
70
|
+
agent?: AgentSettings & {
|
|
71
|
+
workspaceDisplay: string;
|
|
72
|
+
editable: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Per-robot choices. Empty preset/workspace follows the host/default workspace. */
|
|
76
|
+
export interface AgentSettings {
|
|
77
|
+
label: string;
|
|
78
|
+
agentPreset: string;
|
|
79
|
+
workspace: string;
|
|
80
|
+
imageInput: boolean;
|
|
81
|
+
voiceInput: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface AgentCatalog {
|
|
84
|
+
presets: Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
available: boolean;
|
|
89
|
+
}>;
|
|
90
|
+
presetsAvailable: boolean;
|
|
91
|
+
defaultPreset?: string;
|
|
92
|
+
model: string;
|
|
93
|
+
imagesAvailable: boolean;
|
|
94
|
+
voiceAvailable: boolean;
|
|
95
|
+
modelImages?: 'supported' | 'unsupported' | 'unknown';
|
|
96
|
+
}
|
|
97
|
+
export interface ConnectionList {
|
|
98
|
+
connections: Array<{
|
|
99
|
+
id: string;
|
|
100
|
+
label: string;
|
|
101
|
+
phase: ConnectionPhase;
|
|
102
|
+
busy: boolean;
|
|
103
|
+
agentPreset: string;
|
|
104
|
+
}>;
|
|
105
|
+
limit: number;
|
|
57
106
|
}
|
|
107
|
+
export type ManagerEndpoint = 'connections' | 'add-connection' | 'remove-connection' | 'agent-catalog';
|
|
58
108
|
export interface ConnectionProfile {
|
|
59
109
|
name: string;
|
|
60
110
|
appId: string;
|
|
61
111
|
label: string;
|
|
62
112
|
}
|
|
63
|
-
export type ConnectionEndpoint = 'status' | 'profiles' | 'create' | 'use-profile' | 'recheck' | 'cancel' | 'renew-pairing' | 'confirm-owner' | 'pause' | 'resume' | 'disconnect' | 'enable-documents' | 'cancel-documents';
|
|
113
|
+
export type ConnectionEndpoint = 'status' | 'profiles' | 'create' | 'use-profile' | 'recheck' | 'cancel' | 'renew-pairing' | 'confirm-owner' | 'pause' | 'resume' | 'disconnect' | 'enable-documents' | 'cancel-documents' | 'enable-input' | 'cancel-input' | 'update-agent';
|
|
64
114
|
export type ConnectionResult<T> = {
|
|
65
115
|
ok: true;
|
|
66
116
|
value: T;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A failed publication poisons this writer until the owner reloads from disk. */
|
|
2
|
+
export declare class SettingsStore<T> {
|
|
3
|
+
private readonly directory;
|
|
4
|
+
private readonly filename;
|
|
5
|
+
private readonly parse;
|
|
6
|
+
private unavailable;
|
|
7
|
+
private tail;
|
|
8
|
+
constructor(directory: string, filename: string, parse: (raw: unknown) => T);
|
|
9
|
+
read(): Promise<T | undefined>;
|
|
10
|
+
write(value: T): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
/** A failed publication poisons this writer until the owner reloads from disk. */
|
|
5
|
+
export class SettingsStore {
|
|
6
|
+
directory;
|
|
7
|
+
filename;
|
|
8
|
+
parse;
|
|
9
|
+
unavailable = false;
|
|
10
|
+
tail = Promise.resolve();
|
|
11
|
+
constructor(directory, filename, parse) {
|
|
12
|
+
this.directory = directory;
|
|
13
|
+
this.filename = filename;
|
|
14
|
+
this.parse = parse;
|
|
15
|
+
}
|
|
16
|
+
async read() {
|
|
17
|
+
await this.tail;
|
|
18
|
+
if (this.unavailable)
|
|
19
|
+
throw new Error('Settings publication is uncertain');
|
|
20
|
+
try {
|
|
21
|
+
return this.parse(JSON.parse(await readFile(join(this.directory, this.filename), 'utf8')));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === 'ENOENT')
|
|
25
|
+
return;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
write(value) {
|
|
30
|
+
const safe = this.parse(value);
|
|
31
|
+
const job = this.tail.then(async () => {
|
|
32
|
+
if (this.unavailable)
|
|
33
|
+
throw new Error('Settings publication is uncertain');
|
|
34
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
35
|
+
const temp = join(this.directory, `${this.filename}.${randomUUID()}.tmp`);
|
|
36
|
+
let publishing = false;
|
|
37
|
+
try {
|
|
38
|
+
const file = await open(temp, 'wx', 0o600);
|
|
39
|
+
try {
|
|
40
|
+
await file.writeFile(JSON.stringify(safe) + '\n');
|
|
41
|
+
await file.sync();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await file.close();
|
|
45
|
+
}
|
|
46
|
+
publishing = true;
|
|
47
|
+
await rename(temp, join(this.directory, this.filename));
|
|
48
|
+
const directory = await open(this.directory, 'r');
|
|
49
|
+
try {
|
|
50
|
+
await directory.sync();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await directory.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (publishing)
|
|
58
|
+
this.unavailable = true;
|
|
59
|
+
await unlink(temp).catch(() => undefined);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this.tail = job.catch(() => undefined);
|
|
64
|
+
return job;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=settings-store.js.map
|
package/dist/replies.d.ts
CHANGED
|
@@ -44,6 +44,22 @@ export interface ReplyPanel {
|
|
|
44
44
|
}[];
|
|
45
45
|
actions?: ReplyAction[];
|
|
46
46
|
actionColumns?: 1 | 2 | 3;
|
|
47
|
+
/** Trusted catalog controls; option values and callbacks are supplied by the application. */
|
|
48
|
+
select?: {
|
|
49
|
+
placeholder: string;
|
|
50
|
+
options: {
|
|
51
|
+
label: string;
|
|
52
|
+
value: string;
|
|
53
|
+
}[];
|
|
54
|
+
value: Record<string, string>;
|
|
55
|
+
initialOption?: string;
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
};
|
|
58
|
+
search?: {
|
|
59
|
+
query: string;
|
|
60
|
+
value: Record<string, string>;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
};
|
|
47
63
|
note?: string;
|
|
48
64
|
}
|
|
49
65
|
/**
|