@soffits/dsh-telegram-bot 0.4.1 → 0.4.2
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/client.js +334 -0
- package/package.json +3 -1
- package/src/app.js +40 -7
- package/src/client.js +334 -0
- package/src/constants.js +6 -0
package/client.js
CHANGED
|
@@ -70,6 +70,19 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
loadingSettings: 'Loading settings…',
|
|
71
71
|
unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
|
|
72
72
|
readOnly: 'This deployment stores settings read-only.',
|
|
73
|
+
postTitle: 'Sent to Telegram',
|
|
74
|
+
postTopic: 'topic',
|
|
75
|
+
postJob: 'job',
|
|
76
|
+
postSession: 'session',
|
|
77
|
+
postOtherSession: 'another session',
|
|
78
|
+
postSticker: 'Sticker',
|
|
79
|
+
postFile: 'file',
|
|
80
|
+
postCopy: 'Copy',
|
|
81
|
+
postCopied: 'Copied',
|
|
82
|
+
postCode: 'Code',
|
|
83
|
+
postWrap: 'Wrap',
|
|
84
|
+
postUnwrap: 'Unwrap',
|
|
85
|
+
postFootnotes: 'Footnotes',
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
const ru = {
|
|
@@ -127,6 +140,19 @@ window.__ModuleLoader__.load({
|
|
|
127
140
|
loadingSettings: 'Загрузка настроек…',
|
|
128
141
|
unavailable: 'Плагин не загружен, поэтому настройки сейчас недоступны.',
|
|
129
142
|
readOnly: 'Этот deployment хранит настройки только для чтения.',
|
|
143
|
+
postTitle: 'Отправлено в Telegram',
|
|
144
|
+
postTopic: 'тема',
|
|
145
|
+
postJob: 'задача',
|
|
146
|
+
postSession: 'сессия',
|
|
147
|
+
postOtherSession: 'другая сессия',
|
|
148
|
+
postSticker: 'Стикер',
|
|
149
|
+
postFile: 'файл',
|
|
150
|
+
postCopy: 'Копировать',
|
|
151
|
+
postCopied: 'Скопировано',
|
|
152
|
+
postCode: 'Код',
|
|
153
|
+
postWrap: 'Переносить',
|
|
154
|
+
postUnwrap: 'Не переносить',
|
|
155
|
+
postFootnotes: 'Сноски',
|
|
130
156
|
}
|
|
131
157
|
|
|
132
158
|
const CSS = `
|
|
@@ -730,6 +756,293 @@ window.__ModuleLoader__.load({
|
|
|
730
756
|
)
|
|
731
757
|
}
|
|
732
758
|
|
|
759
|
+
// ---- Telegram posts in the Chat transcript ----
|
|
760
|
+
//
|
|
761
|
+
// A send into a topic leaves a user-role copy in the topic's session, so
|
|
762
|
+
// the model knows what was said there. DSH's Chat would draw an ordinary
|
|
763
|
+
// user message as the user's own bubble; these Definitions claim the copy
|
|
764
|
+
// and draw it as what it is: something sent to Telegram, on the
|
|
765
|
+
// assistant's side, pictures included. The recorded message is unchanged.
|
|
766
|
+
|
|
767
|
+
/** Chat node kind, conversation Definition kind and Host source kind of a post. */
|
|
768
|
+
const POST_KIND = 'telegram-post'
|
|
769
|
+
/** Request-id prefix of posts recorded before they had their own source kind. */
|
|
770
|
+
const LEGACY_POST_RPC = 'telegram-post:'
|
|
771
|
+
/** First text line of every post: what the model reads as its sender. */
|
|
772
|
+
const HEADER_LINE = /^\[(Sent to the Telegram [^\n]*)\]$/u
|
|
773
|
+
/** Line naming a sent sticker; its still picture follows as an image. */
|
|
774
|
+
const STICKER_LINE = /^\[Sticker ([^\n]*)\]$/u
|
|
775
|
+
/** The header's own words, for posts that carry no structured source. */
|
|
776
|
+
const HEADER_PARTS = /^Sent to the Telegram topic (?:"(.*)"|(\d+))(?: by (.*))?$/u
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Whether a durable message source marks a topic send's copy.
|
|
780
|
+
* @param source - `user/message` source.
|
|
781
|
+
* @returns true for posts, old and new.
|
|
782
|
+
*/
|
|
783
|
+
function isPostSource(source) {
|
|
784
|
+
if (typeof source !== 'object' || source === null) return false
|
|
785
|
+
if (source.kind === POST_KIND) return true
|
|
786
|
+
return source.kind === 'user' && typeof source.rpcId === 'string' && source.rpcId.startsWith(LEGACY_POST_RPC)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Claim a post's `user/message` on the Chat target. Only append-origin
|
|
791
|
+
* copies are claimed: a replacement copy (compaction) belongs to the
|
|
792
|
+
* system-prompt Definition and is never drawn.
|
|
793
|
+
* @param event - Client history event.
|
|
794
|
+
* @returns the match, or null when the event is not a post.
|
|
795
|
+
*/
|
|
796
|
+
function matchPost(event) {
|
|
797
|
+
if (event?.type !== 'user/message' || event.surfaceOp !== 'append') return null
|
|
798
|
+
const data = event.data
|
|
799
|
+
if (data === null || typeof data !== 'object' || !isPostSource(data.source)) return null
|
|
800
|
+
return { id: String(data.id), role: 'start', exclusive: true }
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Who sent a post and where, from its source or, for old records, from
|
|
805
|
+
* the header line the model reads.
|
|
806
|
+
* @param source - the post's source.
|
|
807
|
+
* @param header - header line without brackets, when present.
|
|
808
|
+
* @returns `{ topic, threadId, job, label, fromSession, self }`; unknown fields are null.
|
|
809
|
+
*/
|
|
810
|
+
function postMeta(source, header) {
|
|
811
|
+
if (source?.kind === POST_KIND) {
|
|
812
|
+
return {
|
|
813
|
+
topic: typeof source.topic === 'string' ? source.topic : null,
|
|
814
|
+
threadId: typeof source.threadId === 'number' ? source.threadId : null,
|
|
815
|
+
job: typeof source.job === 'string' ? source.job : null,
|
|
816
|
+
label: typeof source.label === 'string' ? source.label : null,
|
|
817
|
+
fromSession: typeof source.fromSession === 'string' ? source.fromSession : null,
|
|
818
|
+
self: source.self === true,
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const meta = { topic: null, threadId: null, job: null, label: null, fromSession: null, self: false }
|
|
822
|
+
const parts = header === null ? null : HEADER_PARTS.exec(header)
|
|
823
|
+
if (parts === null) return meta
|
|
824
|
+
if (parts[1] !== undefined) meta.topic = parts[1]
|
|
825
|
+
else meta.threadId = Number(parts[2])
|
|
826
|
+
const by = parts[3]
|
|
827
|
+
if (by === undefined) return meta
|
|
828
|
+
if (by === 'this session') {
|
|
829
|
+
meta.self = true
|
|
830
|
+
return meta
|
|
831
|
+
}
|
|
832
|
+
const job = /^job "(.*?)"(?: from session (\S+))?$/u.exec(by)
|
|
833
|
+
const session = /^session (\S+)$/u.exec(by)
|
|
834
|
+
if (job !== null) {
|
|
835
|
+
meta.job = job[1]
|
|
836
|
+
meta.fromSession = job[2] ?? null
|
|
837
|
+
} else if (session !== null) {
|
|
838
|
+
meta.fromSession = session[1]
|
|
839
|
+
} else {
|
|
840
|
+
meta.label = by
|
|
841
|
+
}
|
|
842
|
+
return meta
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Split a post's recorded content into what the transcript shows.
|
|
847
|
+
* @param content - the message's content blocks.
|
|
848
|
+
* @param source - the message's source.
|
|
849
|
+
* @returns `{ meta, text, stickers, images, files }`: the body without
|
|
850
|
+
* its header and sticker lines, sticker descriptions, image sources
|
|
851
|
+
* for the gallery and file attachments.
|
|
852
|
+
*/
|
|
853
|
+
function readPost(content, source) {
|
|
854
|
+
const blocks = Array.isArray(content) ? content : []
|
|
855
|
+
const lines = blocks
|
|
856
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
857
|
+
.map((block) => block.text)
|
|
858
|
+
.join('\n')
|
|
859
|
+
.split('\n')
|
|
860
|
+
const first = HEADER_LINE.exec(lines[0] ?? '')
|
|
861
|
+
if (first !== null) lines.shift()
|
|
862
|
+
const stickers = []
|
|
863
|
+
const body = []
|
|
864
|
+
for (const line of lines) {
|
|
865
|
+
const sticker = STICKER_LINE.exec(line)
|
|
866
|
+
if (sticker !== null) stickers.push(sticker[1])
|
|
867
|
+
else body.push(line)
|
|
868
|
+
}
|
|
869
|
+
const images = []
|
|
870
|
+
const files = []
|
|
871
|
+
for (const block of blocks) {
|
|
872
|
+
if (block?.attachment === undefined || block.attachment === null) continue
|
|
873
|
+
if (block.type === 'image') {
|
|
874
|
+
const name = block.attachment.name
|
|
875
|
+
images.push(typeof name === 'string' && name.length > 0 ? { attachment: block.attachment, label: name } : { attachment: block.attachment })
|
|
876
|
+
} else if (block.type === 'file') {
|
|
877
|
+
files.push(block.attachment)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
meta: postMeta(source, first === null ? null : first[1]),
|
|
882
|
+
text: body.join('\n').trim(),
|
|
883
|
+
stickers,
|
|
884
|
+
images,
|
|
885
|
+
files,
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Chat Definition for topic sends. The row stays in its Turn outside the
|
|
891
|
+
* folded work rows (`turn`): it is something sent, not a step of work,
|
|
892
|
+
* and it neither opens nor joins a Turn's input.
|
|
893
|
+
*/
|
|
894
|
+
const postDefinition = {
|
|
895
|
+
kind: POST_KIND,
|
|
896
|
+
target: 'chat',
|
|
897
|
+
match: { 'user/message': matchPost },
|
|
898
|
+
start: (_context, match) => {
|
|
899
|
+
const event = match.event
|
|
900
|
+
return {
|
|
901
|
+
seq: event.seq,
|
|
902
|
+
time: event.time,
|
|
903
|
+
...readPost(event.data.content, event.data.source),
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
update: (context) => context.state,
|
|
907
|
+
buildViewNode: (context) => {
|
|
908
|
+
if (context.state === undefined) return null
|
|
909
|
+
const location = context.start?.location ?? context.matches[0]?.location ?? { kind: 'unresolved' }
|
|
910
|
+
return {
|
|
911
|
+
key: context.key,
|
|
912
|
+
kind: POST_KIND,
|
|
913
|
+
id: context.id,
|
|
914
|
+
target: 'chat',
|
|
915
|
+
anchorSeq: context.state.seq,
|
|
916
|
+
location,
|
|
917
|
+
visibility: 'visible',
|
|
918
|
+
groupTree: location.kind === 'turn' || location.kind === 'step' ? 'turn' : undefined,
|
|
919
|
+
data: context.state,
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Where and from whom, as short header pieces.
|
|
926
|
+
* @param meta - see postMeta.
|
|
927
|
+
* @param t - locale reader.
|
|
928
|
+
* @returns `{ where, who, whoTitle }`; `who` is null for the session's
|
|
929
|
+
* own sends, which already sit on its side of the transcript.
|
|
930
|
+
*/
|
|
931
|
+
function postLabels(meta, t) {
|
|
932
|
+
const where = meta.topic !== null ? meta.topic : meta.threadId !== null ? `${t('postTopic')} ${meta.threadId}` : null
|
|
933
|
+
let who = null
|
|
934
|
+
let whoTitle
|
|
935
|
+
if (meta.job !== null) {
|
|
936
|
+
who = `${t('postJob')} ${meta.job}`
|
|
937
|
+
if (meta.fromSession !== null) whoTitle = `${t('postSession')} ${meta.fromSession}`
|
|
938
|
+
} else if (!meta.self && meta.fromSession !== null) {
|
|
939
|
+
who = t('postOtherSession')
|
|
940
|
+
whoTitle = `${t('postSession')} ${meta.fromSession}`
|
|
941
|
+
} else if (!meta.self && meta.label !== null) {
|
|
942
|
+
who = meta.label
|
|
943
|
+
}
|
|
944
|
+
return { where, who, whoTitle }
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Clock and full date of the post, in the browser's locale.
|
|
949
|
+
* @param time - event time (epoch milliseconds or ISO text).
|
|
950
|
+
* @returns `{ clock, iso, full }`, or null for a missing time.
|
|
951
|
+
*/
|
|
952
|
+
function postTime(time) {
|
|
953
|
+
if (time === undefined || time === null) return null
|
|
954
|
+
const date = new Date(time)
|
|
955
|
+
if (Number.isNaN(date.getTime())) return null
|
|
956
|
+
return {
|
|
957
|
+
clock: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
958
|
+
iso: date.toISOString(),
|
|
959
|
+
full: date.toLocaleString(),
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Transcript styles; theme tokens keep it in step with light and dark. */
|
|
964
|
+
const POST_CSS = `
|
|
965
|
+
.dtb-post{display:flex;flex-direction:column;gap:6px;min-width:0;padding:2px 0 2px 12px;border-left:2px solid var(--dsw-alias-border-l2,rgba(127,127,127,.35))}
|
|
966
|
+
.dtb-postHead{display:flex;align-items:center;gap:6px;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px))}
|
|
967
|
+
.dtb-postIcon{display:inline-flex;flex:none}
|
|
968
|
+
.dtb-postTitle{flex:none}
|
|
969
|
+
.dtb-postSep{flex:none;width:2px;height:2px;border-radius:1px;background:var(--dsw-alias-label-caption,currentColor)}
|
|
970
|
+
.dtb-postMeta{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
971
|
+
.dtb-postTime{flex:none;margin-left:auto;font:var(--dsw-font-xxs-12,12px/16px inherit)}
|
|
972
|
+
.dtb-postBody{min-width:0;color:var(--dsw-alias-label-primary)}
|
|
973
|
+
.dtb-postPlain{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
974
|
+
.dtb-postCaption{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}
|
|
975
|
+
.dtb-postFiles{display:flex;flex-wrap:wrap;gap:6px}
|
|
976
|
+
.dtb-postFile{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:var(--dsw-radius-md,8px);padding:4px 10px;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
977
|
+
`
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Markdown control labels from this plugin's dictionary.
|
|
981
|
+
* @param t - locale reader.
|
|
982
|
+
*/
|
|
983
|
+
function postMarkdownLabels(t) {
|
|
984
|
+
return {
|
|
985
|
+
code: {
|
|
986
|
+
copyLabel: t('postCopy'),
|
|
987
|
+
copiedLabel: t('postCopied'),
|
|
988
|
+
toolbarLabels: { codeLabel: t('postCode'), wrapLabel: t('postWrap'), unwrapLabel: t('postUnwrap') },
|
|
989
|
+
},
|
|
990
|
+
footnotes: t('postFootnotes'),
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Keyed Chat renderer of one post.
|
|
996
|
+
* @param props - `node`, `loadImage`, `renderFactorySlot` and `t` from the Chat node slot.
|
|
997
|
+
*/
|
|
998
|
+
function TelegramPostView(props) {
|
|
999
|
+
const { node, loadImage, renderFactorySlot, t } = props
|
|
1000
|
+
const data = node.data
|
|
1001
|
+
const labels = React.useMemo(() => postMarkdownLabels(t), [t])
|
|
1002
|
+
const { where, who, whoTitle } = postLabels(data.meta, t)
|
|
1003
|
+
const time = postTime(data.time)
|
|
1004
|
+
const Icon = primitives.IconPaperPlaneOutlineRegular
|
|
1005
|
+
const Markdown = primitives.MarkdownText
|
|
1006
|
+
const head = [
|
|
1007
|
+
h('span', { key: 'icon', className: 'dtb-postIcon', 'aria-hidden': true }, Icon === undefined ? null : h(Icon, { size: 14 })),
|
|
1008
|
+
h('span', { key: 'title', className: 'dtb-postTitle' }, t('postTitle')),
|
|
1009
|
+
]
|
|
1010
|
+
if (where !== null) {
|
|
1011
|
+
head.push(h('span', { key: 'sep-where', className: 'dtb-postSep', 'aria-hidden': true }))
|
|
1012
|
+
head.push(h('span', { key: 'where', className: 'dtb-postMeta' }, where))
|
|
1013
|
+
}
|
|
1014
|
+
if (who !== null) {
|
|
1015
|
+
head.push(h('span', { key: 'sep-who', className: 'dtb-postSep', 'aria-hidden': true }))
|
|
1016
|
+
head.push(h('span', { key: 'who', className: 'dtb-postMeta', title: whoTitle }, who))
|
|
1017
|
+
}
|
|
1018
|
+
if (time !== null) head.push(h('time', { key: 'time', className: 'dtb-postTime', dateTime: time.iso, title: time.full }, time.clock))
|
|
1019
|
+
return h(
|
|
1020
|
+
'div',
|
|
1021
|
+
{ className: 'dtb-post', 'data-telegram-post': true },
|
|
1022
|
+
h('div', { className: 'dtb-postHead' }, ...head),
|
|
1023
|
+
data.text.length === 0
|
|
1024
|
+
? null
|
|
1025
|
+
: h(
|
|
1026
|
+
'div',
|
|
1027
|
+
{ className: 'dtb-postBody' },
|
|
1028
|
+
Markdown === undefined ? h('div', { className: 'dtb-postPlain' }, data.text) : h(Markdown, { text: data.text, labels }),
|
|
1029
|
+
),
|
|
1030
|
+
data.images.length === 0 || typeof renderFactorySlot !== 'function'
|
|
1031
|
+
? null
|
|
1032
|
+
: renderFactorySlot('conversation.images.content', { images: data.images, loadImage, align: 'start' }),
|
|
1033
|
+
...data.stickers.map((sticker, index) => h('div', { key: `sticker-${index}`, className: 'dtb-postCaption' }, `${t('postSticker')} ${sticker}`)),
|
|
1034
|
+
data.files.length === 0
|
|
1035
|
+
? null
|
|
1036
|
+
: h(
|
|
1037
|
+
'div',
|
|
1038
|
+
{ className: 'dtb-postFiles' },
|
|
1039
|
+
...data.files.map((file, index) =>
|
|
1040
|
+
h('span', { key: `file-${index}`, className: 'dtb-postFile', title: file.name }, `📎 ${file.name ?? t('postFile')}`),
|
|
1041
|
+
),
|
|
1042
|
+
),
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
733
1046
|
/** Required client services. */
|
|
734
1047
|
const inject = ['slots', 'locale', 'remote', 'remote.credentials', 'configForms']
|
|
735
1048
|
|
|
@@ -799,11 +1112,32 @@ window.__ModuleLoader__.load({
|
|
|
799
1112
|
),
|
|
800
1113
|
'dsh-telegram-bot: bundle settings',
|
|
801
1114
|
)
|
|
1115
|
+
|
|
1116
|
+
if (typeof document !== 'undefined') {
|
|
1117
|
+
ctx.effect(() => {
|
|
1118
|
+
const style = document.createElement('style')
|
|
1119
|
+
style.dataset.plugin = '@soffits/dsh-telegram-bot'
|
|
1120
|
+
style.textContent = POST_CSS
|
|
1121
|
+
document.head.appendChild(style)
|
|
1122
|
+
return () => style.remove()
|
|
1123
|
+
}, 'dsh-telegram-bot: post styles')
|
|
1124
|
+
}
|
|
1125
|
+
// Chat may be absent (a client without the transcript); settings still load.
|
|
1126
|
+
ctx.inject(['uiConversation'], (scope) => {
|
|
1127
|
+
scope.uiConversation.events.register(postDefinition)
|
|
1128
|
+
})
|
|
1129
|
+
ctx.slots.inject('conversation.chat.node', () =>
|
|
1130
|
+
ctx.slots.register({ name: 'conversation.chat.node', key: POST_KIND, locale: NS }, TelegramPostView),
|
|
1131
|
+
)
|
|
802
1132
|
}
|
|
803
1133
|
|
|
804
1134
|
exports.NS = NS
|
|
805
1135
|
exports.apply = apply
|
|
806
1136
|
exports.inject = inject
|
|
1137
|
+
exports.postDefinition = postDefinition
|
|
1138
|
+
exports.readPost = readPost
|
|
1139
|
+
exports.postLabels = postLabels
|
|
1140
|
+
exports.TelegramPostView = TelegramPostView
|
|
807
1141
|
return module.exports
|
|
808
1142
|
},
|
|
809
1143
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soffits/dsh-telegram-bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Telegram sessions, topics, model controls and approvals for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"immediately": true,
|
|
37
37
|
"inject": [
|
|
38
38
|
"@deepseek-ai/dsh-client-locale",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
39
41
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
40
42
|
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
41
43
|
]
|
package/src/app.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
FORWARD_BATCH_MS,
|
|
11
11
|
MAX_TELEGRAM_TEXT,
|
|
12
12
|
POLL_TIMEOUT_SECONDS,
|
|
13
|
+
POST_SOURCE_KIND,
|
|
13
14
|
RESTART_DEBOUNCE_MS,
|
|
14
15
|
RETRY_DELAY_MS,
|
|
15
16
|
SILENT_REPLY,
|
|
@@ -3374,9 +3375,9 @@ export function apply(ctx, config) {
|
|
|
3374
3375
|
* Put a message into a session without starting a turn: appended now to an
|
|
3375
3376
|
* idle session whose surface starts with its system prompt, otherwise queued
|
|
3376
3377
|
* for the next step (see recordNote for why). Either way the Web UI shows
|
|
3377
|
-
* it
|
|
3378
|
+
* it and the model reads it at its next step.
|
|
3378
3379
|
* @param agent - the session's live agent.
|
|
3379
|
-
* @param message - a user message (`{ id, role, content, source }`).
|
|
3380
|
+
* @param message - a user-role message (`{ id, role, content, source }`).
|
|
3380
3381
|
*/
|
|
3381
3382
|
function recordMessage(agent, message) {
|
|
3382
3383
|
if (agent.status === 'idle' && typeof agent.session?.append === 'function' && hasSystemHead(agent.session)) {
|
|
@@ -3755,6 +3756,33 @@ export function apply(ctx, config) {
|
|
|
3755
3756
|
return parts.join(' ')
|
|
3756
3757
|
}
|
|
3757
3758
|
|
|
3759
|
+
/**
|
|
3760
|
+
* The source of a send's copy in the bound session. It is not `user`, so
|
|
3761
|
+
* DSH does not take the copy for something the user typed (Web UI bubble,
|
|
3762
|
+
* session title, turn outline, last-prompt time) and the mirror skips it.
|
|
3763
|
+
* `form: 'notice'` with `summary` is how DSH's Chat shows it when the
|
|
3764
|
+
* bot's client is not loaded: a one-line notice row. The bot's client
|
|
3765
|
+
* draws it in full, pictures included, on the assistant's side.
|
|
3766
|
+
* @param post - `{ header, threadId, topic, from, self }`; `header` is the
|
|
3767
|
+
* line the model also reads, `from` the request's sender.
|
|
3768
|
+
* @returns the message source.
|
|
3769
|
+
*/
|
|
3770
|
+
function postSource({ header, threadId, topic, from, self }) {
|
|
3771
|
+
const job = typeof from?.job === 'string' && from.job.trim().length > 0 ? from.job.trim() : null
|
|
3772
|
+
const label = typeof from?.label === 'string' && from.label.trim().length > 0 ? from.label.trim() : null
|
|
3773
|
+
return {
|
|
3774
|
+
kind: POST_SOURCE_KIND,
|
|
3775
|
+
form: 'notice',
|
|
3776
|
+
summary: header.slice(0, 120),
|
|
3777
|
+
threadId,
|
|
3778
|
+
topic: topic ?? null,
|
|
3779
|
+
job,
|
|
3780
|
+
label: job === null ? label : null,
|
|
3781
|
+
fromSession: typeof from?.sessionId === 'string' && from.sessionId.length > 0 ? from.sessionId : null,
|
|
3782
|
+
self: self === true,
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3758
3786
|
/**
|
|
3759
3787
|
* An error a retry will not fix: callers such as scheduled-job delivery
|
|
3760
3788
|
* read `retryable: false` and stop trying.
|
|
@@ -3881,14 +3909,19 @@ export function apply(ctx, config) {
|
|
|
3881
3909
|
const self = sessionId === request?.from?.sessionId
|
|
3882
3910
|
const label = self ? ' by this session' : sender.length > 0 ? ` by ${sender}` : ''
|
|
3883
3911
|
const where = target.name === undefined ? `topic ${threadId}` : `topic "${target.name}"`
|
|
3912
|
+
const header = `Sent to the Telegram ${where}${label}`
|
|
3884
3913
|
const stickerLine = sticker === undefined ? '' : `\n[Sticker ${describeSticker(sticker)}]`
|
|
3885
|
-
const content = [{ type: 'text', text: `[
|
|
3914
|
+
const content = [{ type: 'text', text: `[${header}]${text.trim().length > 0 ? `\n${text.trim()}` : ''}${stickerLine}` }]
|
|
3886
3915
|
content.push(...await recordedFiles(files))
|
|
3887
3916
|
if (sticker !== undefined) content.push(...await recordedSticker(sticker, signal))
|
|
3888
|
-
|
|
3889
|
-
//
|
|
3890
|
-
|
|
3891
|
-
|
|
3917
|
+
// Recording never starts a turn. The model reads it as user-role
|
|
3918
|
+
// context; the source says it is a post, not something the user typed.
|
|
3919
|
+
recordMessage(agent, {
|
|
3920
|
+
id: randomUUID(),
|
|
3921
|
+
role: 'user',
|
|
3922
|
+
content,
|
|
3923
|
+
source: postSource({ header, threadId, topic: target.name, from: request?.from, self }),
|
|
3924
|
+
})
|
|
3892
3925
|
recorded = true
|
|
3893
3926
|
} catch (error) {
|
|
3894
3927
|
// The message is already in Telegram; report the missing record
|
package/src/client.js
CHANGED
|
@@ -70,6 +70,19 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
loadingSettings: 'Loading settings…',
|
|
71
71
|
unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
|
|
72
72
|
readOnly: 'This deployment stores settings read-only.',
|
|
73
|
+
postTitle: 'Sent to Telegram',
|
|
74
|
+
postTopic: 'topic',
|
|
75
|
+
postJob: 'job',
|
|
76
|
+
postSession: 'session',
|
|
77
|
+
postOtherSession: 'another session',
|
|
78
|
+
postSticker: 'Sticker',
|
|
79
|
+
postFile: 'file',
|
|
80
|
+
postCopy: 'Copy',
|
|
81
|
+
postCopied: 'Copied',
|
|
82
|
+
postCode: 'Code',
|
|
83
|
+
postWrap: 'Wrap',
|
|
84
|
+
postUnwrap: 'Unwrap',
|
|
85
|
+
postFootnotes: 'Footnotes',
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
const ru = {
|
|
@@ -127,6 +140,19 @@ window.__ModuleLoader__.load({
|
|
|
127
140
|
loadingSettings: 'Загрузка настроек…',
|
|
128
141
|
unavailable: 'Плагин не загружен, поэтому настройки сейчас недоступны.',
|
|
129
142
|
readOnly: 'Этот deployment хранит настройки только для чтения.',
|
|
143
|
+
postTitle: 'Отправлено в Telegram',
|
|
144
|
+
postTopic: 'тема',
|
|
145
|
+
postJob: 'задача',
|
|
146
|
+
postSession: 'сессия',
|
|
147
|
+
postOtherSession: 'другая сессия',
|
|
148
|
+
postSticker: 'Стикер',
|
|
149
|
+
postFile: 'файл',
|
|
150
|
+
postCopy: 'Копировать',
|
|
151
|
+
postCopied: 'Скопировано',
|
|
152
|
+
postCode: 'Код',
|
|
153
|
+
postWrap: 'Переносить',
|
|
154
|
+
postUnwrap: 'Не переносить',
|
|
155
|
+
postFootnotes: 'Сноски',
|
|
130
156
|
}
|
|
131
157
|
|
|
132
158
|
const CSS = `
|
|
@@ -730,6 +756,293 @@ window.__ModuleLoader__.load({
|
|
|
730
756
|
)
|
|
731
757
|
}
|
|
732
758
|
|
|
759
|
+
// ---- Telegram posts in the Chat transcript ----
|
|
760
|
+
//
|
|
761
|
+
// A send into a topic leaves a user-role copy in the topic's session, so
|
|
762
|
+
// the model knows what was said there. DSH's Chat would draw an ordinary
|
|
763
|
+
// user message as the user's own bubble; these Definitions claim the copy
|
|
764
|
+
// and draw it as what it is: something sent to Telegram, on the
|
|
765
|
+
// assistant's side, pictures included. The recorded message is unchanged.
|
|
766
|
+
|
|
767
|
+
/** Chat node kind, conversation Definition kind and Host source kind of a post. */
|
|
768
|
+
const POST_KIND = 'telegram-post'
|
|
769
|
+
/** Request-id prefix of posts recorded before they had their own source kind. */
|
|
770
|
+
const LEGACY_POST_RPC = 'telegram-post:'
|
|
771
|
+
/** First text line of every post: what the model reads as its sender. */
|
|
772
|
+
const HEADER_LINE = /^\[(Sent to the Telegram [^\n]*)\]$/u
|
|
773
|
+
/** Line naming a sent sticker; its still picture follows as an image. */
|
|
774
|
+
const STICKER_LINE = /^\[Sticker ([^\n]*)\]$/u
|
|
775
|
+
/** The header's own words, for posts that carry no structured source. */
|
|
776
|
+
const HEADER_PARTS = /^Sent to the Telegram topic (?:"(.*)"|(\d+))(?: by (.*))?$/u
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Whether a durable message source marks a topic send's copy.
|
|
780
|
+
* @param source - `user/message` source.
|
|
781
|
+
* @returns true for posts, old and new.
|
|
782
|
+
*/
|
|
783
|
+
function isPostSource(source) {
|
|
784
|
+
if (typeof source !== 'object' || source === null) return false
|
|
785
|
+
if (source.kind === POST_KIND) return true
|
|
786
|
+
return source.kind === 'user' && typeof source.rpcId === 'string' && source.rpcId.startsWith(LEGACY_POST_RPC)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Claim a post's `user/message` on the Chat target. Only append-origin
|
|
791
|
+
* copies are claimed: a replacement copy (compaction) belongs to the
|
|
792
|
+
* system-prompt Definition and is never drawn.
|
|
793
|
+
* @param event - Client history event.
|
|
794
|
+
* @returns the match, or null when the event is not a post.
|
|
795
|
+
*/
|
|
796
|
+
function matchPost(event) {
|
|
797
|
+
if (event?.type !== 'user/message' || event.surfaceOp !== 'append') return null
|
|
798
|
+
const data = event.data
|
|
799
|
+
if (data === null || typeof data !== 'object' || !isPostSource(data.source)) return null
|
|
800
|
+
return { id: String(data.id), role: 'start', exclusive: true }
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Who sent a post and where, from its source or, for old records, from
|
|
805
|
+
* the header line the model reads.
|
|
806
|
+
* @param source - the post's source.
|
|
807
|
+
* @param header - header line without brackets, when present.
|
|
808
|
+
* @returns `{ topic, threadId, job, label, fromSession, self }`; unknown fields are null.
|
|
809
|
+
*/
|
|
810
|
+
function postMeta(source, header) {
|
|
811
|
+
if (source?.kind === POST_KIND) {
|
|
812
|
+
return {
|
|
813
|
+
topic: typeof source.topic === 'string' ? source.topic : null,
|
|
814
|
+
threadId: typeof source.threadId === 'number' ? source.threadId : null,
|
|
815
|
+
job: typeof source.job === 'string' ? source.job : null,
|
|
816
|
+
label: typeof source.label === 'string' ? source.label : null,
|
|
817
|
+
fromSession: typeof source.fromSession === 'string' ? source.fromSession : null,
|
|
818
|
+
self: source.self === true,
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const meta = { topic: null, threadId: null, job: null, label: null, fromSession: null, self: false }
|
|
822
|
+
const parts = header === null ? null : HEADER_PARTS.exec(header)
|
|
823
|
+
if (parts === null) return meta
|
|
824
|
+
if (parts[1] !== undefined) meta.topic = parts[1]
|
|
825
|
+
else meta.threadId = Number(parts[2])
|
|
826
|
+
const by = parts[3]
|
|
827
|
+
if (by === undefined) return meta
|
|
828
|
+
if (by === 'this session') {
|
|
829
|
+
meta.self = true
|
|
830
|
+
return meta
|
|
831
|
+
}
|
|
832
|
+
const job = /^job "(.*?)"(?: from session (\S+))?$/u.exec(by)
|
|
833
|
+
const session = /^session (\S+)$/u.exec(by)
|
|
834
|
+
if (job !== null) {
|
|
835
|
+
meta.job = job[1]
|
|
836
|
+
meta.fromSession = job[2] ?? null
|
|
837
|
+
} else if (session !== null) {
|
|
838
|
+
meta.fromSession = session[1]
|
|
839
|
+
} else {
|
|
840
|
+
meta.label = by
|
|
841
|
+
}
|
|
842
|
+
return meta
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Split a post's recorded content into what the transcript shows.
|
|
847
|
+
* @param content - the message's content blocks.
|
|
848
|
+
* @param source - the message's source.
|
|
849
|
+
* @returns `{ meta, text, stickers, images, files }`: the body without
|
|
850
|
+
* its header and sticker lines, sticker descriptions, image sources
|
|
851
|
+
* for the gallery and file attachments.
|
|
852
|
+
*/
|
|
853
|
+
function readPost(content, source) {
|
|
854
|
+
const blocks = Array.isArray(content) ? content : []
|
|
855
|
+
const lines = blocks
|
|
856
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
857
|
+
.map((block) => block.text)
|
|
858
|
+
.join('\n')
|
|
859
|
+
.split('\n')
|
|
860
|
+
const first = HEADER_LINE.exec(lines[0] ?? '')
|
|
861
|
+
if (first !== null) lines.shift()
|
|
862
|
+
const stickers = []
|
|
863
|
+
const body = []
|
|
864
|
+
for (const line of lines) {
|
|
865
|
+
const sticker = STICKER_LINE.exec(line)
|
|
866
|
+
if (sticker !== null) stickers.push(sticker[1])
|
|
867
|
+
else body.push(line)
|
|
868
|
+
}
|
|
869
|
+
const images = []
|
|
870
|
+
const files = []
|
|
871
|
+
for (const block of blocks) {
|
|
872
|
+
if (block?.attachment === undefined || block.attachment === null) continue
|
|
873
|
+
if (block.type === 'image') {
|
|
874
|
+
const name = block.attachment.name
|
|
875
|
+
images.push(typeof name === 'string' && name.length > 0 ? { attachment: block.attachment, label: name } : { attachment: block.attachment })
|
|
876
|
+
} else if (block.type === 'file') {
|
|
877
|
+
files.push(block.attachment)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
meta: postMeta(source, first === null ? null : first[1]),
|
|
882
|
+
text: body.join('\n').trim(),
|
|
883
|
+
stickers,
|
|
884
|
+
images,
|
|
885
|
+
files,
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Chat Definition for topic sends. The row stays in its Turn outside the
|
|
891
|
+
* folded work rows (`turn`): it is something sent, not a step of work,
|
|
892
|
+
* and it neither opens nor joins a Turn's input.
|
|
893
|
+
*/
|
|
894
|
+
const postDefinition = {
|
|
895
|
+
kind: POST_KIND,
|
|
896
|
+
target: 'chat',
|
|
897
|
+
match: { 'user/message': matchPost },
|
|
898
|
+
start: (_context, match) => {
|
|
899
|
+
const event = match.event
|
|
900
|
+
return {
|
|
901
|
+
seq: event.seq,
|
|
902
|
+
time: event.time,
|
|
903
|
+
...readPost(event.data.content, event.data.source),
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
update: (context) => context.state,
|
|
907
|
+
buildViewNode: (context) => {
|
|
908
|
+
if (context.state === undefined) return null
|
|
909
|
+
const location = context.start?.location ?? context.matches[0]?.location ?? { kind: 'unresolved' }
|
|
910
|
+
return {
|
|
911
|
+
key: context.key,
|
|
912
|
+
kind: POST_KIND,
|
|
913
|
+
id: context.id,
|
|
914
|
+
target: 'chat',
|
|
915
|
+
anchorSeq: context.state.seq,
|
|
916
|
+
location,
|
|
917
|
+
visibility: 'visible',
|
|
918
|
+
groupTree: location.kind === 'turn' || location.kind === 'step' ? 'turn' : undefined,
|
|
919
|
+
data: context.state,
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Where and from whom, as short header pieces.
|
|
926
|
+
* @param meta - see postMeta.
|
|
927
|
+
* @param t - locale reader.
|
|
928
|
+
* @returns `{ where, who, whoTitle }`; `who` is null for the session's
|
|
929
|
+
* own sends, which already sit on its side of the transcript.
|
|
930
|
+
*/
|
|
931
|
+
function postLabels(meta, t) {
|
|
932
|
+
const where = meta.topic !== null ? meta.topic : meta.threadId !== null ? `${t('postTopic')} ${meta.threadId}` : null
|
|
933
|
+
let who = null
|
|
934
|
+
let whoTitle
|
|
935
|
+
if (meta.job !== null) {
|
|
936
|
+
who = `${t('postJob')} ${meta.job}`
|
|
937
|
+
if (meta.fromSession !== null) whoTitle = `${t('postSession')} ${meta.fromSession}`
|
|
938
|
+
} else if (!meta.self && meta.fromSession !== null) {
|
|
939
|
+
who = t('postOtherSession')
|
|
940
|
+
whoTitle = `${t('postSession')} ${meta.fromSession}`
|
|
941
|
+
} else if (!meta.self && meta.label !== null) {
|
|
942
|
+
who = meta.label
|
|
943
|
+
}
|
|
944
|
+
return { where, who, whoTitle }
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Clock and full date of the post, in the browser's locale.
|
|
949
|
+
* @param time - event time (epoch milliseconds or ISO text).
|
|
950
|
+
* @returns `{ clock, iso, full }`, or null for a missing time.
|
|
951
|
+
*/
|
|
952
|
+
function postTime(time) {
|
|
953
|
+
if (time === undefined || time === null) return null
|
|
954
|
+
const date = new Date(time)
|
|
955
|
+
if (Number.isNaN(date.getTime())) return null
|
|
956
|
+
return {
|
|
957
|
+
clock: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
958
|
+
iso: date.toISOString(),
|
|
959
|
+
full: date.toLocaleString(),
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Transcript styles; theme tokens keep it in step with light and dark. */
|
|
964
|
+
const POST_CSS = `
|
|
965
|
+
.dtb-post{display:flex;flex-direction:column;gap:6px;min-width:0;padding:2px 0 2px 12px;border-left:2px solid var(--dsw-alias-border-l2,rgba(127,127,127,.35))}
|
|
966
|
+
.dtb-postHead{display:flex;align-items:center;gap:6px;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px))}
|
|
967
|
+
.dtb-postIcon{display:inline-flex;flex:none}
|
|
968
|
+
.dtb-postTitle{flex:none}
|
|
969
|
+
.dtb-postSep{flex:none;width:2px;height:2px;border-radius:1px;background:var(--dsw-alias-label-caption,currentColor)}
|
|
970
|
+
.dtb-postMeta{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
971
|
+
.dtb-postTime{flex:none;margin-left:auto;font:var(--dsw-font-xxs-12,12px/16px inherit)}
|
|
972
|
+
.dtb-postBody{min-width:0;color:var(--dsw-alias-label-primary)}
|
|
973
|
+
.dtb-postPlain{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
974
|
+
.dtb-postCaption{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}
|
|
975
|
+
.dtb-postFiles{display:flex;flex-wrap:wrap;gap:6px}
|
|
976
|
+
.dtb-postFile{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:var(--dsw-radius-md,8px);padding:4px 10px;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
977
|
+
`
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Markdown control labels from this plugin's dictionary.
|
|
981
|
+
* @param t - locale reader.
|
|
982
|
+
*/
|
|
983
|
+
function postMarkdownLabels(t) {
|
|
984
|
+
return {
|
|
985
|
+
code: {
|
|
986
|
+
copyLabel: t('postCopy'),
|
|
987
|
+
copiedLabel: t('postCopied'),
|
|
988
|
+
toolbarLabels: { codeLabel: t('postCode'), wrapLabel: t('postWrap'), unwrapLabel: t('postUnwrap') },
|
|
989
|
+
},
|
|
990
|
+
footnotes: t('postFootnotes'),
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Keyed Chat renderer of one post.
|
|
996
|
+
* @param props - `node`, `loadImage`, `renderFactorySlot` and `t` from the Chat node slot.
|
|
997
|
+
*/
|
|
998
|
+
function TelegramPostView(props) {
|
|
999
|
+
const { node, loadImage, renderFactorySlot, t } = props
|
|
1000
|
+
const data = node.data
|
|
1001
|
+
const labels = React.useMemo(() => postMarkdownLabels(t), [t])
|
|
1002
|
+
const { where, who, whoTitle } = postLabels(data.meta, t)
|
|
1003
|
+
const time = postTime(data.time)
|
|
1004
|
+
const Icon = primitives.IconPaperPlaneOutlineRegular
|
|
1005
|
+
const Markdown = primitives.MarkdownText
|
|
1006
|
+
const head = [
|
|
1007
|
+
h('span', { key: 'icon', className: 'dtb-postIcon', 'aria-hidden': true }, Icon === undefined ? null : h(Icon, { size: 14 })),
|
|
1008
|
+
h('span', { key: 'title', className: 'dtb-postTitle' }, t('postTitle')),
|
|
1009
|
+
]
|
|
1010
|
+
if (where !== null) {
|
|
1011
|
+
head.push(h('span', { key: 'sep-where', className: 'dtb-postSep', 'aria-hidden': true }))
|
|
1012
|
+
head.push(h('span', { key: 'where', className: 'dtb-postMeta' }, where))
|
|
1013
|
+
}
|
|
1014
|
+
if (who !== null) {
|
|
1015
|
+
head.push(h('span', { key: 'sep-who', className: 'dtb-postSep', 'aria-hidden': true }))
|
|
1016
|
+
head.push(h('span', { key: 'who', className: 'dtb-postMeta', title: whoTitle }, who))
|
|
1017
|
+
}
|
|
1018
|
+
if (time !== null) head.push(h('time', { key: 'time', className: 'dtb-postTime', dateTime: time.iso, title: time.full }, time.clock))
|
|
1019
|
+
return h(
|
|
1020
|
+
'div',
|
|
1021
|
+
{ className: 'dtb-post', 'data-telegram-post': true },
|
|
1022
|
+
h('div', { className: 'dtb-postHead' }, ...head),
|
|
1023
|
+
data.text.length === 0
|
|
1024
|
+
? null
|
|
1025
|
+
: h(
|
|
1026
|
+
'div',
|
|
1027
|
+
{ className: 'dtb-postBody' },
|
|
1028
|
+
Markdown === undefined ? h('div', { className: 'dtb-postPlain' }, data.text) : h(Markdown, { text: data.text, labels }),
|
|
1029
|
+
),
|
|
1030
|
+
data.images.length === 0 || typeof renderFactorySlot !== 'function'
|
|
1031
|
+
? null
|
|
1032
|
+
: renderFactorySlot('conversation.images.content', { images: data.images, loadImage, align: 'start' }),
|
|
1033
|
+
...data.stickers.map((sticker, index) => h('div', { key: `sticker-${index}`, className: 'dtb-postCaption' }, `${t('postSticker')} ${sticker}`)),
|
|
1034
|
+
data.files.length === 0
|
|
1035
|
+
? null
|
|
1036
|
+
: h(
|
|
1037
|
+
'div',
|
|
1038
|
+
{ className: 'dtb-postFiles' },
|
|
1039
|
+
...data.files.map((file, index) =>
|
|
1040
|
+
h('span', { key: `file-${index}`, className: 'dtb-postFile', title: file.name }, `📎 ${file.name ?? t('postFile')}`),
|
|
1041
|
+
),
|
|
1042
|
+
),
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
733
1046
|
/** Required client services. */
|
|
734
1047
|
const inject = ['slots', 'locale', 'remote', 'remote.credentials', 'configForms']
|
|
735
1048
|
|
|
@@ -799,11 +1112,32 @@ window.__ModuleLoader__.load({
|
|
|
799
1112
|
),
|
|
800
1113
|
'dsh-telegram-bot: bundle settings',
|
|
801
1114
|
)
|
|
1115
|
+
|
|
1116
|
+
if (typeof document !== 'undefined') {
|
|
1117
|
+
ctx.effect(() => {
|
|
1118
|
+
const style = document.createElement('style')
|
|
1119
|
+
style.dataset.plugin = '@soffits/dsh-telegram-bot'
|
|
1120
|
+
style.textContent = POST_CSS
|
|
1121
|
+
document.head.appendChild(style)
|
|
1122
|
+
return () => style.remove()
|
|
1123
|
+
}, 'dsh-telegram-bot: post styles')
|
|
1124
|
+
}
|
|
1125
|
+
// Chat may be absent (a client without the transcript); settings still load.
|
|
1126
|
+
ctx.inject(['uiConversation'], (scope) => {
|
|
1127
|
+
scope.uiConversation.events.register(postDefinition)
|
|
1128
|
+
})
|
|
1129
|
+
ctx.slots.inject('conversation.chat.node', () =>
|
|
1130
|
+
ctx.slots.register({ name: 'conversation.chat.node', key: POST_KIND, locale: NS }, TelegramPostView),
|
|
1131
|
+
)
|
|
802
1132
|
}
|
|
803
1133
|
|
|
804
1134
|
exports.NS = NS
|
|
805
1135
|
exports.apply = apply
|
|
806
1136
|
exports.inject = inject
|
|
1137
|
+
exports.postDefinition = postDefinition
|
|
1138
|
+
exports.readPost = readPost
|
|
1139
|
+
exports.postLabels = postLabels
|
|
1140
|
+
exports.TelegramPostView = TelegramPostView
|
|
807
1141
|
return module.exports
|
|
808
1142
|
},
|
|
809
1143
|
})
|
package/src/constants.js
CHANGED
|
@@ -19,6 +19,12 @@ export const FORWARD_BATCH_MS = 1200
|
|
|
19
19
|
/** Loader row id and settings namespace; the npm package name is @soffits/dsh-telegram-bot. */
|
|
20
20
|
export const name = 'telegram-bot'
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Source kind of the copy a topic send leaves in the bound session; the
|
|
24
|
+
* client draws these as posts on the assistant's side.
|
|
25
|
+
*/
|
|
26
|
+
export const POST_SOURCE_KIND = 'telegram-post'
|
|
27
|
+
|
|
22
28
|
/** Command menu shown by Telegram clients while the user types `/`. */
|
|
23
29
|
const BOT_COMMANDS = [
|
|
24
30
|
{ command: 'start', description: 'Start the bot and show help' },
|