@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.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 +73 -55
- package/docs/access-control-design.md +7 -5
- package/docs/attachment-pipeline-v2.md +33 -41
- package/docs/feishu-onboarding.md +33 -33
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +1 -1
- package/docs/workspace-isolation-design.md +7 -8
- package/lib/client.js +173 -287
- package/lib/client.js.map +1 -1
- package/lib/index.js +3001 -2627
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/cards.ts +9 -9
- package/src/access/controller.ts +10 -10
- package/src/access/index.ts +1 -0
- package/src/access/policy.ts +10 -10
- package/src/access/requests.ts +32 -31
- package/src/access/store.ts +12 -12
- package/src/access/targets.ts +56 -0
- package/src/cards/approval.ts +96 -26
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +1 -0
- package/src/cards/picker.ts +115 -0
- package/src/cards/streaming.ts +56 -7
- package/src/client/api.ts +5 -25
- package/src/client/panel/access-card.tsx +24 -15
- package/src/client/panel/access-tab.tsx +3 -3
- package/src/client/panel/connection-tab.tsx +70 -1
- package/src/client/panel/index.tsx +11 -62
- package/src/client/panel/shared.tsx +0 -23
- package/src/client/panel/workspace-tab.tsx +2 -18
- package/src/client/styles.ts +2 -0
- package/src/commands/registry.ts +2 -2
- package/src/config.ts +25 -56
- package/src/domain.ts +28 -125
- package/src/feishu/inbound-attachments.ts +1 -1
- package/src/feishu/manifest.ts +8 -31
- package/src/feishu/message-read.ts +19 -22
- package/src/feishu/scopes.ts +171 -0
- package/src/feishu/send-file.ts +543 -0
- package/src/feishu/slash-commands.ts +134 -0
- package/src/feishu/tools.ts +72 -112
- package/src/feishu-channel.ts +270 -9
- package/src/index.ts +5 -4
- package/src/interactions/approvals.ts +187 -7
- package/src/interactions/pending.ts +3 -3
- package/src/routing/inbound.ts +73 -106
- package/src/service.ts +463 -438
- package/src/session/manager.ts +1 -1
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +68 -85
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +37 -88
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +21 -29
- package/src/cleanup.ts +0 -546
- package/src/client/panel/cleanup-card.tsx +0 -106
- package/src/feishu/pending-attachments.ts +0 -562
package/src/feishu/tools.ts
CHANGED
|
@@ -14,21 +14,19 @@
|
|
|
14
14
|
* against a per-route whitelist of messages the channel has already admitted
|
|
15
15
|
* as relevant:
|
|
16
16
|
*
|
|
17
|
-
* - the triggering message
|
|
18
|
-
*
|
|
19
|
-
* - silently registered attachment messages from the `pending_attachments`
|
|
20
|
-
* registry (same `(chat, thread?, sender)` scope rules);
|
|
17
|
+
* - the triggering message, a quoted message, and history rows, registered by
|
|
18
|
+
* the inbound layer through {@link AttachmentFetchToolRuntime.noteKnownMessage};
|
|
21
19
|
* - messages this tool already resolved (successful or failed attempts), so a
|
|
22
|
-
* later turn in the same session keeps its authorized resources
|
|
20
|
+
* later turn in the same session keeps its authorized resources;
|
|
21
|
+
* - on a whitelist miss, a message the tool reads back from Feishu and finds to
|
|
22
|
+
* be the route owner's own message inside the route's container (its topic,
|
|
23
|
+
* or the main timeline of a degraded chat route); another topic never
|
|
24
|
+
* matches.
|
|
23
25
|
*
|
|
24
|
-
* The whitelist is an in-memory LRU per route
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* anything. The whitelist is not replayed from Session history: after a host
|
|
29
|
-
* restart the admission/manifest layer re-registers the current turn's
|
|
30
|
-
* messages, while older attachments remain reachable through the durable
|
|
31
|
-
* pending registry until its TTL.
|
|
26
|
+
* The whitelist is an in-memory LRU per route and is not replayed from Session
|
|
27
|
+
* history; the read-back rule keeps the owner's earlier attachments fetchable
|
|
28
|
+
* after a host restart. A prompt-injected message id from another chat, user,
|
|
29
|
+
* or topic therefore cannot download anything.
|
|
32
30
|
*
|
|
33
31
|
* Failure model: every per-resource failure is a structured value (never a
|
|
34
32
|
* thrown raw error) so one bad resource does not abort the call and session
|
|
@@ -40,7 +38,6 @@
|
|
|
40
38
|
|
|
41
39
|
import { z } from 'zod'
|
|
42
40
|
import type { FeishuConfig } from '../config.ts'
|
|
43
|
-
import type { PendingAttachmentRecord, PendingAttachmentResource } from '../domain.ts'
|
|
44
41
|
import type {
|
|
45
42
|
AgentContext, ToolContentBlock, ToolDefinition, ToolExecution,
|
|
46
43
|
} from '../host/agent.ts'
|
|
@@ -70,7 +67,6 @@ import {
|
|
|
70
67
|
import type {
|
|
71
68
|
AttachmentFailureCode, AttachmentResult, NativeAttachmentStore, StoredFile, StoredImage,
|
|
72
69
|
} from './native-attachments.ts'
|
|
73
|
-
import type { PendingAttachmentScope, PendingAttachmentStore } from './pending-attachments.ts'
|
|
74
70
|
|
|
75
71
|
// ---- Public identity ----------------------------------------------------
|
|
76
72
|
|
|
@@ -90,10 +86,10 @@ export const DEFAULT_KNOWN_MESSAGES_PER_ROUTE = 128
|
|
|
90
86
|
* must not block session creation; if they appear later the pending injection
|
|
91
87
|
* still registers the tool.
|
|
92
88
|
*/
|
|
93
|
-
const MOUNT_INJECT_WAIT_MS = 2_000
|
|
89
|
+
export const MOUNT_INJECT_WAIT_MS = 2_000
|
|
94
90
|
|
|
95
91
|
/** Retry wait for later sessions once the first mount timed out (fast-fail). */
|
|
96
|
-
const MOUNT_INJECT_RETRY_WAIT_MS = 250
|
|
92
|
+
export const MOUNT_INJECT_RETRY_WAIT_MS = 250
|
|
97
93
|
|
|
98
94
|
// ---- Route / whitelist vocabulary ---------------------------------------
|
|
99
95
|
|
|
@@ -108,14 +104,13 @@ export interface AttachmentFetchRoute {
|
|
|
108
104
|
readonly kind: 'thread' | 'chat'
|
|
109
105
|
readonly chatId: string
|
|
110
106
|
readonly chatType: 'p2p' | 'group'
|
|
111
|
-
readonly
|
|
107
|
+
readonly senderUserId: string
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
/** Why a message became known to one route. */
|
|
115
111
|
export type KnownAttachmentSource =
|
|
116
112
|
| 'trigger'
|
|
117
113
|
| 'quote'
|
|
118
|
-
| 'pending'
|
|
119
114
|
| 'tool-result'
|
|
120
115
|
| 'manual'
|
|
121
116
|
/** A message discovered while reading topic history/quote context (P2). */
|
|
@@ -138,7 +133,7 @@ export interface KnownAttachmentMessage {
|
|
|
138
133
|
readonly kind: 'message' | 'merge_forward'
|
|
139
134
|
readonly resources: readonly KnownAttachmentResource[]
|
|
140
135
|
/** Sender the message was attributed to when it entered the whitelist. */
|
|
141
|
-
readonly
|
|
136
|
+
readonly senderUserId?: string
|
|
142
137
|
}
|
|
143
138
|
|
|
144
139
|
/** Registration input for the whitelist. */
|
|
@@ -162,17 +157,7 @@ function routesMatch(left: AttachmentFetchRoute, right: AttachmentFetchRoute): b
|
|
|
162
157
|
&& left.kind === right.kind
|
|
163
158
|
&& left.chatId === right.chatId
|
|
164
159
|
&& left.chatType === right.chatType
|
|
165
|
-
&& left.
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Pending-registry scope for one route (`threadId` only for topic routes). */
|
|
169
|
-
export function pendingScopeOfRoute(route: AttachmentFetchRoute): PendingAttachmentScope {
|
|
170
|
-
const threadId = threadIdOfRoute(route)
|
|
171
|
-
return {
|
|
172
|
-
chatId: route.chatId,
|
|
173
|
-
senderOpenId: route.senderOpenId,
|
|
174
|
-
...(threadId === undefined ? {} : { threadId }),
|
|
175
|
-
}
|
|
160
|
+
&& left.senderUserId === right.senderUserId
|
|
176
161
|
}
|
|
177
162
|
|
|
178
163
|
interface WhitelistedMessage {
|
|
@@ -185,11 +170,10 @@ interface WhitelistedMessage {
|
|
|
185
170
|
/**
|
|
186
171
|
* Per-route LRU of messages the channel has declared relevant.
|
|
187
172
|
*
|
|
188
|
-
* The store is intentionally process-local:
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* history into the whitelist).
|
|
173
|
+
* The store is intentionally process-local: after a restart the inbound layer
|
|
174
|
+
* re-registers the current turn's messages, and the fetch tool's owner
|
|
175
|
+
* read-back authorizes the owner's earlier messages; Session history is never
|
|
176
|
+
* replayed into the whitelist.
|
|
193
177
|
*/
|
|
194
178
|
export class RouteAttachmentWhitelist {
|
|
195
179
|
private readonly routes = new Map<string, Map<string, WhitelistedMessage>>()
|
|
@@ -216,7 +200,7 @@ export class RouteAttachmentWhitelist {
|
|
|
216
200
|
messageId: message.messageId,
|
|
217
201
|
kind: message.kind,
|
|
218
202
|
resources: message.resources.map(resource => ({ ...resource })),
|
|
219
|
-
...(message.
|
|
203
|
+
...(message.senderUserId === undefined ? {} : { senderUserId: message.senderUserId }),
|
|
220
204
|
},
|
|
221
205
|
source,
|
|
222
206
|
registeredAt: now,
|
|
@@ -285,7 +269,7 @@ export type AttachmentFetchFailureCode =
|
|
|
285
269
|
|
|
286
270
|
const FETCH_FAILURE_REASONS: Readonly<Record<string, string>> = {
|
|
287
271
|
ROUTE_NOT_FOUND: '当前会话尚未绑定飞书对话,无法获取附件',
|
|
288
|
-
AUTHORIZATION_DENIED: '
|
|
272
|
+
AUTHORIZATION_DENIED: '该消息不在本会话的附件授权范围内(仅可获取触发消息、引用消息、附件清单中列出的消息或本人在本话题发送的消息)',
|
|
289
273
|
CHANNEL_UNAVAILABLE: '飞书连接未运行,无法获取附件',
|
|
290
274
|
MESSAGE_READ_FAILED: '读取飞书消息内容失败',
|
|
291
275
|
NO_RESOURCES: '该消息不包含可下载的图片或文件',
|
|
@@ -327,6 +311,9 @@ function failure(code: AttachmentFetchFailureCode, reason?: string): AttachmentF
|
|
|
327
311
|
* schema is the authoritative argument gate; it also gives tests and
|
|
328
312
|
* embedded/non-native callers a typed value.
|
|
329
313
|
*/
|
|
314
|
+
/** Feishu message id accepted from model arguments before any Feishu request. */
|
|
315
|
+
const MESSAGE_ID_PATTERN = /^om_[A-Za-z0-9_-]{1,128}$/
|
|
316
|
+
|
|
330
317
|
export const attachmentFetchArgsSchema = z.object({
|
|
331
318
|
message_id: z.string().min(1),
|
|
332
319
|
file_key: z.string().min(1).optional(),
|
|
@@ -423,8 +410,6 @@ export interface AttachmentFetchRuntimeDeps {
|
|
|
423
410
|
config(): FeishuConfig
|
|
424
411
|
/** Live Feishu channel; undefined while disconnected/shut down. */
|
|
425
412
|
channel(): FeishuChannel | undefined
|
|
426
|
-
/** Durable silent-registration registry (authorization + consumption). */
|
|
427
|
-
pending?(): PendingAttachmentStore | undefined
|
|
428
413
|
/** Resolve the route currently bound to one session. */
|
|
429
414
|
routeOfSession(sessionId: string): AttachmentFetchRoute | undefined
|
|
430
415
|
/** Structured warning sink (mount degradation, unexpected failures). */
|
|
@@ -443,7 +428,6 @@ interface FetchCallContext {
|
|
|
443
428
|
readonly native: NativeAttachmentStore
|
|
444
429
|
readonly imageCap: number
|
|
445
430
|
readonly channel: FeishuChannel | undefined
|
|
446
|
-
readonly pending: PendingAttachmentStore | undefined
|
|
447
431
|
}
|
|
448
432
|
|
|
449
433
|
interface DownloadBudget {
|
|
@@ -496,25 +480,13 @@ export class AttachmentFetchToolRuntime {
|
|
|
496
480
|
}
|
|
497
481
|
|
|
498
482
|
/**
|
|
499
|
-
* Whether one message id is
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* message (no `thread_id`) may be detached into the current topic route.
|
|
504
|
-
*
|
|
505
|
-
* Never throws: an unavailable registry fails closed.
|
|
483
|
+
* Whether one message id is in the route's whitelist. The
|
|
484
|
+
* `feishu_topic_messages` tool calls this to decide whether another sender's
|
|
485
|
+
* main-timeline message (no `thread_id`) may be read from the current topic
|
|
486
|
+
* route.
|
|
506
487
|
*/
|
|
507
488
|
isKnownMessage(route: AttachmentFetchRoute, messageId: string): boolean {
|
|
508
|
-
|
|
509
|
-
const pending = this.deps.pending?.()
|
|
510
|
-
if (pending === undefined) return false
|
|
511
|
-
try {
|
|
512
|
-
return pending.get(pendingScopeOfRoute(route), messageId) !== undefined
|
|
513
|
-
} catch {
|
|
514
|
-
// Authorization lookups fail closed; a storage hiccup must not throw into
|
|
515
|
-
// the topic tool's read path.
|
|
516
|
-
return false
|
|
517
|
-
}
|
|
489
|
+
return this.whitelist.lookup(route, messageId) !== undefined
|
|
518
490
|
}
|
|
519
491
|
|
|
520
492
|
/**
|
|
@@ -566,15 +538,15 @@ export class AttachmentFetchToolRuntime {
|
|
|
566
538
|
}
|
|
567
539
|
}
|
|
568
540
|
|
|
569
|
-
/** Drop the whitelist entries of one retired route (record GC
|
|
541
|
+
/** Drop the whitelist entries of one retired route (record GC). */
|
|
570
542
|
forgetRoute(routeKey: string): void {
|
|
571
543
|
this.whitelist.forget(routeKey)
|
|
572
544
|
}
|
|
573
545
|
|
|
574
546
|
/**
|
|
575
547
|
* Drop whitelist routes that no longer resolve as active route records. The
|
|
576
|
-
* inbound TTL path retires a thread route without
|
|
577
|
-
*
|
|
548
|
+
* inbound TTL path retires a thread route without calling `forgetRoute`, so
|
|
549
|
+
* the periodic GC reconciles the whitelist afterwards.
|
|
578
550
|
* @returns number of route buckets dropped.
|
|
579
551
|
*/
|
|
580
552
|
pruneRoutes(stillActive: (routeKey: string) => boolean): number {
|
|
@@ -702,7 +674,6 @@ export class AttachmentFetchToolRuntime {
|
|
|
702
674
|
native: nativeAttachmentStoreOf(agentCtx),
|
|
703
675
|
imageCap: imageByteCapOf(agentCtx),
|
|
704
676
|
channel: this.deps.channel(),
|
|
705
|
-
pending: this.deps.pending?.(),
|
|
706
677
|
}
|
|
707
678
|
try {
|
|
708
679
|
return await this.fetch(call)
|
|
@@ -726,8 +697,11 @@ export class AttachmentFetchToolRuntime {
|
|
|
726
697
|
}
|
|
727
698
|
|
|
728
699
|
// 1. Authorize the message against this route's known resources.
|
|
729
|
-
const known = this.resolveKnownMessage(call, messageId)
|
|
700
|
+
const known = await this.resolveKnownMessage(call, messageId)
|
|
730
701
|
if (known === undefined) {
|
|
702
|
+
if (call.signal.aborted) {
|
|
703
|
+
return callFailure(messageId, failure(call.timeout.value ? 'TIMEOUT' : 'ABORTED'))
|
|
704
|
+
}
|
|
731
705
|
return callFailure(messageId, failure('AUTHORIZATION_DENIED'))
|
|
732
706
|
}
|
|
733
707
|
|
|
@@ -796,21 +770,6 @@ export class AttachmentFetchToolRuntime {
|
|
|
796
770
|
}
|
|
797
771
|
|
|
798
772
|
const fetched = results.filter(item => item.ok).length
|
|
799
|
-
// A pending manifest row is consumed only once an attachment was actually
|
|
800
|
-
// fetched: a run where every download failed keeps the row discoverable
|
|
801
|
-
// (retries are still authorized by the whitelist entry recorded above).
|
|
802
|
-
// The durable write is best-effort and raced against the call signal, so a
|
|
803
|
-
// stalled storage backend can never keep the tool call pending.
|
|
804
|
-
if (fetched > 0 && call.pending !== undefined) {
|
|
805
|
-
try {
|
|
806
|
-
await cancellable(
|
|
807
|
-
Promise.resolve(call.pending.consume(pendingScopeOfRoute(call.route), known.messageId)),
|
|
808
|
-
call.signal,
|
|
809
|
-
)
|
|
810
|
-
} catch {
|
|
811
|
-
// Bookkeeping only: the attachments were already delivered.
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
773
|
const shownMessageId = displayText(messageId)
|
|
815
774
|
const summary = fetched > 0
|
|
816
775
|
? `已获取 ${fetched}/${results.length} 个附件(消息 ${shownMessageId})`
|
|
@@ -826,18 +785,35 @@ export class AttachmentFetchToolRuntime {
|
|
|
826
785
|
}
|
|
827
786
|
}
|
|
828
787
|
|
|
829
|
-
/** Whitelist first, then the
|
|
830
|
-
private resolveKnownMessage(
|
|
788
|
+
/** Whitelist first, then the owner read-back. */
|
|
789
|
+
private async resolveKnownMessage(
|
|
831
790
|
call: FetchCallContext,
|
|
832
791
|
messageId: string,
|
|
833
|
-
): KnownAttachmentMessage | undefined {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
792
|
+
): Promise<KnownAttachmentMessage | undefined> {
|
|
793
|
+
return this.whitelist.lookup(call.route, messageId) ?? await this.readOwnedMessage(call, messageId)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Authorize a whitelist miss by reading the message back from Feishu: the
|
|
798
|
+
* route owner's own, undeleted message that belongs to the route
|
|
799
|
+
* (`messageBelongsToRoute`) is authorized. Every read failure fails closed.
|
|
800
|
+
*/
|
|
801
|
+
private async readOwnedMessage(
|
|
802
|
+
call: FetchCallContext,
|
|
803
|
+
messageId: string,
|
|
804
|
+
): Promise<KnownAttachmentMessage | undefined> {
|
|
805
|
+
const channel = call.channel
|
|
806
|
+
if (channel === undefined) return undefined
|
|
807
|
+
// The id comes from model arguments: never send a malformed one to Feishu.
|
|
808
|
+
if (!MESSAGE_ID_PATTERN.test(messageId)) return undefined
|
|
809
|
+
const reader = new MessageReader({ channel: () => channel })
|
|
810
|
+
const read = await reader.readMessage(messageId, { signal: call.signal })
|
|
811
|
+
if (!read.ok) return undefined
|
|
812
|
+
const container = quoteItemsOf(read.value)
|
|
813
|
+
if (container === undefined || container.deleted) return undefined
|
|
814
|
+
if (container.senderUserId !== call.route.senderUserId) return undefined
|
|
815
|
+
if (!messageBelongsToRoute(call.route, container)) return undefined
|
|
816
|
+
return knownMessageOfItems(read.value)
|
|
841
817
|
}
|
|
842
818
|
|
|
843
819
|
/** Record the resolved message as known for later turns of this route. */
|
|
@@ -906,8 +882,8 @@ export class AttachmentFetchToolRuntime {
|
|
|
906
882
|
message: KnownAttachmentMessage,
|
|
907
883
|
resource: KnownAttachmentResource,
|
|
908
884
|
): Promise<AttachmentFetchResultItem> {
|
|
909
|
-
// `containerMessageId` is part of a trusted registration
|
|
910
|
-
//
|
|
885
|
+
// `containerMessageId` is part of a trusted whitelist registration, never
|
|
886
|
+
// model input: the P0 rule makes
|
|
911
887
|
// merged-forward child bytes downloadable only through the top-level
|
|
912
888
|
// container id, so child ids are never used as download ids here.
|
|
913
889
|
const downloadId = resource.containerMessageId ?? message.messageId
|
|
@@ -1505,28 +1481,12 @@ function isPostBody(record: Record<string, unknown>): boolean {
|
|
|
1505
1481
|
return Array.isArray(record.content) || Array.isArray(record.files)
|
|
1506
1482
|
}
|
|
1507
1483
|
|
|
1508
|
-
/** Pending registry record → known message (authorization snapshot). */
|
|
1509
|
-
function knownMessageOfPending(record: PendingAttachmentRecord): KnownAttachmentMessage {
|
|
1510
|
-
return {
|
|
1511
|
-
messageId: record.messageId,
|
|
1512
|
-
kind: record.kind === 'merge_forward' ? 'merge_forward' : 'message',
|
|
1513
|
-
senderOpenId: record.senderOpenId,
|
|
1514
|
-
resources: record.resources.map((resource: PendingAttachmentResource): KnownAttachmentResource => ({
|
|
1515
|
-
type: resource.type,
|
|
1516
|
-
fileKey: resource.fileKey,
|
|
1517
|
-
...(resource.fileName === undefined ? {} : { fileName: resource.fileName }),
|
|
1518
|
-
...(resource.folder === true ? { folder: true } : {}),
|
|
1519
|
-
...(resource.containerMessageId === undefined ? {} : { containerMessageId: resource.containerMessageId }),
|
|
1520
|
-
})),
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
1484
|
/**
|
|
1525
1485
|
* Wait for one cordis injection to settle, but never longer than `timeoutMs`:
|
|
1526
1486
|
* a hard dependency on a service a deployment does not mount would otherwise
|
|
1527
1487
|
* block agent creation forever.
|
|
1528
1488
|
*/
|
|
1529
|
-
function waitForAvailability(
|
|
1489
|
+
export function waitForAvailability(
|
|
1530
1490
|
task: PromiseLike<unknown>,
|
|
1531
1491
|
timeoutMs: number,
|
|
1532
1492
|
): Promise<'ready' | 'failed' | 'timeout'> {
|
|
@@ -1664,7 +1624,7 @@ const TOPIC_MESSAGES_FETCH_UNAVAILABLE_DESCRIPTION =
|
|
|
1664
1624
|
* Chinese error rather than a schema message.
|
|
1665
1625
|
*/
|
|
1666
1626
|
export const topicMessagesArgsSchema = z.object({
|
|
1667
|
-
message_id: z.string().min(1).max(160).regex(
|
|
1627
|
+
message_id: z.string().min(1).max(160).regex(MESSAGE_ID_PATTERN).optional(),
|
|
1668
1628
|
limit: z.number().int().min(1).max(TOPIC_MESSAGES_MAX_LIMIT).optional(),
|
|
1669
1629
|
cursor: z.string().min(1).max(2048).optional(),
|
|
1670
1630
|
}).strict()
|
|
@@ -1722,7 +1682,7 @@ const TOPIC_MESSAGES_FAILURE_REASONS: Readonly<Record<TopicMessagesFailureCode,
|
|
|
1722
1682
|
TOOL_DISABLED: '话题消息读取工具已禁用',
|
|
1723
1683
|
INVALID_ARGUMENT: '读取消息的参数不合法',
|
|
1724
1684
|
MESSAGE_OUT_OF_ROUTE:
|
|
1725
|
-
'
|
|
1685
|
+
'该消息不在当前会话的飞书话题/聊天范围内(仅可读取当前话题的消息或本会话已知的消息)',
|
|
1726
1686
|
MESSAGE_READ_FAILED: '读取飞书消息失败',
|
|
1727
1687
|
PERMISSION_DENIED: '飞书应用缺少读取消息的权限(如 im:message:readonly),无法读取引用或话题消息',
|
|
1728
1688
|
ABORTED: '读取飞书消息已取消',
|
|
@@ -1838,7 +1798,7 @@ export interface TopicMessagesRuntimeDeps {
|
|
|
1838
1798
|
message: KnownAttachmentMessageInput,
|
|
1839
1799
|
source: KnownAttachmentSource,
|
|
1840
1800
|
): void
|
|
1841
|
-
/** Whether one message id is already
|
|
1801
|
+
/** Whether one message id is already in the route's whitelist. */
|
|
1842
1802
|
isKnownMessage(route: AttachmentFetchRoute, messageId: string): boolean
|
|
1843
1803
|
/**
|
|
1844
1804
|
* Whether `feishu_attachment_fetch` is usable in this deployment (native
|
|
@@ -2122,8 +2082,8 @@ export class TopicMessagesToolRuntime {
|
|
|
2122
2082
|
}
|
|
2123
2083
|
// Route membership is enforced on the container: another chat, another
|
|
2124
2084
|
// topic of the same group, or an unregistered main-timeline message is
|
|
2125
|
-
// refused. A message already admitted to this route (trigger/quote/
|
|
2126
|
-
// may be read even without a topic id (
|
|
2085
|
+
// refused. A message already admitted to this route (trigger/quote/recent)
|
|
2086
|
+
// may be read even without a topic id (it precedes the topic).
|
|
2127
2087
|
const known = this.deps.isKnownMessage(route, messageId)
|
|
2128
2088
|
if (!messageBelongsToRoute(route, container, known ? [messageId] : [])) {
|
|
2129
2089
|
return {
|