@addozhang/dsh-discord 0.3.0 → 0.5.0-alpha.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 +13 -13
- package/README.zh.md +13 -13
- package/cordis.patch.yml +1 -1
- package/lib/client/card-controller.d.ts +1 -1
- package/lib/client/card-form.d.ts +2 -1
- package/lib/client/index.d.ts +2 -2
- package/lib/client/slot-contract.d.ts +7 -0
- package/lib/client/snapshot-store.d.ts +1 -1
- package/lib/compose.d.ts +22 -2
- package/lib/compose.js +9 -4
- package/lib/dsh/host-asks.d.ts +90 -0
- package/lib/dsh/host-asks.js +143 -0
- package/lib/dsh/host-events.d.ts +42 -0
- package/lib/dsh/host-events.js +234 -0
- package/lib/dsh/host-face.d.ts +418 -0
- package/lib/dsh/host-face.js +598 -0
- package/lib/features/adapter-status.d.ts +15 -2
- package/lib/features/adapter-status.js +100 -4
- package/lib/features/approval-store.d.ts +1 -1
- package/lib/features/image-collection.d.ts +11 -0
- package/lib/features/image-collection.js +22 -8
- package/lib/features/image-download.d.ts +6 -0
- package/lib/features/image-download.js +21 -0
- package/lib/features/interaction-router.d.ts +2 -2
- package/lib/features/model-control.d.ts +1 -1
- package/lib/features/project-list.d.ts +1 -1
- package/lib/features/project-list.js +1 -1
- package/lib/features/prompt-submission.d.ts +9 -0
- package/lib/features/prompt-submission.js +7 -1
- package/lib/features/session-mainline.d.ts +42 -1
- package/lib/features/session-mainline.js +29 -3
- package/lib/features/session-resume.d.ts +1 -1
- package/lib/gateway/inbound.d.ts +14 -0
- package/lib/gateway/inbound.js +41 -0
- package/lib/i18n.d.ts +1 -0
- package/lib/i18n.js +2 -0
- package/lib/index.js +142 -65
- package/lib/settings.js +5 -2
- package/lib/startup.d.ts +1 -1
- package/lib/startup.js +14 -7
- package/lib/stream/live.d.ts +0 -37
- package/lib/stream/live.js +10 -58
- package/lib/stream/tool-view.d.ts +12 -2
- package/lib/stream/tool-view.js +32 -2
- package/package.json +10 -12
- package/lib/dsh/api-proxy-face.d.ts +0 -390
- package/lib/dsh/api-proxy-face.js +0 -510
- package/lib/features/image-submission.d.ts +0 -67
- package/lib/features/image-submission.js +0 -57
package/lib/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { DEFAULT_DISCORD_SETTINGS, DiscordSettingsSchema, installDiscordSettings, normalizeDiscordSettings, } from './settings.js';
|
|
2
|
-
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
2
|
import { installCancellationRoot } from './lifecycle.js';
|
|
4
3
|
import { ALLOWED_MENTIONS_NONE, DISCORD_SUPPRESS_NOTIFICATIONS_FLAG, OUTBOUND_EPHEMERAL_FLAGS, safeTitle } from './policy/disclosure.js';
|
|
5
4
|
import { validateHostCapabilities } from './startup.js';
|
|
@@ -10,7 +9,7 @@ import { createRestThreadPort } from './discord/thread-port.js';
|
|
|
10
9
|
import { createComponentRegistry } from './discord/components.js';
|
|
11
10
|
import { buildCommandRegistrations } from './discord/commands.js';
|
|
12
11
|
import { startDiscordAdapter } from './compose.js';
|
|
13
|
-
import { createModelPort, createWorkspaceCatalogPort, createWorkspaceResolver, readWorkspaceDetail, promptSession, createSessionViaProxy, cancelSessionViaProxy, steerSession, removeQueueItemViaProxy } from './dsh/
|
|
12
|
+
import { createModelPort, createWorkspaceCatalogPort, createWorkspaceResolver, readWorkspaceDetail, promptSession, createSessionViaProxy, cancelSessionViaProxy, steerSession, removeQueueItemViaProxy, resolveHostFace } from './dsh/host-face.js';
|
|
14
13
|
import { createApprovalStore } from './features/approval-store.js';
|
|
15
14
|
import { createAskWiring } from './features/ask-wiring.js';
|
|
16
15
|
import { sweepExpiredApprovals } from './features/approval-expiry.js';
|
|
@@ -22,7 +21,9 @@ import { createInteractionRouter } from './features/interaction-router.js';
|
|
|
22
21
|
import { channelBindingKey, parseChannelBindingKey, threadBindingKey, parseThreadBindingKey, discordDomainSpec, CHANNEL_BINDINGS_TABLE, THREAD_BINDINGS_TABLE, INTENTS_TABLE } from './state/domain.js';
|
|
23
22
|
import { planBindingReconciliation } from './features/reconcile-bindings.js';
|
|
24
23
|
import { guildKeysToForget, sweepExpired } from './state/retention.js';
|
|
25
|
-
import {
|
|
24
|
+
import { createHostEventRouter } from './dsh/host-events.js';
|
|
25
|
+
import { listSessionIds, listSessionSummaries } from './dsh/host-face.js';
|
|
26
|
+
import { installAskServicePatches } from './dsh/host-asks.js';
|
|
26
27
|
import { createBindingStore } from './state/bindings.js';
|
|
27
28
|
import { createIntentStore } from './state/intents.js';
|
|
28
29
|
import { createTurnTracker } from './features/turn-ownership.js';
|
|
@@ -31,12 +32,13 @@ import { createSessionCreationFlow } from './features/session-creation.js';
|
|
|
31
32
|
import { createResumeCandidatesPort } from './features/session-resume.js';
|
|
32
33
|
import { createPromptSubmissionFlow } from './features/prompt-submission.js';
|
|
33
34
|
import { createSessionMainline, requestIdFor } from './features/session-mainline.js';
|
|
35
|
+
import { collectImages, createSafeImageDownloadPort } from './features/image-collection.js';
|
|
34
36
|
import { startLiveRender } from './stream/live.js';
|
|
35
37
|
import { planWorkspaceChannel, workspaceChannelName } from './features/workspace-channel.js';
|
|
36
38
|
/** Stable Cordis plugin name for diagnostics. */
|
|
37
39
|
export const name = 'dsh-discord';
|
|
38
40
|
/** Host services required by the complete embedded adapter. */
|
|
39
|
-
export const inject = ['
|
|
41
|
+
export const inject = ['sessionController', 'workspaceController', 'sessionQuery', 'webServer', 'credentials', 'settings', 'storageDomain', 'connection'];
|
|
40
42
|
export const Config = DiscordSettingsSchema;
|
|
41
43
|
/**
|
|
42
44
|
* Fixed Milestone 1 Gateway intents: guilds, messages, content. Member data
|
|
@@ -74,7 +76,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
74
76
|
const resolveLanguage = () => {
|
|
75
77
|
if (current.language !== 'auto')
|
|
76
78
|
return current.language;
|
|
77
|
-
const locale = ctx.get('settings').get(
|
|
79
|
+
const locale = ctx.get('settings').get('locale');
|
|
78
80
|
return typeof locale?.preference === 'string' && locale.preference.startsWith('zh') ? 'zh' : 'en';
|
|
79
81
|
};
|
|
80
82
|
const copy = new Proxy({}, {
|
|
@@ -101,15 +103,20 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
101
103
|
// Late-bound so the management channel can reach the runtime built inside
|
|
102
104
|
// the composition IIFE below (the card's Connect button).
|
|
103
105
|
const runtimeRef = { current: undefined };
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
// Channel registration mounts a prefix route through our OWN context
|
|
107
|
+
// effect (see installAdapterStatusRpc: the host's connection.rpc.handle
|
|
108
|
+
// is owner-scoped and unusable from an external plugin).
|
|
109
|
+
{
|
|
110
|
+
installAdapterStatusRpc(ctx, statusTracker, {
|
|
111
|
+
tracker: statusTracker,
|
|
112
|
+
setToken: async (value) => {
|
|
113
|
+
await ctx.get('credentials').set(DISCORD_BOT_TOKEN_REF, value);
|
|
114
|
+
statusTracker.setCredential({ configured: true });
|
|
115
|
+
},
|
|
116
|
+
connect: () => { runtimeRef.current?.connect(); },
|
|
117
|
+
disconnect: () => { runtimeRef.current?.disconnect(); },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
113
120
|
void describeDiscordCredential(ctx.get('credentials'))
|
|
114
121
|
.then((view) => { statusTracker.setCredential(view); })
|
|
115
122
|
.catch((cause) => {
|
|
@@ -117,10 +124,10 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
117
124
|
statusTracker.setCredential({ configured: false });
|
|
118
125
|
});
|
|
119
126
|
const credentials = ctx.get('credentials');
|
|
120
|
-
// The in-process
|
|
121
|
-
//
|
|
122
|
-
// logging live in src/dsh/
|
|
123
|
-
const
|
|
127
|
+
// The in-process 0.1.6 controller face: plain requests in, plain values
|
|
128
|
+
// out, business rejections as thrown RemoteErrors; boundedness and outcome
|
|
129
|
+
// logging live in src/dsh/host-face.ts.
|
|
130
|
+
const dsh = resolveHostFace(ctx);
|
|
124
131
|
// Default-quiet logging (16.33): flow records ride the Host's debug level
|
|
125
132
|
// only — the adapter prints nothing into the DSH process at the default
|
|
126
133
|
// level. Failure-shaped events (…failed/…threw/…blocked/…unknown) escalate
|
|
@@ -132,13 +139,16 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
132
139
|
}
|
|
133
140
|
emitLog(ctx, /(?:failed|threw|blocked|unknown)/.test(event) ? 'warn' : 'debug', { event, detail: serialized });
|
|
134
141
|
};
|
|
142
|
+
// The 0.1.6 event bridge (declared early: every session-acquisition site
|
|
143
|
+
// below tracks into it; the live renderer consumes its fan-in stream).
|
|
144
|
+
const hostEvents = createHostEventRouter(dsh.session, { log: rpcLog });
|
|
135
145
|
void (async () => {
|
|
136
146
|
// The durable domain gates the whole composition: no adapter starts
|
|
137
147
|
// without its bindings/intents tables, and close() drains queued writes
|
|
138
148
|
// on teardown (fail-closed across restarts, design §10).
|
|
139
149
|
const domain = await ctx.storageDomain.open(discordDomainSpec);
|
|
140
150
|
ctx.effect(() => () => { void domain.close(); }, 'discord durable domain');
|
|
141
|
-
const catalogPort = createWorkspaceCatalogPort(
|
|
151
|
+
const catalogPort = createWorkspaceCatalogPort(dsh, { log: rpcLog });
|
|
142
152
|
const policy = () => ({
|
|
143
153
|
allowedGuildIds: [...current.allowedGuildIds],
|
|
144
154
|
memberUserIds: [...current.memberUserIds],
|
|
@@ -179,7 +189,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
179
189
|
const sharedRegistry = createComponentRegistry();
|
|
180
190
|
const questionCancelPort = {
|
|
181
191
|
cancel: async ({ sessionId }) => {
|
|
182
|
-
const cancelled = await cancelSessionViaProxy(
|
|
192
|
+
const cancelled = await cancelSessionViaProxy(dsh, { sessionId }, { log: rpcLog });
|
|
183
193
|
const turn = turnTracker.active(sessionId);
|
|
184
194
|
if (turn !== undefined)
|
|
185
195
|
turnTracker.complete(turn.requestId);
|
|
@@ -283,10 +293,50 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
283
293
|
};
|
|
284
294
|
})();
|
|
285
295
|
const dshSessionPort = {
|
|
286
|
-
createSession: request =>
|
|
296
|
+
createSession: async (request) => {
|
|
297
|
+
const created = await createSessionViaProxy(dsh, request, { log: rpcLog });
|
|
298
|
+
if (created.outcome === 'completed')
|
|
299
|
+
hostEvents.track(created.sessionId);
|
|
300
|
+
return created;
|
|
301
|
+
},
|
|
287
302
|
};
|
|
288
303
|
const dshPromptPort = {
|
|
289
|
-
submit:
|
|
304
|
+
submit: async (request) => {
|
|
305
|
+
const submitted = await promptSession(dsh, {
|
|
306
|
+
sessionId: request.sessionId,
|
|
307
|
+
prompt: request.prompt,
|
|
308
|
+
...(request.images === undefined ? {} : { images: request.images }),
|
|
309
|
+
}, { log: rpcLog, rpcId: request.requestId });
|
|
310
|
+
if (submitted.outcome === 'accepted')
|
|
311
|
+
hostEvents.track(request.sessionId);
|
|
312
|
+
return submitted;
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* The bounded image collection boundary (16.50): downloads ride the
|
|
317
|
+
* safe-download port (allowlisted CDN hosts, capped sizes, timeout) and
|
|
318
|
+
* come back base64-encoded for the prompt parts.
|
|
319
|
+
*/
|
|
320
|
+
const IMAGE_DOWNLOAD_TIMEOUT_MS = 15_000;
|
|
321
|
+
const imageCollector = {
|
|
322
|
+
collect: async ({ attachments }) => {
|
|
323
|
+
const result = await collectImages(createSafeImageDownloadPort(), {
|
|
324
|
+
nowMs: Date.now(),
|
|
325
|
+
timeoutMs: IMAGE_DOWNLOAD_TIMEOUT_MS,
|
|
326
|
+
attachments,
|
|
327
|
+
});
|
|
328
|
+
if (result.outcome !== 'collected') {
|
|
329
|
+
const reason = result.outcome === 'too-large' ? `too-large:${result.reason}` : result.outcome;
|
|
330
|
+
return { outcome: 'failed', reason };
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
outcome: 'collected',
|
|
334
|
+
images: result.downloaded.map(image => ({
|
|
335
|
+
mediaType: image.mediaType,
|
|
336
|
+
base64: Buffer.from(image.body).toString('base64'),
|
|
337
|
+
})),
|
|
338
|
+
};
|
|
339
|
+
},
|
|
290
340
|
};
|
|
291
341
|
const composedMainline = createSessionMainline({
|
|
292
342
|
threads: createThreadCreationFlow({
|
|
@@ -305,6 +355,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
305
355
|
nowMs: () => Date.now(),
|
|
306
356
|
}),
|
|
307
357
|
turns: turnTracker,
|
|
358
|
+
images: imageCollector,
|
|
308
359
|
});
|
|
309
360
|
/** Mainline outcome → copy key; the string resolves live per language. */
|
|
310
361
|
const MAINLINE_FAILURE_KEYS = {
|
|
@@ -314,6 +365,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
314
365
|
'session-unknown': 'mainlineSessionUnknown',
|
|
315
366
|
'prompt-rejected': 'mainlinePromptRejected',
|
|
316
367
|
'prompt-unknown': 'mainlinePromptUnknown',
|
|
368
|
+
'image-failed': 'mainlineImageFailed',
|
|
317
369
|
};
|
|
318
370
|
const mainline = {
|
|
319
371
|
admitMention: async (request) => {
|
|
@@ -334,9 +386,13 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
334
386
|
},
|
|
335
387
|
continueInThread: async (request) => {
|
|
336
388
|
const result = await composedMainline.continueInThread(request);
|
|
337
|
-
if (result.outcome === 'rejected' || result.outcome === 'unknown') {
|
|
389
|
+
if (result.outcome === 'rejected' || result.outcome === 'unknown' || result.outcome === 'image-failed') {
|
|
338
390
|
rpcLog('discord_continuation_not_queued', { messageId: request.messageId, outcome: result.outcome });
|
|
339
|
-
const notice = result.outcome === 'rejected'
|
|
391
|
+
const notice = result.outcome === 'rejected'
|
|
392
|
+
? copy.continuationRejected
|
|
393
|
+
: result.outcome === 'image-failed'
|
|
394
|
+
? copy.mainlineImageFailed
|
|
395
|
+
: copy.continuationUnknown;
|
|
340
396
|
void withRest(rest => rest.request('POST', `/channels/${request.threadId}/messages`, { content: notice, flags: DISCORD_SUPPRESS_NOTIFICATIONS_FLAG, allowed_mentions: ALLOWED_MENTIONS_NONE }))
|
|
341
397
|
.catch((cause) => { rpcLog('discord_mainline_notice_failed', String(cause)); });
|
|
342
398
|
}
|
|
@@ -346,7 +402,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
346
402
|
// Bind provision resolves the selection against the live catalog; the
|
|
347
403
|
// Discord confirm button carries only opaque registry ids between the
|
|
348
404
|
// command and the write.
|
|
349
|
-
const resolver = createWorkspaceResolver(
|
|
405
|
+
const resolver = createWorkspaceResolver(dsh, { log: rpcLog });
|
|
350
406
|
// Adapter-owned guild surfaces: the "DeepSeek Harness" category, the
|
|
351
407
|
// general control channel (both provisioned on READY), and — on bind —
|
|
352
408
|
// one channel per Workspace, named after its display title. Nothing
|
|
@@ -500,7 +556,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
500
556
|
rpcLog('discord_reconcile_blocked', 'workspace-catalog-unavailable');
|
|
501
557
|
return;
|
|
502
558
|
}
|
|
503
|
-
const sessionIds = await listSessionIds(
|
|
559
|
+
const sessionIds = await listSessionIds(dsh, { log: rpcLog });
|
|
504
560
|
if (sessionIds.outcome !== 'completed') {
|
|
505
561
|
rpcLog('discord_reconcile_blocked', 'session-list-unavailable');
|
|
506
562
|
return;
|
|
@@ -633,13 +689,44 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
633
689
|
}).catch((cause) => { rpcLog('discord_question_expiry_threw', String(cause)); });
|
|
634
690
|
}, 30_000);
|
|
635
691
|
ctx.effect(() => () => { clearInterval(expiryTimer); }, 'discord interaction expiry sweep');
|
|
636
|
-
|
|
637
|
-
|
|
692
|
+
const threadForSession = (sessionId) => {
|
|
693
|
+
for (const [key, record] of threadTable.entries()) {
|
|
694
|
+
if (record.sessionId !== sessionId)
|
|
695
|
+
continue;
|
|
696
|
+
const scope = parseThreadBindingKey(key);
|
|
697
|
+
if (scope !== undefined)
|
|
698
|
+
return scope.threadId;
|
|
699
|
+
}
|
|
700
|
+
return undefined;
|
|
701
|
+
};
|
|
702
|
+
// DSH ask answerers (0.1.6 composed-approval model): the waterfalls
|
|
703
|
+
// dispatch on the BASE TREE's event bus (the profile composes one event
|
|
704
|
+
// tree per bundle and a waterfall only enumerates its own registry), so
|
|
705
|
+
// the listeners register there — resolved off a base service's context.
|
|
706
|
+
// Ask carrier: wrap the base-tree ask services at the method boundary
|
|
707
|
+
// (the composed-answerer waterfalls are not reachable from an external
|
|
708
|
+
// plugin's registrations — see host-asks.ts).
|
|
709
|
+
const approvalService = ctx.get('approval');
|
|
710
|
+
const questionsService = ctx.get('userQuestions');
|
|
711
|
+
if (approvalService === undefined)
|
|
712
|
+
throw new TypeError('dsh-discord cannot reach the approval service');
|
|
713
|
+
const hostAsksRef = { current: undefined };
|
|
714
|
+
hostAsksRef.current = installAskServicePatches(approvalService, questionsService, {
|
|
715
|
+
threadForSession,
|
|
716
|
+
askWiring,
|
|
717
|
+
approvalTimeoutMs: () => current.approvalTimeoutMs,
|
|
718
|
+
questionTimeoutMs: () => current.questionTimeoutMs,
|
|
719
|
+
nowMs: () => Date.now(),
|
|
720
|
+
log: rpcLog,
|
|
721
|
+
});
|
|
722
|
+
ctx.effect(() => () => { hostAsksRef.current?.dispose(); }, 'dsh-discord host ask answerers');
|
|
638
723
|
const approvalRespondPort = {
|
|
639
|
-
respond:
|
|
640
|
-
const
|
|
641
|
-
rpcLog('
|
|
642
|
-
|
|
724
|
+
respond: ({ approvalId, outcome }) => {
|
|
725
|
+
const settled = hostAsksRef.current?.settleApproval(approvalId, outcome) ?? false;
|
|
726
|
+
rpcLog('discord_approval_settled', { approvalId, outcome, settled });
|
|
727
|
+
// The settle promise IS the confirmation: an in-process resolution
|
|
728
|
+
// has no wire receipt to doubt.
|
|
729
|
+
return Promise.resolve(settled ? { outcome: 'confirmed' } : { outcome: 'rejected', reason: 'not-pending' });
|
|
643
730
|
},
|
|
644
731
|
};
|
|
645
732
|
const queueSnapshots = new Map();
|
|
@@ -657,10 +744,10 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
657
744
|
rpcLog('discord_guild_forget_deleted', { guildId, channels: plan.channelKeys.length, threads: plan.threadKeys.length });
|
|
658
745
|
};
|
|
659
746
|
const questionRespondPort = {
|
|
660
|
-
respond:
|
|
661
|
-
const
|
|
662
|
-
rpcLog('
|
|
663
|
-
return
|
|
747
|
+
respond: (input) => {
|
|
748
|
+
const settled = hostAsksRef.current?.settleQuestion(input.rpcId, input.answer) ?? false;
|
|
749
|
+
rpcLog('discord_question_settled', { rpcId: input.rpcId, settled });
|
|
750
|
+
return Promise.resolve(settled ? { outcome: 'confirmed' } : { outcome: 'rejected', reason: 'not-pending' });
|
|
664
751
|
},
|
|
665
752
|
};
|
|
666
753
|
const questionRoutingDeps = {
|
|
@@ -685,10 +772,15 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
685
772
|
handleQuestionComponent: input => handleSelectInput(questionRoutingDeps, input),
|
|
686
773
|
handleQuestionModal: input => handleModalSubmit(questionRoutingDeps, input),
|
|
687
774
|
dsh: {
|
|
688
|
-
cancel: sessionId => cancelSessionViaProxy(
|
|
689
|
-
steer: (sessionId, prompt) =>
|
|
690
|
-
|
|
691
|
-
|
|
775
|
+
cancel: sessionId => cancelSessionViaProxy(dsh, { sessionId }, { log: rpcLog }),
|
|
776
|
+
steer: async (sessionId, prompt) => {
|
|
777
|
+
const steered = await steerSession(dsh, { sessionId, prompt }, { log: rpcLog });
|
|
778
|
+
if (steered.outcome === 'accepted')
|
|
779
|
+
hostEvents.track(sessionId);
|
|
780
|
+
return steered;
|
|
781
|
+
},
|
|
782
|
+
removeQueueItem: (sessionId, itemId) => removeQueueItemViaProxy(dsh, { sessionId, itemId }, { log: rpcLog }),
|
|
783
|
+
readWorkspaceDetail: reference => readWorkspaceDetail(dsh, reference, { log: rpcLog }),
|
|
692
784
|
},
|
|
693
785
|
catalogPort,
|
|
694
786
|
resolver,
|
|
@@ -701,10 +793,10 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
701
793
|
await channelTable.delete(bindChannelKey(guildId, channelId));
|
|
702
794
|
rpcLog('discord_reconcile_channel_retired', { channelId, reason: 'discord-deleted' });
|
|
703
795
|
},
|
|
704
|
-
model: createModelPort(
|
|
796
|
+
model: createModelPort(dsh, { log: rpcLog }),
|
|
705
797
|
modelSelectOperatorOnly: () => current.modelSelectOperatorOnly,
|
|
706
798
|
resumeCandidates: createResumeCandidatesPort({
|
|
707
|
-
listSessions: () => listSessionSummaries(
|
|
799
|
+
listSessions: () => listSessionSummaries(dsh, { log: rpcLog }),
|
|
708
800
|
boundSessionIds: () => {
|
|
709
801
|
const bound = new Set();
|
|
710
802
|
for (const [, record] of threadTable.entries())
|
|
@@ -735,7 +827,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
735
827
|
return { outcome: 'refused-control-channel' };
|
|
736
828
|
}
|
|
737
829
|
}
|
|
738
|
-
const summaries = await listSessionSummaries(
|
|
830
|
+
const summaries = await listSessionSummaries(dsh, { log: rpcLog });
|
|
739
831
|
if (summaries.outcome !== 'completed')
|
|
740
832
|
return { outcome: 'failed' };
|
|
741
833
|
const summary = summaries.sessions.find(candidate => candidate.sessionId === sessionId);
|
|
@@ -786,6 +878,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
786
878
|
createdAtMs: Date.now(),
|
|
787
879
|
});
|
|
788
880
|
rpcLog('discord_session_resumed', { sessionId, threadId: thread.body.id, title });
|
|
881
|
+
hostEvents.track(sessionId);
|
|
789
882
|
return { outcome: 'started', threadId: thread.body.id };
|
|
790
883
|
},
|
|
791
884
|
log: rpcLog,
|
|
@@ -898,28 +991,15 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
898
991
|
void registerCommands().catch((cause) => {
|
|
899
992
|
emitLog(ctx, 'warn', { event: 'discord_command_register_failed', cause: String(cause) });
|
|
900
993
|
});
|
|
901
|
-
// ── Live streaming:
|
|
994
|
+
// ── Live streaming: host follow/control fan-in → per-thread rendering ──
|
|
902
995
|
// The queue snapshot cache (declared with the interaction router) is the
|
|
903
|
-
// /queue surface's data source:
|
|
904
|
-
// authoritative whole-snapshot
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (record.sessionId !== sessionId)
|
|
908
|
-
continue;
|
|
909
|
-
const scope = parseThreadBindingKey(key);
|
|
910
|
-
if (scope !== undefined)
|
|
911
|
-
return scope.threadId;
|
|
912
|
-
}
|
|
913
|
-
return undefined;
|
|
914
|
-
};
|
|
996
|
+
// /queue surface's data source: the 0.1.6 `session/control` stream owns
|
|
997
|
+
// the authoritative whole-snapshot queue frames.
|
|
998
|
+
// The 0.1.6 event bridge: every session the adapter tracks (create,
|
|
999
|
+
// adopt, resume, prompt) joins one fan-in stream of live frames.
|
|
915
1000
|
const liveRef = { current: undefined };
|
|
916
1001
|
liveRef.current = startLiveRender({
|
|
917
|
-
frames:
|
|
918
|
-
const events = apiProxy.events;
|
|
919
|
-
if (events === undefined)
|
|
920
|
-
throw new TypeError('apiProxy.events is unavailable on this Host');
|
|
921
|
-
return events.mux({ rpcId: crypto.randomUUID(), payload: {} }, signal);
|
|
922
|
-
},
|
|
1002
|
+
frames: signal => hostEvents.stream(signal),
|
|
923
1003
|
threadForSession,
|
|
924
1004
|
delivery: {
|
|
925
1005
|
send: async (request) => {
|
|
@@ -998,8 +1078,6 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
998
1078
|
},
|
|
999
1079
|
updateIntervalMs: current.streamUpdateIntervalMs,
|
|
1000
1080
|
typingIntervalMs: current.typingIntervalMs,
|
|
1001
|
-
approvalTimeoutMs: current.approvalTimeoutMs,
|
|
1002
|
-
questionTimeoutMs: current.questionTimeoutMs,
|
|
1003
1081
|
verbosity: current.defaultVerbosity,
|
|
1004
1082
|
log: rpcLog,
|
|
1005
1083
|
interruptedMarker: () => copy.interruptedMarker,
|
|
@@ -1010,7 +1088,6 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
1010
1088
|
turnTracker.complete(turn.requestId);
|
|
1011
1089
|
rpcLog('discord_turn_ended', { sessionId, hadActiveTurn: turn !== undefined });
|
|
1012
1090
|
},
|
|
1013
|
-
requests: askWiring,
|
|
1014
1091
|
});
|
|
1015
1092
|
ctx.effect(() => () => { liveRef.current?.dispose(); }, 'dsh-discord live render');
|
|
1016
1093
|
ctx.effect(() => () => { runtimeRef.current?.dispose(); }, 'dsh-discord composed runtime');
|
package/lib/settings.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { installSettingsSection } from '@deepseek-ai/dsh-settings';
|
|
2
1
|
import z from '@deepseek-ai/schemastery';
|
|
3
2
|
import { DISCORD_SETTINGS_NAMESPACE } from './settings-namespace.js';
|
|
4
3
|
export { DISCORD_SETTINGS_NAMESPACE } from './settings-namespace.js';
|
|
@@ -92,7 +91,11 @@ export function validateDiscordSettings(input) {
|
|
|
92
91
|
}
|
|
93
92
|
export function installDiscordSettings(ctx, entry, onChange) {
|
|
94
93
|
let source = () => entry;
|
|
95
|
-
|
|
94
|
+
const provider = ctx.get('settings');
|
|
95
|
+
if (provider === undefined || typeof provider.installSection !== 'function') {
|
|
96
|
+
throw new TypeError('settings provider with installSection is unavailable on this Host');
|
|
97
|
+
}
|
|
98
|
+
provider.installSection(ctx, DISCORD_SETTINGS_NAMESPACE, DiscordSettingsSchema, entry, {
|
|
96
99
|
validate: value => { validateDiscordSettings(normalizeDiscordSettings(value)); },
|
|
97
100
|
setSource: current => {
|
|
98
101
|
source = () => normalizeDiscordSettings(current());
|
package/lib/startup.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* with a single actionable diagnostic naming every gap.
|
|
7
7
|
*/
|
|
8
8
|
/** The exact Host service roster the adapter requires (design decision 1). */
|
|
9
|
-
export declare const REQUIRED_HOST_SERVICES: readonly ["
|
|
9
|
+
export declare const REQUIRED_HOST_SERVICES: readonly ["sessionController", "workspaceController", "sessionQuery", "webServer", "credentials", "settings", "storageDomain", "connection"];
|
|
10
10
|
export type HostServiceName = (typeof REQUIRED_HOST_SERVICES)[number];
|
|
11
11
|
/**
|
|
12
12
|
* Validate every required Host capability through `resolveService`, which
|
package/lib/startup.js
CHANGED
|
@@ -7,26 +7,33 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** The exact Host service roster the adapter requires (design decision 1). */
|
|
9
9
|
export const REQUIRED_HOST_SERVICES = [
|
|
10
|
-
'
|
|
10
|
+
'sessionController',
|
|
11
|
+
'workspaceController',
|
|
12
|
+
'sessionQuery',
|
|
13
|
+
'webServer',
|
|
11
14
|
'credentials',
|
|
12
15
|
'settings',
|
|
13
16
|
'storageDomain',
|
|
14
17
|
'connection',
|
|
15
18
|
];
|
|
16
19
|
/**
|
|
17
|
-
* Minimum contract members per service, confirmed against the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Minimum contract members per service, confirmed against the 0.1.6
|
|
21
|
+
* controller faces (dsh-api-session-controller / dsh-api-workspace-controller
|
|
22
|
+
* / dsh-session-query). A service lacking any member means a version drift
|
|
23
|
+
* the startup probe should have refused; refuse rather than probe at runtime.
|
|
20
24
|
*/
|
|
21
25
|
const REQUIRED_CONTRACT_MEMBERS = {
|
|
22
|
-
|
|
26
|
+
sessionController: ['prompt', 'create', 'list', 'cancel', 'updateQueue', 'selectModel', 'modelCatalog', 'follow', 'control'],
|
|
27
|
+
workspaceController: ['follow'],
|
|
28
|
+
sessionQuery: ['observeSession'],
|
|
29
|
+
webServer: [],
|
|
23
30
|
credentials: ['resolve', 'describe', 'set', 'unset'],
|
|
24
|
-
settings: ['
|
|
31
|
+
settings: ['installSection'],
|
|
25
32
|
storageDomain: ['open'],
|
|
26
33
|
connection: ['rpc'],
|
|
27
34
|
};
|
|
28
35
|
/** Actionable remediation hint appended to every activation failure. */
|
|
29
|
-
const REMEDIATION = 'install @addozhang/dsh-discord into a dsh web profile providing the
|
|
36
|
+
const REMEDIATION = 'install @addozhang/dsh-discord into a dsh web profile providing the 0.1.6 host controller services';
|
|
30
37
|
function missingMembers(service, required) {
|
|
31
38
|
if (service === undefined || service === null)
|
|
32
39
|
return [...required];
|
package/lib/stream/live.d.ts
CHANGED
|
@@ -84,10 +84,6 @@ export interface LiveRenderDeps {
|
|
|
84
84
|
threadName?: (channelId: string) => Promise<string | undefined>;
|
|
85
85
|
updateIntervalMs: number;
|
|
86
86
|
typingIntervalMs: number;
|
|
87
|
-
/** Approval ask deadline (approvalTimeoutMs setting). */
|
|
88
|
-
approvalTimeoutMs: number;
|
|
89
|
-
/** Question ask deadline (questionTimeoutMs setting). */
|
|
90
|
-
questionTimeoutMs: number;
|
|
91
87
|
/** Coalescing budget for tool-activity edits (default 1s). */
|
|
92
88
|
activityCoalesceMs?: number;
|
|
93
89
|
verbosity?: DiscordVerbosity;
|
|
@@ -101,39 +97,6 @@ export interface LiveRenderDeps {
|
|
|
101
97
|
interruptedMarker?: () => string;
|
|
102
98
|
/** Turn ownership release on turn/end. */
|
|
103
99
|
onTurnEnded?: (sessionId: string) => void;
|
|
104
|
-
/**
|
|
105
|
-
* Answerable server-request frames (interaction-routing spec): approvals
|
|
106
|
-
* and questions arrive as mux frames with an envelope rpcId the response
|
|
107
|
-
* must echo. The composition owns rendering, ownership, and expiry.
|
|
108
|
-
*/
|
|
109
|
-
requests?: {
|
|
110
|
-
onApprovalRequested(input: {
|
|
111
|
-
sessionId: string;
|
|
112
|
-
threadId: string;
|
|
113
|
-
rpcId: string;
|
|
114
|
-
approvalId: string;
|
|
115
|
-
toolName: string;
|
|
116
|
-
reason?: string | undefined;
|
|
117
|
-
expiresAtMs: number;
|
|
118
|
-
}): void;
|
|
119
|
-
onApprovalResolved(input: {
|
|
120
|
-
sessionId: string;
|
|
121
|
-
approvalId: string;
|
|
122
|
-
outcome?: string | undefined;
|
|
123
|
-
}): void;
|
|
124
|
-
onQuestionRequested(input: {
|
|
125
|
-
sessionId: string;
|
|
126
|
-
threadId: string;
|
|
127
|
-
rpcId: string;
|
|
128
|
-
questions: Array<Record<string, unknown>>;
|
|
129
|
-
expiresAtMs: number;
|
|
130
|
-
}): void;
|
|
131
|
-
onQuestionResolved(input: {
|
|
132
|
-
sessionId: string;
|
|
133
|
-
questionRpcId: string;
|
|
134
|
-
outcome: 'answered' | 'cancelled';
|
|
135
|
-
}): void;
|
|
136
|
-
};
|
|
137
100
|
}
|
|
138
101
|
export declare function startLiveRender(deps: LiveRenderDeps): {
|
|
139
102
|
dispose(): void;
|
package/lib/stream/live.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createToolActivitySurface } from './tool-view.js';
|
|
|
15
15
|
import { createAnswerFinalizer } from './finalizer.js';
|
|
16
16
|
import { buildOutboundMessage } from './outbound.js';
|
|
17
17
|
import { toolCategoryIcon } from './icons.js';
|
|
18
|
+
import { shellCommandTitle } from './tool-view.js';
|
|
18
19
|
import { discordChannelNameKey, safeTitle } from '../policy/disclosure.js';
|
|
19
20
|
/** Coalescing budget for activity-message edits under parallel tools. */
|
|
20
21
|
const DEFAULT_ACTIVITY_COALESCE_MS = 1_000;
|
|
@@ -313,7 +314,8 @@ export function startLiveRender(deps) {
|
|
|
313
314
|
case 'tool/call': {
|
|
314
315
|
if (typeof data['callId'] !== 'string' || typeof data['name'] !== 'string')
|
|
315
316
|
return;
|
|
316
|
-
const
|
|
317
|
+
const rawArguments = typeof data['arguments'] === 'string' ? data['arguments'] : undefined;
|
|
318
|
+
const title = presentationTitle(frameView) ?? shellCommandTitle(data['name'], rawArguments);
|
|
317
319
|
runtime.toolNames.set(data['callId'], data['name']);
|
|
318
320
|
if (title !== undefined)
|
|
319
321
|
runtime.toolTitles.set(data['callId'], title);
|
|
@@ -322,7 +324,7 @@ export function startLiveRender(deps) {
|
|
|
322
324
|
toolName: data['name'],
|
|
323
325
|
state: 'running',
|
|
324
326
|
title,
|
|
325
|
-
rawArguments
|
|
327
|
+
rawArguments,
|
|
326
328
|
});
|
|
327
329
|
runtime.activityScheduler?.schedule(renderActivityContent(runtime));
|
|
328
330
|
return;
|
|
@@ -375,7 +377,7 @@ export function startLiveRender(deps) {
|
|
|
375
377
|
.join(' '), 120);
|
|
376
378
|
return { id, summary: text === '' ? '(非文本消息)' : text };
|
|
377
379
|
}
|
|
378
|
-
function handleFrame(raw
|
|
380
|
+
function handleFrame(raw) {
|
|
379
381
|
const frame = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
380
382
|
const type = frame['type'];
|
|
381
383
|
const sessionId = typeof frame['sessionId'] === 'string' ? frame['sessionId'] : undefined;
|
|
@@ -436,52 +438,8 @@ export function startLiveRender(deps) {
|
|
|
436
438
|
});
|
|
437
439
|
return;
|
|
438
440
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (threadId === undefined || deps.requests === undefined)
|
|
442
|
-
return;
|
|
443
|
-
deps.requests.onApprovalRequested({
|
|
444
|
-
sessionId,
|
|
445
|
-
threadId,
|
|
446
|
-
rpcId: envelopeRpcId ?? '',
|
|
447
|
-
approvalId: typeof frame['approvalId'] === 'string' ? frame['approvalId'] : '',
|
|
448
|
-
toolName: typeof frame['toolName'] === 'string' ? frame['toolName'] : 'tool',
|
|
449
|
-
reason: typeof frame['reason'] === 'string' ? frame['reason'] : undefined,
|
|
450
|
-
expiresAtMs: Date.now() + deps.approvalTimeoutMs,
|
|
451
|
-
});
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (type === 'approval/resolved') {
|
|
455
|
-
deps.requests?.onApprovalResolved({
|
|
456
|
-
sessionId,
|
|
457
|
-
approvalId: typeof frame['approvalId'] === 'string' ? frame['approvalId'] : '',
|
|
458
|
-
outcome: typeof frame['outcome'] === 'string' ? frame['outcome'] : undefined,
|
|
459
|
-
});
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
if (type === 'question/requested') {
|
|
463
|
-
const threadId = deps.threadForSession(sessionId);
|
|
464
|
-
if (threadId === undefined || deps.requests === undefined)
|
|
465
|
-
return;
|
|
466
|
-
const questions = Array.isArray(frame['questions']) ? frame['questions'] : [];
|
|
467
|
-
deps.requests.onQuestionRequested({
|
|
468
|
-
sessionId,
|
|
469
|
-
threadId,
|
|
470
|
-
rpcId: envelopeRpcId ?? '',
|
|
471
|
-
questions,
|
|
472
|
-
expiresAtMs: Date.now() + deps.questionTimeoutMs,
|
|
473
|
-
});
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (type === 'question/resolved') {
|
|
477
|
-
const outcome = frame['outcome'] === 'cancelled' ? 'cancelled' : 'answered';
|
|
478
|
-
deps.requests?.onQuestionResolved({
|
|
479
|
-
sessionId,
|
|
480
|
-
questionRpcId: typeof frame['questionRpcId'] === 'string' ? frame['questionRpcId'] : '',
|
|
481
|
-
outcome,
|
|
482
|
-
});
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
441
|
+
// Answerable asks no longer ride the frame stream: the 0.1.6 host routes
|
|
442
|
+
// them through the composed-answerer waterfalls (src/dsh/host-asks.ts).
|
|
485
443
|
if (type !== 'session/event')
|
|
486
444
|
return;
|
|
487
445
|
const threadId = deps.threadForSession(sessionId);
|
|
@@ -508,18 +466,12 @@ export function startLiveRender(deps) {
|
|
|
508
466
|
if (isDisposed())
|
|
509
467
|
return;
|
|
510
468
|
try {
|
|
511
|
-
const envelopeRpcId = (typeof frame === 'object' && frame !== null && 'rpcId' in frame)
|
|
512
|
-
? frame.rpcId
|
|
513
|
-
: undefined;
|
|
514
|
-
const payload = (typeof frame === 'object' && frame !== null && 'payload' in frame)
|
|
515
|
-
? frame.payload
|
|
516
|
-
: frame;
|
|
517
469
|
if (TRACE) {
|
|
518
|
-
const t = (typeof
|
|
519
|
-
const sid = (typeof
|
|
470
|
+
const t = (typeof frame === 'object' && frame !== null ? frame.type : undefined);
|
|
471
|
+
const sid = (typeof frame === 'object' && frame !== null ? frame.sessionId : undefined);
|
|
520
472
|
trace('frame', String(t), String(sid));
|
|
521
473
|
}
|
|
522
|
-
handleFrame(
|
|
474
|
+
handleFrame(frame);
|
|
523
475
|
}
|
|
524
476
|
catch (cause) {
|
|
525
477
|
deps.log?.('discord_live_frame_threw', { cause: String(cause) });
|