@addozhang/dsh-discord 0.2.3 → 0.4.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 +4 -3
- package/README.zh.md +4 -3
- package/lib/compose.d.ts +20 -0
- package/lib/compose.js +8 -3
- package/lib/discord/commands.js +6 -0
- package/lib/dsh/api-proxy-face.d.ts +59 -9
- package/lib/dsh/api-proxy-face.js +67 -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 +42 -0
- package/lib/features/interaction-router.js +79 -6
- package/lib/features/project-list.d.ts +1 -0
- 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 +64 -30
- package/lib/features/session-resume.js +108 -35
- package/lib/gateway/inbound.d.ts +14 -0
- package/lib/gateway/inbound.js +41 -0
- package/lib/i18n.d.ts +14 -0
- package/lib/i18n.js +32 -0
- package/lib/index.js +128 -5
- package/package.json +13 -2
- package/lib/features/image-submission.d.ts +0 -67
- package/lib/features/image-submission.js +0 -57
package/lib/i18n.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ declare const zh: {
|
|
|
78
78
|
mainlineSessionRejected: string;
|
|
79
79
|
mainlineSessionUnknown: string;
|
|
80
80
|
mainlinePromptRejected: string;
|
|
81
|
+
mainlineImageFailed: string;
|
|
81
82
|
mainlinePromptUnknown: string;
|
|
82
83
|
continuationRejected: string;
|
|
83
84
|
continuationUnknown: string;
|
|
@@ -109,6 +110,19 @@ declare const zh: {
|
|
|
109
110
|
modelNotInCatalog: string;
|
|
110
111
|
modelInvalidReasoning: string;
|
|
111
112
|
modelTypedParseFailed: string;
|
|
113
|
+
sessionResumeNeedsBoundChannel: string;
|
|
114
|
+
sessionResumeStarted: (threadId: string) => string;
|
|
115
|
+
sessionResumeAlreadyBound: (threadId: string) => string;
|
|
116
|
+
sessionResumeFailed: string;
|
|
117
|
+
sessionResumeAnchor: (title: string) => string;
|
|
118
|
+
sessionResumeControlChannel: string;
|
|
119
|
+
sessionResumeSubagent: string;
|
|
120
|
+
sessionResumeArchived: string;
|
|
121
|
+
sessionCandidateJustNow: string;
|
|
122
|
+
sessionCandidateMinutesAgo: (minutes: number) => string;
|
|
123
|
+
sessionCandidateHoursAgo: (hours: number) => string;
|
|
124
|
+
sessionCandidateDaysAgo: (days: number) => string;
|
|
125
|
+
sessionCandidateRunning: string;
|
|
112
126
|
approvalRequired: (label: string) => string;
|
|
113
127
|
};
|
|
114
128
|
export type CopyTable = typeof zh;
|
package/lib/i18n.js
CHANGED
|
@@ -84,6 +84,7 @@ const zh = {
|
|
|
84
84
|
mainlineSessionRejected: '⚠️ DSH 拒绝了会话创建(工作区可能已失效);请稍后重试或重新 /project bind。',
|
|
85
85
|
mainlineSessionUnknown: '⚠️ 会话创建结果未知;请到 DSH Web 确认后再重试,不会自动重复创建。',
|
|
86
86
|
mainlinePromptRejected: '⚠️ DSH 拒绝了任务提交。',
|
|
87
|
+
mainlineImageFailed: '⚠️ 附件图片获取失败,任务未提交;请重试或改用文字描述。',
|
|
87
88
|
mainlinePromptUnknown: '⚠️ 任务提交结果未知;为避免重复执行不会自动重发,请确认后重试。',
|
|
88
89
|
continuationRejected: '⚠️ 消息提交被 DSH 拒绝。',
|
|
89
90
|
continuationUnknown: '⚠️ 消息提交结果未知;不会自动重发,请确认后重试。',
|
|
@@ -118,6 +119,22 @@ const zh = {
|
|
|
118
119
|
modelNotInCatalog: '⚠️ 该 provider/模型不在当前会话的目录中。',
|
|
119
120
|
modelInvalidReasoning: '⚠️ 该推理强度对此模型无效。',
|
|
120
121
|
modelTypedParseFailed: '⚠️ 模型需按 `provider/model` 格式填写,或留空进入交互式选择。',
|
|
122
|
+
// ── /session resume ──────────────────────────────────────────────────
|
|
123
|
+
sessionResumeNeedsBoundChannel: '⚠️ /session resume 需要在已绑定工作区的项目频道中使用。',
|
|
124
|
+
sessionResumeStarted: (threadId) => `✅ 会话已恢复到 <#${threadId}>——历史在 Web 界面查看,线程内直接续聊。`,
|
|
125
|
+
sessionResumeAlreadyBound: (threadId) => `该会话已在 <#${threadId}> 中。`,
|
|
126
|
+
sessionResumeFailed: '⚠️ 会话恢复失败,请稍后重试。',
|
|
127
|
+
sessionResumeAnchor: (title) => `📌 恢复会话:${title}`,
|
|
128
|
+
sessionResumeControlChannel: 'general 是控制频道,不承载会话——请到工作区频道使用 /session resume。',
|
|
129
|
+
sessionResumeSubagent: '⚠️ 这是 subagent 会话,不能恢复为顶层线程——可在 Web 界面查看。',
|
|
130
|
+
sessionResumeArchived: '⚠️ 该会话已归档,恢复后不会有任何运行——请在 Web 界面先取消归档。',
|
|
131
|
+
// Discord autocomplete choices carry no description field, so the age and
|
|
132
|
+
// running marker ride the candidate label (16.47).
|
|
133
|
+
sessionCandidateJustNow: '刚刚',
|
|
134
|
+
sessionCandidateMinutesAgo: (minutes) => `${String(minutes)} 分钟前`,
|
|
135
|
+
sessionCandidateHoursAgo: (hours) => `${String(hours)} 小时前`,
|
|
136
|
+
sessionCandidateDaysAgo: (days) => `${String(days)} 天前`,
|
|
137
|
+
sessionCandidateRunning: '运行中',
|
|
121
138
|
// ── approval / question cards ────────────────────────────────────────
|
|
122
139
|
approvalRequired: (label) => `Approval required — ${label}`,
|
|
123
140
|
};
|
|
@@ -191,6 +208,7 @@ const en = {
|
|
|
191
208
|
mainlineSessionRejected: '⚠️ DSH rejected the session creation (the workspace may be stale); try again later or re-run /project bind.',
|
|
192
209
|
mainlineSessionUnknown: '⚠️ Session creation outcome unknown; confirm on the DSH web console before retrying. It will not be resubmitted automatically.',
|
|
193
210
|
mainlinePromptRejected: '⚠️ DSH rejected the task submission.',
|
|
211
|
+
mainlineImageFailed: '⚠️ Could not fetch the attached image(s); the task was not submitted. Retry or describe it in text.',
|
|
194
212
|
mainlinePromptUnknown: '⚠️ Task submission outcome unknown; it will not be resent automatically to avoid double execution. Confirm and retry.',
|
|
195
213
|
continuationRejected: '⚠️ The message submission was rejected by DSH.',
|
|
196
214
|
continuationUnknown: '⚠️ Message submission outcome unknown; it will not be resent automatically. Confirm and retry.',
|
|
@@ -223,6 +241,20 @@ const en = {
|
|
|
223
241
|
modelNotInCatalog: "⚠️ That provider/model is not in this session's catalog.",
|
|
224
242
|
modelInvalidReasoning: '⚠️ That reasoning effort is not valid for this model.',
|
|
225
243
|
modelTypedParseFailed: '⚠️ The model must be `provider/model`, or left empty for the interactive cascade.',
|
|
244
|
+
// ── /session resume ──────────────────────────────────────────────────
|
|
245
|
+
sessionResumeNeedsBoundChannel: '⚠️ /session resume must run in a bound project channel.',
|
|
246
|
+
sessionResumeStarted: threadId => `✅ Session resumed into <#${threadId}> — full history lives in the web UI; continue in the thread.`,
|
|
247
|
+
sessionResumeAlreadyBound: threadId => `This session already lives in <#${threadId}>.`,
|
|
248
|
+
sessionResumeFailed: '⚠️ Resuming the session failed; try again later.',
|
|
249
|
+
sessionResumeAnchor: title => `📌 Resumed session: ${title}`,
|
|
250
|
+
sessionResumeControlChannel: 'general is the control channel and carries no sessions — use /session resume in a workspace channel.',
|
|
251
|
+
sessionResumeSubagent: '⚠️ That is a subagent session — it cannot be resumed as a top-level thread; view it in the web UI.',
|
|
252
|
+
sessionResumeArchived: '⚠️ That session is archived — a resumed one never runs; unarchive it in the web UI first.',
|
|
253
|
+
sessionCandidateJustNow: 'just now',
|
|
254
|
+
sessionCandidateMinutesAgo: minutes => `${String(minutes)}m ago`,
|
|
255
|
+
sessionCandidateHoursAgo: hours => `${String(hours)}h ago`,
|
|
256
|
+
sessionCandidateDaysAgo: days => `${String(days)}d ago`,
|
|
257
|
+
sessionCandidateRunning: 'running',
|
|
226
258
|
// ── approval / question cards ────────────────────────────────────────
|
|
227
259
|
approvalRequired: label => `Approval required — ${label}`,
|
|
228
260
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_DISCORD_SETTINGS, DiscordSettingsSchema, installDiscordSettings, normalizeDiscordSettings, } from './settings.js';
|
|
2
2
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
3
|
import { installCancellationRoot } from './lifecycle.js';
|
|
4
|
-
import { ALLOWED_MENTIONS_NONE, DISCORD_SUPPRESS_NOTIFICATIONS_FLAG, OUTBOUND_EPHEMERAL_FLAGS } from './policy/disclosure.js';
|
|
4
|
+
import { ALLOWED_MENTIONS_NONE, DISCORD_SUPPRESS_NOTIFICATIONS_FLAG, OUTBOUND_EPHEMERAL_FLAGS, safeTitle } from './policy/disclosure.js';
|
|
5
5
|
import { validateHostCapabilities } from './startup.js';
|
|
6
6
|
import { createAdapterStatusTracker, installAdapterStatusRpc, } from './features/adapter-status.js';
|
|
7
7
|
import { DISCORD_BOT_TOKEN_REF, describeDiscordCredential, resolveDiscordBotToken } from './credential.js';
|
|
@@ -22,14 +22,16 @@ import { createInteractionRouter } from './features/interaction-router.js';
|
|
|
22
22
|
import { channelBindingKey, parseChannelBindingKey, threadBindingKey, parseThreadBindingKey, discordDomainSpec, CHANNEL_BINDINGS_TABLE, THREAD_BINDINGS_TABLE, INTENTS_TABLE } from './state/domain.js';
|
|
23
23
|
import { planBindingReconciliation } from './features/reconcile-bindings.js';
|
|
24
24
|
import { guildKeysToForget, sweepExpired } from './state/retention.js';
|
|
25
|
-
import { listSessionIds, createClientRespondPort } from './dsh/api-proxy-face.js';
|
|
25
|
+
import { listSessionIds, listSessionSummaries, createClientRespondPort } from './dsh/api-proxy-face.js';
|
|
26
26
|
import { createBindingStore } from './state/bindings.js';
|
|
27
27
|
import { createIntentStore } from './state/intents.js';
|
|
28
28
|
import { createTurnTracker } from './features/turn-ownership.js';
|
|
29
29
|
import { createThreadCreationFlow } from './features/thread-creation.js';
|
|
30
30
|
import { createSessionCreationFlow } from './features/session-creation.js';
|
|
31
|
+
import { createResumeCandidatesPort } from './features/session-resume.js';
|
|
31
32
|
import { createPromptSubmissionFlow } from './features/prompt-submission.js';
|
|
32
33
|
import { createSessionMainline, requestIdFor } from './features/session-mainline.js';
|
|
34
|
+
import { collectImages, createSafeImageDownloadPort } from './features/image-collection.js';
|
|
33
35
|
import { startLiveRender } from './stream/live.js';
|
|
34
36
|
import { planWorkspaceChannel, workspaceChannelName } from './features/workspace-channel.js';
|
|
35
37
|
/** Stable Cordis plugin name for diagnostics. */
|
|
@@ -285,7 +287,37 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
285
287
|
createSession: request => createSessionViaProxy(apiProxy, request, { log: rpcLog }),
|
|
286
288
|
};
|
|
287
289
|
const dshPromptPort = {
|
|
288
|
-
submit: request => promptSession(apiProxy, {
|
|
290
|
+
submit: request => promptSession(apiProxy, {
|
|
291
|
+
sessionId: request.sessionId,
|
|
292
|
+
prompt: request.prompt,
|
|
293
|
+
...(request.images === undefined ? {} : { images: request.images }),
|
|
294
|
+
}, { log: rpcLog, rpcId: request.requestId }),
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* The bounded image collection boundary (16.50): downloads ride the
|
|
298
|
+
* safe-download port (allowlisted CDN hosts, capped sizes, timeout) and
|
|
299
|
+
* come back base64-encoded for the prompt parts.
|
|
300
|
+
*/
|
|
301
|
+
const IMAGE_DOWNLOAD_TIMEOUT_MS = 15_000;
|
|
302
|
+
const imageCollector = {
|
|
303
|
+
collect: async ({ attachments }) => {
|
|
304
|
+
const result = await collectImages(createSafeImageDownloadPort(), {
|
|
305
|
+
nowMs: Date.now(),
|
|
306
|
+
timeoutMs: IMAGE_DOWNLOAD_TIMEOUT_MS,
|
|
307
|
+
attachments,
|
|
308
|
+
});
|
|
309
|
+
if (result.outcome !== 'collected') {
|
|
310
|
+
const reason = result.outcome === 'too-large' ? `too-large:${result.reason}` : result.outcome;
|
|
311
|
+
return { outcome: 'failed', reason };
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
outcome: 'collected',
|
|
315
|
+
images: result.downloaded.map(image => ({
|
|
316
|
+
mediaType: image.mediaType,
|
|
317
|
+
base64: Buffer.from(image.body).toString('base64'),
|
|
318
|
+
})),
|
|
319
|
+
};
|
|
320
|
+
},
|
|
289
321
|
};
|
|
290
322
|
const composedMainline = createSessionMainline({
|
|
291
323
|
threads: createThreadCreationFlow({
|
|
@@ -304,6 +336,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
304
336
|
nowMs: () => Date.now(),
|
|
305
337
|
}),
|
|
306
338
|
turns: turnTracker,
|
|
339
|
+
images: imageCollector,
|
|
307
340
|
});
|
|
308
341
|
/** Mainline outcome → copy key; the string resolves live per language. */
|
|
309
342
|
const MAINLINE_FAILURE_KEYS = {
|
|
@@ -313,6 +346,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
313
346
|
'session-unknown': 'mainlineSessionUnknown',
|
|
314
347
|
'prompt-rejected': 'mainlinePromptRejected',
|
|
315
348
|
'prompt-unknown': 'mainlinePromptUnknown',
|
|
349
|
+
'image-failed': 'mainlineImageFailed',
|
|
316
350
|
};
|
|
317
351
|
const mainline = {
|
|
318
352
|
admitMention: async (request) => {
|
|
@@ -333,9 +367,13 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
333
367
|
},
|
|
334
368
|
continueInThread: async (request) => {
|
|
335
369
|
const result = await composedMainline.continueInThread(request);
|
|
336
|
-
if (result.outcome === 'rejected' || result.outcome === 'unknown') {
|
|
370
|
+
if (result.outcome === 'rejected' || result.outcome === 'unknown' || result.outcome === 'image-failed') {
|
|
337
371
|
rpcLog('discord_continuation_not_queued', { messageId: request.messageId, outcome: result.outcome });
|
|
338
|
-
const notice = result.outcome === 'rejected'
|
|
372
|
+
const notice = result.outcome === 'rejected'
|
|
373
|
+
? copy.continuationRejected
|
|
374
|
+
: result.outcome === 'image-failed'
|
|
375
|
+
? copy.mainlineImageFailed
|
|
376
|
+
: copy.continuationUnknown;
|
|
339
377
|
void withRest(rest => rest.request('POST', `/channels/${request.threadId}/messages`, { content: notice, flags: DISCORD_SUPPRESS_NOTIFICATIONS_FLAG, allowed_mentions: ALLOWED_MENTIONS_NONE }))
|
|
340
378
|
.catch((cause) => { rpcLog('discord_mainline_notice_failed', String(cause)); });
|
|
341
379
|
}
|
|
@@ -702,6 +740,91 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
|
|
|
702
740
|
},
|
|
703
741
|
model: createModelPort(apiProxy, { log: rpcLog }),
|
|
704
742
|
modelSelectOperatorOnly: () => current.modelSelectOperatorOnly,
|
|
743
|
+
resumeCandidates: createResumeCandidatesPort({
|
|
744
|
+
listSessions: () => listSessionSummaries(apiProxy, { log: rpcLog }),
|
|
745
|
+
boundSessionIds: () => {
|
|
746
|
+
const bound = new Set();
|
|
747
|
+
for (const [, record] of threadTable.entries())
|
|
748
|
+
bound.add(record.sessionId);
|
|
749
|
+
return bound;
|
|
750
|
+
},
|
|
751
|
+
copy,
|
|
752
|
+
}),
|
|
753
|
+
resumeSession: async ({ sessionId, workspaceId, guildId, parentChannelId, actorId }) => {
|
|
754
|
+
const rest = await sharedRest();
|
|
755
|
+
if (rest === undefined)
|
|
756
|
+
return { outcome: 'failed' };
|
|
757
|
+
// threadBindings is the Discord-ownership record: a session with a
|
|
758
|
+
// binding already lives in a thread (16.44; the owners-store wiring
|
|
759
|
+
// stays deferred per 16.28).
|
|
760
|
+
const existingThread = threadForSession(sessionId);
|
|
761
|
+
if (existingThread !== undefined)
|
|
762
|
+
return { outcome: 'already-bound', threadId: existingThread };
|
|
763
|
+
// The control channel never carries sessions: refuse explicitly so
|
|
764
|
+
// the user is pointed at the workspace channel (16.44 rev).
|
|
765
|
+
const listedChannels = await rest.request('GET', `/guilds/${guildId}/channels`);
|
|
766
|
+
if (listedChannels.outcome === 'completed') {
|
|
767
|
+
const category = listedChannels.body.find(channel => channel.type === 4 && channel.name.toLowerCase() === CATEGORY_NAME.toLowerCase());
|
|
768
|
+
const control = category !== undefined
|
|
769
|
+
? listedChannels.body.find(channel => channel.type === 0 && channel.parent_id === category.id && channel.name.toLowerCase() === 'general')
|
|
770
|
+
: undefined;
|
|
771
|
+
if (control !== undefined && control.id === parentChannelId) {
|
|
772
|
+
return { outcome: 'refused-control-channel' };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const summaries = await listSessionSummaries(apiProxy, { log: rpcLog });
|
|
776
|
+
if (summaries.outcome !== 'completed')
|
|
777
|
+
return { outcome: 'failed' };
|
|
778
|
+
const summary = summaries.sessions.find(candidate => candidate.sessionId === sessionId);
|
|
779
|
+
if (summary === undefined || summary.blank)
|
|
780
|
+
return { outcome: 'failed' };
|
|
781
|
+
// Spec scenario "Subagent Session selected": a subagent spawn is
|
|
782
|
+
// never a top-level writable thread, even if handed in directly
|
|
783
|
+
// (the candidates already hide it — 16.48).
|
|
784
|
+
if (summary.origin === 'subagent')
|
|
785
|
+
return { outcome: 'refused-subagent' };
|
|
786
|
+
// Same defense for archived sessions (16.49): adoption would
|
|
787
|
+
// succeed and the thread would never see a turn.
|
|
788
|
+
const catalog = await catalogPort.listWorkspaces();
|
|
789
|
+
if (catalog.outcome === 'completed'
|
|
790
|
+
&& catalog.archivedSessionIds.includes(sessionId)) {
|
|
791
|
+
return { outcome: 'refused-archived' };
|
|
792
|
+
}
|
|
793
|
+
const title = summary.title ?? `Resume ${sessionId.slice(0, 8)}`;
|
|
794
|
+
// Anchor post: a durable marker the resume thread hangs off.
|
|
795
|
+
const anchor = await rest.request('POST', `/channels/${parentChannelId}/messages`, {
|
|
796
|
+
content: copy.sessionResumeAnchor(safeTitle(title)),
|
|
797
|
+
flags: DISCORD_SUPPRESS_NOTIFICATIONS_FLAG,
|
|
798
|
+
allowed_mentions: ALLOWED_MENTIONS_NONE,
|
|
799
|
+
});
|
|
800
|
+
if (anchor.outcome === 'rejected') {
|
|
801
|
+
rpcLog('discord_session_resume_anchor_failed', { sessionId, status: `HTTP ${String(anchor.status)}` });
|
|
802
|
+
return { outcome: 'failed' };
|
|
803
|
+
}
|
|
804
|
+
if (anchor.outcome === 'unknown' || typeof anchor.body.id !== 'string') {
|
|
805
|
+
rpcLog('discord_session_resume_anchor_unknown', { sessionId });
|
|
806
|
+
return { outcome: 'failed' };
|
|
807
|
+
}
|
|
808
|
+
const thread = await rest.request('POST', `/channels/${parentChannelId}/messages/${anchor.body.id}/threads`, {
|
|
809
|
+
name: safeTitle(title),
|
|
810
|
+
});
|
|
811
|
+
if (thread.outcome === 'rejected') {
|
|
812
|
+
rpcLog('discord_session_resume_thread_failed', { sessionId, status: `HTTP ${String(thread.status)}` });
|
|
813
|
+
return { outcome: 'failed' };
|
|
814
|
+
}
|
|
815
|
+
if (thread.outcome === 'unknown' || typeof thread.body.id !== 'string') {
|
|
816
|
+
rpcLog('discord_session_resume_thread_unknown', { sessionId });
|
|
817
|
+
return { outcome: 'failed' };
|
|
818
|
+
}
|
|
819
|
+
await threadBindingStore.bind(threadBindingKey({ applicationId: applicationIdRef.current, guildId, threadId: thread.body.id }), {
|
|
820
|
+
sessionId,
|
|
821
|
+
workspaceId,
|
|
822
|
+
createdBy: actorId,
|
|
823
|
+
createdAtMs: Date.now(),
|
|
824
|
+
});
|
|
825
|
+
rpcLog('discord_session_resumed', { sessionId, threadId: thread.body.id, title });
|
|
826
|
+
return { outcome: 'started', threadId: thread.body.id };
|
|
827
|
+
},
|
|
705
828
|
log: rpcLog,
|
|
706
829
|
warn: (event, detail) => {
|
|
707
830
|
emitLog(ctx, 'warn', { event, detail: typeof detail === 'string' ? detail : JSON.stringify(detail ?? null) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addozhang/dsh-discord",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Discord-first adapter for DeepSeek Harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -91,5 +91,16 @@
|
|
|
91
91
|
"bugs": {
|
|
92
92
|
"url": "https://github.com/addozhang/dsh-discord/issues"
|
|
93
93
|
},
|
|
94
|
-
"homepage": "https://github.com/addozhang/dsh-discord#readme"
|
|
94
|
+
"homepage": "https://github.com/addozhang/dsh-discord#readme",
|
|
95
|
+
"keywords": [
|
|
96
|
+
"discord",
|
|
97
|
+
"deepseek",
|
|
98
|
+
"deepseek-harness",
|
|
99
|
+
"dsh",
|
|
100
|
+
"dsh-plugin",
|
|
101
|
+
"bot",
|
|
102
|
+
"ai-agent",
|
|
103
|
+
"claude-code",
|
|
104
|
+
"coding-agent"
|
|
105
|
+
]
|
|
95
106
|
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image submission through `session.prompt` (design.md §12, task 12.3).
|
|
3
|
-
* Mixed text/images encode into ordered prompt parts; the model's modality
|
|
4
|
-
* is checked BEFORE any DSH call; and the request id claims an intent so a
|
|
5
|
-
* duplicate Discord delivery never resubmits. Host outcomes map to values
|
|
6
|
-
* with the intent state kept honest (failed on rejection, unknown preserved
|
|
7
|
-
* for reconciliation).
|
|
8
|
-
*/
|
|
9
|
-
import { type IntentStore } from '../state/intents.js';
|
|
10
|
-
export type PromptPart = {
|
|
11
|
-
type: 'text';
|
|
12
|
-
text: string;
|
|
13
|
-
} | {
|
|
14
|
-
type: 'image';
|
|
15
|
-
mediaType: string;
|
|
16
|
-
data: string;
|
|
17
|
-
};
|
|
18
|
-
export interface DshImagePromptPort {
|
|
19
|
-
submit(request: {
|
|
20
|
-
requestId: string;
|
|
21
|
-
sessionId: string;
|
|
22
|
-
parts: readonly PromptPart[];
|
|
23
|
-
}): Promise<{
|
|
24
|
-
outcome: 'accepted';
|
|
25
|
-
} | {
|
|
26
|
-
outcome: 'rejected';
|
|
27
|
-
} | {
|
|
28
|
-
outcome: 'unknown';
|
|
29
|
-
}>;
|
|
30
|
-
}
|
|
31
|
-
export interface ImageSubmissionDeps {
|
|
32
|
-
prompts: DshImagePromptPort;
|
|
33
|
-
intents: IntentStore;
|
|
34
|
-
/** Whether the session's current model accepts image parts. */
|
|
35
|
-
modality: () => boolean;
|
|
36
|
-
nowMs: () => number;
|
|
37
|
-
}
|
|
38
|
-
export type ImageSubmissionResult = {
|
|
39
|
-
outcome: 'accepted';
|
|
40
|
-
} | {
|
|
41
|
-
outcome: 'rejected';
|
|
42
|
-
} | {
|
|
43
|
-
outcome: 'unknown';
|
|
44
|
-
} | {
|
|
45
|
-
outcome: 'already-submitted';
|
|
46
|
-
} | {
|
|
47
|
-
outcome: 'conflict';
|
|
48
|
-
} | {
|
|
49
|
-
outcome: 'refused';
|
|
50
|
-
reason: 'unsupported-modality';
|
|
51
|
-
};
|
|
52
|
-
/** Encode text plus images into ordered prompt parts. */
|
|
53
|
-
export declare function encodePromptParts(text: string, images: ReadonlyArray<{
|
|
54
|
-
mediaType: string;
|
|
55
|
-
base64: string;
|
|
56
|
-
}>): PromptPart[];
|
|
57
|
-
export declare function createImageSubmissionFlow(deps: ImageSubmissionDeps): {
|
|
58
|
-
submit(request: {
|
|
59
|
-
requestId: string;
|
|
60
|
-
sessionId: string;
|
|
61
|
-
text: string;
|
|
62
|
-
images: ReadonlyArray<{
|
|
63
|
-
mediaType: string;
|
|
64
|
-
base64: string;
|
|
65
|
-
}>;
|
|
66
|
-
}): Promise<ImageSubmissionResult>;
|
|
67
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image submission through `session.prompt` (design.md §12, task 12.3).
|
|
3
|
-
* Mixed text/images encode into ordered prompt parts; the model's modality
|
|
4
|
-
* is checked BEFORE any DSH call; and the request id claims an intent so a
|
|
5
|
-
* duplicate Discord delivery never resubmits. Host outcomes map to values
|
|
6
|
-
* with the intent state kept honest (failed on rejection, unknown preserved
|
|
7
|
-
* for reconciliation).
|
|
8
|
-
*/
|
|
9
|
-
import { hashPayload } from '../state/intents.js';
|
|
10
|
-
/** Encode text plus images into ordered prompt parts. */
|
|
11
|
-
export function encodePromptParts(text, images) {
|
|
12
|
-
const parts = [];
|
|
13
|
-
const trimmed = text.trim();
|
|
14
|
-
if (trimmed !== '')
|
|
15
|
-
parts.push({ type: 'text', text: trimmed });
|
|
16
|
-
for (const image of images) {
|
|
17
|
-
parts.push({ type: 'image', mediaType: image.mediaType, data: image.base64 });
|
|
18
|
-
}
|
|
19
|
-
return parts;
|
|
20
|
-
}
|
|
21
|
-
export function createImageSubmissionFlow(deps) {
|
|
22
|
-
return {
|
|
23
|
-
async submit(request) {
|
|
24
|
-
const parts = encodePromptParts(request.text, request.images);
|
|
25
|
-
// Modality gate: refuse BEFORE the intent claim so an explicit user
|
|
26
|
-
// retry with a text-only message is a clean new attempt.
|
|
27
|
-
if (request.images.length > 0 && !deps.modality()) {
|
|
28
|
-
return { outcome: 'refused', reason: 'unsupported-modality' };
|
|
29
|
-
}
|
|
30
|
-
const contentHash = await hashPayload({ sessionId: request.sessionId, parts });
|
|
31
|
-
const claim = await deps.intents.claim({
|
|
32
|
-
messageId: request.requestId,
|
|
33
|
-
contentHash,
|
|
34
|
-
claimedAtMs: deps.nowMs(),
|
|
35
|
-
});
|
|
36
|
-
if (claim.outcome === 'conflict')
|
|
37
|
-
return { outcome: 'conflict' };
|
|
38
|
-
if (claim.outcome === 'duplicate')
|
|
39
|
-
return { outcome: 'already-submitted' };
|
|
40
|
-
const submitted = await deps.prompts.submit({
|
|
41
|
-
requestId: request.requestId,
|
|
42
|
-
sessionId: request.sessionId,
|
|
43
|
-
parts,
|
|
44
|
-
});
|
|
45
|
-
if (submitted.outcome === 'accepted') {
|
|
46
|
-
await deps.intents.resolve(request.requestId, 'succeeded', deps.nowMs());
|
|
47
|
-
return { outcome: 'accepted' };
|
|
48
|
-
}
|
|
49
|
-
if (submitted.outcome === 'rejected') {
|
|
50
|
-
await deps.intents.resolve(request.requestId, 'failed', deps.nowMs());
|
|
51
|
-
return { outcome: 'rejected' };
|
|
52
|
-
}
|
|
53
|
-
await deps.intents.resolve(request.requestId, 'unknown', deps.nowMs());
|
|
54
|
-
return { outcome: 'unknown' };
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|