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.
Files changed (51) hide show
  1. package/README.md +10 -5
  2. package/dist/bot-service.js +4 -2
  3. package/dist/bridge.d.ts +27 -1
  4. package/dist/bridge.js +333 -66
  5. package/dist/card-actions.js +3 -1
  6. package/dist/client/AgentSettings.d.ts +12 -0
  7. package/dist/client/ConnectionCard.d.ts +5 -1
  8. package/dist/client/ManagerPanel.d.ts +3 -0
  9. package/dist/client/api.d.ts +14 -2
  10. package/dist/client/index.d.ts +1 -1
  11. package/dist/client/manager-styles.d.ts +2 -0
  12. package/dist/client/use-connection.d.ts +2 -2
  13. package/dist/client.cjs +727 -258
  14. package/dist/config.d.ts +4 -0
  15. package/dist/config.js +8 -0
  16. package/dist/dsh-runtime.d.ts +3 -0
  17. package/dist/dsh-runtime.js +46 -4
  18. package/dist/lark-cli.d.ts +20 -1
  19. package/dist/lark-cli.js +168 -2
  20. package/dist/media-input.d.ts +28 -0
  21. package/dist/media-input.js +127 -0
  22. package/dist/media-scopes.d.ts +5 -0
  23. package/dist/media-scopes.js +6 -0
  24. package/dist/message.d.ts +7 -0
  25. package/dist/message.js +27 -3
  26. package/dist/onboarding/agents.d.ts +9 -0
  27. package/dist/onboarding/agents.js +100 -0
  28. package/dist/onboarding/controller.d.ts +15 -2
  29. package/dist/onboarding/controller.js +155 -14
  30. package/dist/onboarding/host.d.ts +14 -2
  31. package/dist/onboarding/host.js +49 -32
  32. package/dist/onboarding/lark-platform.d.ts +6 -0
  33. package/dist/onboarding/lark-platform.js +15 -4
  34. package/dist/onboarding/manager.d.ts +32 -0
  35. package/dist/onboarding/manager.js +129 -0
  36. package/dist/onboarding/model-check.d.ts +5 -0
  37. package/dist/onboarding/model-check.js +51 -0
  38. package/dist/onboarding/protocol.d.ts +51 -1
  39. package/dist/onboarding/settings-store.d.ts +11 -0
  40. package/dist/onboarding/settings-store.js +67 -0
  41. package/dist/replies.d.ts +16 -0
  42. package/dist/replies.js +37 -11
  43. package/dist/session-controls.d.ts +34 -0
  44. package/dist/session-controls.js +66 -0
  45. package/dist/session-store.d.ts +7 -1
  46. package/dist/session-store.js +44 -11
  47. package/dist/topic-cards.d.ts +3 -0
  48. package/dist/topic-cards.js +8 -0
  49. package/dist/topic-store.d.ts +40 -0
  50. package/dist/topic-store.js +192 -0
  51. package/package.json +1 -1
@@ -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, DshRuntime } from '../dsh-runtime.js';
7
+ import { checkRuntimeCompatibility } from '../dsh-runtime.js';
8
8
  import { acquireLock, LockHeldError } from '../lock.js';
9
- import { ConnectionController, SetupError } from './controller.js';
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 (!endpoints.has(endpoint))
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 allowed = endpoint === 'create' ? ['cards', 'loading'] : endpoint === 'use-profile' ? ['name'] : endpoint === 'confirm-owner' ? ['id'] : [];
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
- return { ok: true, value: await controller.command(endpoint, payload) };
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 endpoints) {
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 ConnectionController({ store: new ConnectionStore(directory), platform: new LarkPlatform({ executable }), executable,
258
+ const controller = new ConnectionManager(directory, () => ({ platform: new LarkPlatform({ executable }), executable,
259
+ validateAgent: settings => validateAgentSettings(ctx, settings),
231
260
  checkRuntime: () => requireRuntime(ctx),
232
- preflight: async (config, signal) => {
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: () => withUpdate(controller.snapshot(), updates),
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, defaultScopesReady } from '../rich-scopes.js';
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' && defaultScopesReady(current.grantedScopes))
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: [...DOCUMENT_SCOPES, STREAMING_SCOPE] } },
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' || !defaultScopesReady(inspection.grantedScopes))
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
  /**