@modesto_ma/dsh-channels 0.3.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 +123 -53
- package/docs/access-control-design.md +7 -5
- package/docs/attachment-pipeline-v2.md +310 -0
- package/docs/feasibility-and-design.md +1 -1
- package/docs/feishu-onboarding.md +127 -21
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +2 -2
- package/docs/workspace-isolation-design.md +13 -11
- package/lib/client.js +179 -288
- package/lib/client.js.map +1 -1
- package/lib/index.js +12125 -6382
- 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 +190 -32
- package/src/domain.ts +28 -18
- package/src/feishu/agent-context.ts +65 -19
- package/src/feishu/inbound-attachments.ts +440 -3
- package/src/feishu/manifest.ts +247 -0
- package/src/feishu/message-read.ts +1330 -0
- package/src/feishu/native-attachments.ts +998 -0
- 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 +2305 -0
- package/src/feishu-channel.ts +322 -6
- package/src/host/agent.ts +55 -0
- package/src/host/attachments.ts +210 -0
- package/src/host/index.ts +1 -0
- 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 +620 -49
- package/src/service.ts +808 -375
- package/src/session/manager.ts +67 -13
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +90 -89
- package/src/settings/overlay.ts +124 -13
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +156 -82
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +39 -26
- package/src/cleanup.ts +0 -535
- package/src/client/panel/cleanup-card.tsx +0 -106
package/src/domain.ts
CHANGED
|
@@ -41,8 +41,8 @@ export const routeRecordSchema = z.object({
|
|
|
41
41
|
chatId: z.string().min(1),
|
|
42
42
|
/** `p2p` or `group`. */
|
|
43
43
|
chatType: z.enum(['p2p', 'group']),
|
|
44
|
-
/** Workspace owner (sender
|
|
45
|
-
|
|
44
|
+
/** Workspace owner (sender user_id). */
|
|
45
|
+
senderUserId: z.string().min(1),
|
|
46
46
|
/** Workspace root owning this route: the Session header cwd / sandbox root. */
|
|
47
47
|
workspacePath: z.string().min(1),
|
|
48
48
|
/**
|
|
@@ -81,15 +81,14 @@ export const expiredRouteRecordSchema = z.object({
|
|
|
81
81
|
/** Original chat type, retained for diagnostics and future policy. */
|
|
82
82
|
chatType: z.enum(['p2p', 'group']),
|
|
83
83
|
/** Original route owner; never used to grant access after expiry. */
|
|
84
|
-
|
|
84
|
+
senderUserId: z.string().min(1),
|
|
85
85
|
/** Epoch ms when the route was retired. */
|
|
86
86
|
expiredAt: z.number().int().nonnegative(),
|
|
87
87
|
/** Workspace the retired route belonged to; absent in tombstones written by
|
|
88
88
|
* older plugin versions, so the field must stay optional. */
|
|
89
89
|
workspacePath: z.string().min(1).optional(),
|
|
90
90
|
/** Effective working directory the retired route's Session used; absent in
|
|
91
|
-
* legacy tombstones
|
|
92
|
-
* directory, matching pre-v2 routes). */
|
|
91
|
+
* legacy tombstones, where `workspacePath` was the directory (pre-v2 routes). */
|
|
93
92
|
sessionDir: z.string().min(1).optional(),
|
|
94
93
|
/** Git branch of that directory when it was a worktree checkout. */
|
|
95
94
|
worktreeBranch: z.string().min(1).optional(),
|
|
@@ -202,7 +201,7 @@ export const accessGroupRecordSchema = z.object({
|
|
|
202
201
|
name: z.string().optional(),
|
|
203
202
|
/** Which authorization entry produced this grant. */
|
|
204
203
|
source: z.enum(['command', 'card', 'panel']),
|
|
205
|
-
/** Administrator
|
|
204
|
+
/** Administrator user_id that authorized it. */
|
|
206
205
|
addedBy: z.string().min(1),
|
|
207
206
|
/** Epoch ms when it was authorized. */
|
|
208
207
|
addedAt: z.number().int().nonnegative(),
|
|
@@ -211,18 +210,18 @@ export const accessGroupRecordSchema = z.object({
|
|
|
211
210
|
export type AccessGroupRecord = z.infer<typeof accessGroupRecordSchema>
|
|
212
211
|
|
|
213
212
|
/**
|
|
214
|
-
* One authorized user (`
|
|
213
|
+
* One authorized user (`user_id`), global across every authorized group.
|
|
215
214
|
* Private chats are administrator-only, so this grant only affects group
|
|
216
215
|
* conversations.
|
|
217
216
|
*/
|
|
218
217
|
export const accessUserRecordSchema = z.object({
|
|
219
|
-
/** Feishu
|
|
220
|
-
|
|
218
|
+
/** Feishu `user_id` — also the table key. */
|
|
219
|
+
userId: z.string().min(1),
|
|
221
220
|
/** Display name captured when the user was authorized (optional). */
|
|
222
221
|
name: z.string().optional(),
|
|
223
222
|
/** Which authorization entry produced this grant. */
|
|
224
223
|
source: z.enum(['command', 'card', 'panel']),
|
|
225
|
-
/** Administrator
|
|
224
|
+
/** Administrator user_id that authorized it. */
|
|
226
225
|
addedBy: z.string().min(1),
|
|
227
226
|
/** Epoch ms when it was authorized. */
|
|
228
227
|
addedAt: z.number().int().nonnegative(),
|
|
@@ -241,14 +240,14 @@ export const accessRequestRecordSchema = z.object({
|
|
|
241
240
|
token: z.string().min(1),
|
|
242
241
|
/** What is being requested. */
|
|
243
242
|
kind: z.enum(['group', 'user']),
|
|
244
|
-
/** `chat_id` for a group request; `
|
|
243
|
+
/** `chat_id` for a group request; `user_id` for a user request. */
|
|
245
244
|
targetId: z.string().min(1),
|
|
246
245
|
/** Chat the request originated from, when applicable. */
|
|
247
246
|
chatId: z.string().optional(),
|
|
248
247
|
/** Group name captured for the card (group requests, or the group the user was seen in). */
|
|
249
248
|
groupName: z.string().optional(),
|
|
250
249
|
/** User who triggered a user request. */
|
|
251
|
-
|
|
250
|
+
requesterUserId: z.string().optional(),
|
|
252
251
|
/** Display name of that user, when the event carried one. */
|
|
253
252
|
requesterName: z.string().optional(),
|
|
254
253
|
/** Lifecycle state; only `pending` requests accept a card action. */
|
|
@@ -283,7 +282,7 @@ export const processedRecordSchema = z.object({
|
|
|
283
282
|
export type ProcessedRecord = z.infer<typeof processedRecordSchema>
|
|
284
283
|
|
|
285
284
|
export interface FeishuDomainSpecShape {
|
|
286
|
-
name: '
|
|
285
|
+
name: 'channels_routes_v2'
|
|
287
286
|
version: 1
|
|
288
287
|
global: {
|
|
289
288
|
schema: z.ZodType<FeishuDomainGlobal>
|
|
@@ -338,7 +337,9 @@ export type FeishuDomainGlobal = z.infer<typeof feishuDomainGlobalSchema>
|
|
|
338
337
|
* runtime import of `@deepseek-ai/dsh-storage-domain`.
|
|
339
338
|
*/
|
|
340
339
|
export const feishuDomain: FeishuDomainSpecShape = {
|
|
341
|
-
|
|
340
|
+
// v2 keys every user by `user_id`. The open_id-keyed `channels_routes` unit
|
|
341
|
+
// is neither read nor migrated; it stays on disk untouched.
|
|
342
|
+
name: 'channels_routes_v2',
|
|
342
343
|
version: 1,
|
|
343
344
|
global: {
|
|
344
345
|
schema: feishuDomainGlobalSchema,
|
|
@@ -402,7 +403,7 @@ export interface AccessGroupsTable {
|
|
|
402
403
|
entries(): IterableIterator<[string, AccessGroupRecord]>
|
|
403
404
|
}
|
|
404
405
|
|
|
405
|
-
/** Normalized table handle for authorized users (key = `
|
|
406
|
+
/** Normalized table handle for authorized users (key = `user_id`). */
|
|
406
407
|
export interface AccessUsersTable {
|
|
407
408
|
get(key: string): AccessUserRecord | undefined
|
|
408
409
|
put(key: string, value: AccessUserRecord): Promise<unknown> | void
|
|
@@ -451,9 +452,18 @@ export interface RawDomainHandle {
|
|
|
451
452
|
close(): Promise<void>
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Feishu tenant `user_id` (letters, digits, `_`, `-`). It doubles as a
|
|
457
|
+
* workspace path segment and as a DM target, where the SDK infers the
|
|
458
|
+
* receive id type from the prefix: `ou_`/`on_`/`oc_` would be sent as an
|
|
459
|
+
* open_id/union_id/chat_id, so those prefixes are rejected (a pasted open_id
|
|
460
|
+
* fails validation instead of matching nobody).
|
|
461
|
+
*/
|
|
462
|
+
export const FEISHU_USER_ID_PATTERN = /^(?!ou_|on_|oc_)[A-Za-z0-9_-]+$/u
|
|
463
|
+
|
|
454
464
|
/** Build a chat-scoped fallback key for degraded (topic-less) routing. */
|
|
455
|
-
export function chatRouteKey(chatType: 'p2p' | 'group', chatId: string,
|
|
465
|
+
export function chatRouteKey(chatType: 'p2p' | 'group', chatId: string, senderUserId: string): string {
|
|
456
466
|
return chatType === 'p2p'
|
|
457
|
-
? `chat:p2p:${chatId}:${
|
|
458
|
-
: `chat:group:${chatId}:${
|
|
467
|
+
? `chat:p2p:${chatId}:${senderUserId}`
|
|
468
|
+
: `chat:group:${chatId}:${senderUserId}`
|
|
459
469
|
}
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-scoped system-prompt guidance for Feishu attachments.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Which guidance is mounted follows the *effective* attachment backend
|
|
5
|
+
* (`attachmentStore`, decisions D6/D2):
|
|
6
|
+
* - `native` (the default): inbound images/files live in the DSH native
|
|
7
|
+
* attachment library. Trigger-message images are prefetched and already
|
|
8
|
+
* visible; files stay unloaded and the prompt carries a stable
|
|
9
|
+
* `<feishu_context>` manifest (see `manifest.ts`) that the Agent uses with the
|
|
10
|
+
* `feishu_attachment_fetch` tool on demand.
|
|
11
|
+
* - `archive` (legacy / fallback): inbound images/files are archived below
|
|
12
|
+
* `<session working directory>/attachments/` and read with filesystem tools.
|
|
13
|
+
* Kept for explicit `attachmentStore: 'archive'` deployments and for native
|
|
14
|
+
* deployments whose host does not mount `ctx.attachments`.
|
|
9
15
|
*
|
|
10
|
-
* Registered inside the Agent scope (through `inject(['systemPrompt'])`), so
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (see `SessionManager.mountGuidance`), so
|
|
14
|
-
* when that directory is a Git worktree created after Agent
|
|
16
|
+
* Registered inside the Agent scope (through `inject(['systemPrompt'])`), so it
|
|
17
|
+
* is disposed with the Agent and does not leak into other compositions. The
|
|
18
|
+
* archive flavor is mounted only after the Session's effective working
|
|
19
|
+
* directory is known (see `SessionManager.mountGuidance`), so its absolute path
|
|
20
|
+
* is accurate even when that directory is a Git worktree created after Agent
|
|
21
|
+
* creation.
|
|
15
22
|
*
|
|
16
23
|
* @module dsh-channels/feishu/agent-context
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
import type { AgentContext } from '../host/agent.ts'
|
|
27
|
+
import { FEISHU_CONTEXT_TAG } from './manifest.ts'
|
|
20
28
|
|
|
21
29
|
/** Stable prompt-section name; one registration per Agent scope. */
|
|
22
30
|
export const FEISHU_ATTACHMENTS_SECTION = 'dsh-channels:feishu-attachments'
|
|
@@ -28,24 +36,61 @@ export const FEISHU_ATTACHMENTS_SECTION = 'dsh-channels:feishu-attachments'
|
|
|
28
36
|
*/
|
|
29
37
|
const FEISHU_ATTACHMENTS_ORDER = 950
|
|
30
38
|
|
|
31
|
-
/**
|
|
32
|
-
export
|
|
39
|
+
/** Attachment backend whose guidance to mount; mirrors `attachmentStore`. */
|
|
40
|
+
export type AttachmentGuidanceMode = 'native' | 'archive'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Native-library guidance (new default). Model-facing text kept stable so
|
|
44
|
+
* unchanged sessions keep their KV cache.
|
|
45
|
+
*/
|
|
46
|
+
export const FEISHU_ATTACHMENTS_NATIVE_GUIDANCE = `## Feishu attachments
|
|
47
|
+
Inbound Feishu images and files are stored in the DSH native attachment library and are tied to this conversation's session; they are not copied into the working directory automatically.
|
|
48
|
+
|
|
49
|
+
- Images that arrived in the message which triggered this turn are already visible to you as image blocks. Never load them again.
|
|
50
|
+
- Files are not loaded by default: the conversation only carries their metadata, not their bytes.
|
|
51
|
+
- The user message may carry a manifest in a \`<${FEISHU_CONTEXT_TAG}>\` block. It lists attachments that are not loaded yet, each with its message_id, type, and, when known, file_key and file name. A line with type=merge_forward is a forwarded-chat container: use its message_id without file_key. An image that could not be prefetched is listed the same way as a file.
|
|
52
|
+
- The user message may also carry \`<feishu_quote>\` (the message this one replies to) and, on a new session's first turn, \`<feishu_history>\` (bounded recent topic messages). Attachments named there are not loaded; fetch one with \`feishu_attachment_fetch\` using the message_id shown on its line.
|
|
53
|
+
- Use the \`feishu_topic_messages\` tool to read a quoted message in full, or to page through earlier messages of this topic (newest first, limit/cursor). Messages and attachments it returns are authorized for \`feishu_attachment_fetch\`.
|
|
54
|
+
|
|
55
|
+
When the user's request genuinely needs an attachment, fetch it with the \`feishu_attachment_fetch\` tool: pass message_id, and add file_key to fetch one specific file of that message. Fetch nothing when the request can be answered without the bytes, and do not proactively enumerate or fetch attachments the user did not mention.
|
|
56
|
+
|
|
57
|
+
Never read, copy, or print Feishu app credentials (for example files under the DSH home directory) and never call the Feishu OpenAPI directly with them; use only the Feishu tools provided in this conversation.
|
|
58
|
+
|
|
59
|
+
A fetched file is an immutable, read-only copy in the native attachment library. Before changing it, copy it into the working directory and modify the copy. Only access attachments of this conversation; never read or mention another conversation's attachments.`
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Legacy archive guidance: attachments are archived under the conversation
|
|
63
|
+
* workspace's `attachments/` directory and read through filesystem tools.
|
|
64
|
+
* Model-facing text kept stable so unchanged sessions keep their KV cache.
|
|
65
|
+
*/
|
|
66
|
+
export const FEISHU_ATTACHMENTS_ARCHIVE_GUIDANCE = `## Feishu attachments
|
|
33
67
|
Every Feishu conversation (topic) has its own working directory inside its user's workspace, and every working directory has its own \`attachments/\` directory. Images, files, and forwarded chat records sent through Feishu are archived verbatim in this conversation's \`attachments/\` directory. Each file name starts with its receipt time (\`YYYYMMDD-HHmmss-\`), so newer attachments sort later and \`ls -lt <dir>\` lists the newest first.
|
|
34
68
|
|
|
35
69
|
Attachments are never inserted into the conversation automatically. Read them only when the user's request needs them, and do not read attachments the user did not mention:
|
|
36
70
|
- images: use \`read_image\` with the file path;
|
|
37
71
|
- text and code: use \`read\` when the session mounts that tool, otherwise \`bash\` (for example \`sed -n '1,200p' <file>\`);
|
|
38
72
|
- other formats (PDF, Office, archives, ...): inspect or convert them with \`bash\`.
|
|
39
|
-
If the user refers to "the file/image I just sent" without a name, list this conversation's attachments directory and pick the most recent matching attachment. Other Feishu conversations work in separate directories: never read outside this conversation's working directory, and never mention files from another conversation
|
|
73
|
+
If the user refers to "the file/image I just sent" without a name, list this conversation's attachments directory and pick the most recent matching attachment. Other Feishu conversations work in separate directories: never read outside this conversation's working directory, and never mention files from another conversation.
|
|
74
|
+
|
|
75
|
+
Never read, copy, or print Feishu app credentials (for example files under the DSH home directory) and never call the Feishu OpenAPI directly with them; use only the Feishu tools provided in this conversation.
|
|
76
|
+
|
|
77
|
+
A message may also carry \`<feishu_quote>\` / \`<feishu_history>\` text excerpts. Attachments named there arrive lazily and are not in this directory in this deployment; describe what the excerpt says instead of claiming to have read the file.`
|
|
40
78
|
|
|
41
79
|
/**
|
|
42
|
-
* Full guidance for one Session.
|
|
43
|
-
*
|
|
80
|
+
* Full guidance text for one Session.
|
|
81
|
+
*
|
|
82
|
+
* The native flavor ignores `attachmentsDir` (there is no per-conversation
|
|
83
|
+
* directory to name). The archive flavor appends the absolute per-session
|
|
84
|
+
* directory when known so the convention still resolves after the Agent changes
|
|
44
85
|
* its working directory with the `working_directory` tool.
|
|
45
86
|
*/
|
|
46
|
-
export function attachmentsGuidance(
|
|
47
|
-
|
|
48
|
-
|
|
87
|
+
export function attachmentsGuidance(
|
|
88
|
+
mode: AttachmentGuidanceMode = 'native',
|
|
89
|
+
attachmentsDir?: string,
|
|
90
|
+
): string {
|
|
91
|
+
if (mode !== 'archive') return FEISHU_ATTACHMENTS_NATIVE_GUIDANCE
|
|
92
|
+
if (attachmentsDir === undefined || attachmentsDir === '') return FEISHU_ATTACHMENTS_ARCHIVE_GUIDANCE
|
|
93
|
+
return `${FEISHU_ATTACHMENTS_ARCHIVE_GUIDANCE}\n\nThis conversation's attachment directory is \`${attachmentsDir.replace(/`/gu, "'")}\` (absolute).`
|
|
49
94
|
}
|
|
50
95
|
|
|
51
96
|
/**
|
|
@@ -54,13 +99,14 @@ export function attachmentsGuidance(attachmentsDir?: string): string {
|
|
|
54
99
|
*/
|
|
55
100
|
export async function mountChannelGuidance(
|
|
56
101
|
agentCtx: AgentContext,
|
|
102
|
+
mode: AttachmentGuidanceMode = 'native',
|
|
57
103
|
attachmentsDir?: string,
|
|
58
104
|
): Promise<void> {
|
|
59
105
|
await agentCtx.inject(['systemPrompt'], (ctx) => {
|
|
60
106
|
ctx.systemPrompt?.section({
|
|
61
107
|
name: FEISHU_ATTACHMENTS_SECTION,
|
|
62
108
|
order: FEISHU_ATTACHMENTS_ORDER,
|
|
63
|
-
text: attachmentsGuidance(attachmentsDir),
|
|
109
|
+
text: attachmentsGuidance(mode, attachmentsDir),
|
|
64
110
|
})
|
|
65
111
|
})
|
|
66
112
|
}
|