@modesto_ma/dsh-channels 0.3.0 → 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 +71 -19
- package/docs/attachment-pipeline-v2.md +318 -0
- package/docs/feasibility-and-design.md +1 -1
- package/docs/feishu-onboarding.md +108 -2
- package/docs/refactor-plan.md +2 -2
- package/docs/workspace-isolation-design.md +9 -6
- package/lib/client.js +10 -5
- package/lib/client.js.map +1 -1
- package/lib/index.js +10193 -4824
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cleanup.ts +13 -2
- package/src/config.ts +194 -5
- package/src/domain.ts +107 -0
- package/src/feishu/agent-context.ts +65 -19
- package/src/feishu/inbound-attachments.ts +440 -3
- package/src/feishu/manifest.ts +270 -0
- package/src/feishu/message-read.ts +1333 -0
- package/src/feishu/native-attachments.ts +998 -0
- package/src/feishu/pending-attachments.ts +562 -0
- package/src/feishu/tools.ts +2345 -0
- package/src/feishu-channel.ts +55 -0
- 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 +1 -1
- package/src/routing/inbound.ts +652 -48
- package/src/service.ts +417 -9
- package/src/session/manager.ts +66 -12
- package/src/settings/contract.ts +28 -10
- package/src/settings/overlay.ts +124 -13
- package/src/settings/surface.ts +137 -12
- package/src/workspaces.ts +25 -4
package/src/settings/surface.ts
CHANGED
|
@@ -47,8 +47,15 @@ import type {
|
|
|
47
47
|
SaveCredentialsResponse,
|
|
48
48
|
} from './contract.ts'
|
|
49
49
|
import {
|
|
50
|
+
configValueOf,
|
|
51
|
+
deleteOverlayKey,
|
|
52
|
+
mergeOverlay,
|
|
53
|
+
mergeOverlayPatch,
|
|
54
|
+
overlayValueOf,
|
|
55
|
+
overlayValues,
|
|
50
56
|
parseOverlay,
|
|
51
57
|
pickEditable,
|
|
58
|
+
setOverlayValue,
|
|
52
59
|
} from './overlay.ts'
|
|
53
60
|
import type { OverlayLogger, OverlayPatch, OverlayStore } from './overlay.ts'
|
|
54
61
|
import type { SettingsCatalog } from './catalog.ts'
|
|
@@ -79,7 +86,7 @@ export interface SettingsTarget {
|
|
|
79
86
|
configSnapshot(): FeishuConfig
|
|
80
87
|
baseConfigSnapshot(): FeishuConfig
|
|
81
88
|
runningConfigSnapshot(): FeishuConfig | undefined
|
|
82
|
-
applyOverlay(overlay:
|
|
89
|
+
applyOverlay(overlay: OverlayPatch): void
|
|
83
90
|
restart(): Promise<void>
|
|
84
91
|
credentialStates(): Promise<{ appId: CredentialInfo; appSecret: CredentialInfo }>
|
|
85
92
|
setCredential(field: CredentialField, value: string): Promise<void>
|
|
@@ -125,10 +132,17 @@ const BOOLEAN_KEYS = new Set<EditableKey>([
|
|
|
125
132
|
'welcomeOnboarding',
|
|
126
133
|
'allowPermissionSwitch',
|
|
127
134
|
'acceptAttachments',
|
|
135
|
+
'prefetchImages',
|
|
128
136
|
'workspaceRetentionAuto',
|
|
137
|
+
'feishuTools.attachmentFetch',
|
|
138
|
+
'feishuTools.topicMessages',
|
|
129
139
|
])
|
|
130
140
|
|
|
131
141
|
const SELECT_OPTIONS: Partial<Record<EditableKey, ReadonlyArray<{ value: string; label: string }>>> = {
|
|
142
|
+
attachmentStore: [
|
|
143
|
+
{ value: 'native', label: 'DSH 原生附件库(默认)' },
|
|
144
|
+
{ value: 'archive', label: '工作区 attachments/(旧版)' },
|
|
145
|
+
],
|
|
132
146
|
dmMode: [
|
|
133
147
|
{ value: 'open', label: '开放私聊' },
|
|
134
148
|
{ value: 'disabled', label: '禁用私聊' },
|
|
@@ -185,9 +199,104 @@ const FIELD_META: Record<EditableKey, { label: string; help: string; min?: numbe
|
|
|
185
199
|
max: 30,
|
|
186
200
|
step: 1,
|
|
187
201
|
},
|
|
188
|
-
acceptAttachments: {
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
acceptAttachments: {
|
|
203
|
+
label: '接收入站附件',
|
|
204
|
+
help: '接收飞书图片、文件与合并转发记录。native(默认)下入站附件存入 DSH 原生附件库:触发消息的图片预取进首轮、文件保持懒取,纯附件消息静默登记元数据;archive 下按旧版行为下载到工作区 attachments/。关闭后附件消息不保存、不触发模型、也不登记。',
|
|
205
|
+
},
|
|
206
|
+
attachmentStore: {
|
|
207
|
+
label: '入站附件存储后端',
|
|
208
|
+
help: 'native(默认):图片与文件存入 DSH 原生附件库(不可变、内容寻址去重、读取校验、随 Session 回放;不自动清理)。archive:旧版下载到话题工作区 attachments/(可清理但会阻塞会话)。宿主未挂载附件服务时 native 自动降级 archive。',
|
|
209
|
+
},
|
|
210
|
+
prefetchImages: {
|
|
211
|
+
label: '预取触发消息的图片',
|
|
212
|
+
help: 'native 下把触发消息的图片预取为首轮 ImageBlock,会话/话题/卡片不等下载;失败、超限或超时的图片降级为清单中的懒取项。关闭后图片也需用 feishu_attachment_fetch 获取。',
|
|
213
|
+
},
|
|
214
|
+
prefetchTimeoutMs: {
|
|
215
|
+
label: '图片预取整段超时(ms)',
|
|
216
|
+
help: '一次触发消息图片预取的总超时(默认 10000 ms,100–120000);到期后已取回的图片保留,其余降级为清单,不阻塞回合。',
|
|
217
|
+
min: 100,
|
|
218
|
+
max: 120_000,
|
|
219
|
+
step: 100,
|
|
220
|
+
},
|
|
221
|
+
prefetchImageMaxBytes: {
|
|
222
|
+
label: '单张预取图片上限(字节)',
|
|
223
|
+
help: '单张图片超过该字节数即不入首轮图片、留在清单懒取(默认 20971520 = 20 MiB)。',
|
|
224
|
+
min: 1,
|
|
225
|
+
max: 200 * 1024 * 1024,
|
|
226
|
+
step: 1,
|
|
227
|
+
},
|
|
228
|
+
prefetchTotalMaxBytes: {
|
|
229
|
+
label: '单次预取总量上限(字节)',
|
|
230
|
+
help: '一次触发消息图片预取的总字节预算(默认 52428800 = 50 MiB);用尽后剩余图片降级为清单懒取。',
|
|
231
|
+
min: 1,
|
|
232
|
+
max: 1024 * 1024 * 1024,
|
|
233
|
+
step: 1,
|
|
234
|
+
},
|
|
235
|
+
historyBootstrapLimit: {
|
|
236
|
+
label: '首轮话题历史条数',
|
|
237
|
+
help: '新 Session 首轮自动附带本话题最近 N 条消息摘要(0 = 关闭,上限 20);被动读取使用短预算,缺权限/失败时静默省略。更早的消息用 feishu_topic_messages 工具翻页。',
|
|
238
|
+
min: 0,
|
|
239
|
+
max: 20,
|
|
240
|
+
step: 1,
|
|
241
|
+
},
|
|
242
|
+
pendingAttachmentTtlMs: {
|
|
243
|
+
label: '静默附件登记有效期(ms)',
|
|
244
|
+
help: '纯附件/未触发消息只登记元数据(不下载)。超过该时长的登记由周期 GC 清除;0 = 不按时间清理(每个 scope 仍有容量上限)。默认 604800000 = 7 天。',
|
|
245
|
+
min: 0,
|
|
246
|
+
step: 1,
|
|
247
|
+
},
|
|
248
|
+
pendingAttachmentLimit: {
|
|
249
|
+
label: '静默附件登记容量(每个 scope)',
|
|
250
|
+
help: '每个 (会话, 话题, 发送者) scope 最多保留的登记条数(1–100);超出时淘汰该 scope 最旧的一条。',
|
|
251
|
+
min: 1,
|
|
252
|
+
max: 100,
|
|
253
|
+
step: 1,
|
|
254
|
+
},
|
|
255
|
+
promptContextTimeoutMs: {
|
|
256
|
+
label: '被动读取预算(ms)',
|
|
257
|
+
help: '引用摘录与首轮话题历史这类被动 prompt 增强的整段预算(默认 3000 ms,100–10000);超时即省略对应块、回合照跑。模型主动调用工具的读取超时另由 feishu_tools 超时控制。',
|
|
258
|
+
min: 100,
|
|
259
|
+
max: 10_000,
|
|
260
|
+
step: 100,
|
|
261
|
+
},
|
|
262
|
+
'feishuTools.attachmentFetch': {
|
|
263
|
+
label: '启用 feishu_attachment_fetch 工具',
|
|
264
|
+
help: '让模型按 message_id/file_key 懒取已授权消息的图片/文件。关闭后文件无法懒取,插件整体回退 archive 后端(入站文件直接下载到工作区)。对新会话生效。',
|
|
265
|
+
},
|
|
266
|
+
'feishuTools.attachmentFetchMaxResources': {
|
|
267
|
+
label: '单次取回资源数上限',
|
|
268
|
+
help: '一次工具调用最多取回的附件数量(默认 10,1–50);超出会返回可读原因,模型可按 file_key 拆分重试。',
|
|
269
|
+
min: 1,
|
|
270
|
+
max: 50,
|
|
271
|
+
step: 1,
|
|
272
|
+
},
|
|
273
|
+
'feishuTools.attachmentFetchMaxBytes': {
|
|
274
|
+
label: '单次取回字节预算(字节)',
|
|
275
|
+
help: '一次工具调用的总下载字节预算(默认 104857600 = 100 MiB);文件流式写入原生库,不整文件缓冲。',
|
|
276
|
+
min: 1,
|
|
277
|
+
max: 1024 * 1024 * 1024,
|
|
278
|
+
step: 1,
|
|
279
|
+
},
|
|
280
|
+
'feishuTools.attachmentFetchTimeoutMs': {
|
|
281
|
+
label: '单次取回超时(ms)',
|
|
282
|
+
help: '一次 feishu_attachment_fetch 的整段超时(默认 120000 ms,1000–600000);到期取消下载并逐项报告可读原因。',
|
|
283
|
+
min: 1_000,
|
|
284
|
+
max: 600_000,
|
|
285
|
+
step: 1_000,
|
|
286
|
+
},
|
|
287
|
+
'feishuTools.topicMessages': {
|
|
288
|
+
label: '启用 feishu_topic_messages 工具',
|
|
289
|
+
help: '让模型按需读取引用消息与话题内更早的消息/附件。关闭只影响模型工具;被动的引用摘录与首轮历史仍由 historyBootstrapLimit 控制。',
|
|
290
|
+
},
|
|
291
|
+
'feishuTools.topicMessagesTimeoutMs': {
|
|
292
|
+
label: '单次话题读取超时(ms)',
|
|
293
|
+
help: '一次 feishu_topic_messages 的整段超时(默认 20000 ms,1000–120000);被动摘录使用独立的短预算。',
|
|
294
|
+
min: 1_000,
|
|
295
|
+
max: 120_000,
|
|
296
|
+
step: 1_000,
|
|
297
|
+
},
|
|
298
|
+
workspaceRetention: { label: '过期目录保留期', help: '只清理工作区与 Agent 产物目录,不影响 Session 附件:默认 native 存储下附件在 DSH 原生附件库,不参与目录清理;仅当显式使用 archive 后端时,附件副本位于话题目录、会随目录清理。用户工作区本身永不删除,DSH 会话日志不受影响。' },
|
|
299
|
+
workspaceRetentionAuto: { label: '自动执行目录清理', help: '开启后插件在启动时及每天执行一次过期目录清理;关闭时只能在下方卡片手动清理。开启前请确认保留期已设置;清理只清理工作区与 Agent 产物目录,不影响 Session 附件。' },
|
|
191
300
|
provider: { label: '模型服务商', help: '留空 = 使用部署默认;例如 deepseek-official。', placeholder: '留空 = 部署默认' },
|
|
192
301
|
model: { label: '模型 id', help: '留空 = 使用部署默认;例如 deepseek-v4-flash。', placeholder: '留空 = 部署默认' },
|
|
193
302
|
reasoningEffort: { label: '推理档位', help: '模型声明的精确档位 id(如 high / max);留空 = 适配器默认。', placeholder: '留空 = 适配器默认' },
|
|
@@ -210,6 +319,21 @@ const GROUP_OF: Record<EditableKey, EditableFieldWire['group']> = {
|
|
|
210
319
|
accessMode: 'access',
|
|
211
320
|
authRequestTtlDays: 'access',
|
|
212
321
|
acceptAttachments: 'content',
|
|
322
|
+
attachmentStore: 'content',
|
|
323
|
+
prefetchImages: 'content',
|
|
324
|
+
prefetchTimeoutMs: 'content',
|
|
325
|
+
prefetchImageMaxBytes: 'content',
|
|
326
|
+
prefetchTotalMaxBytes: 'content',
|
|
327
|
+
historyBootstrapLimit: 'content',
|
|
328
|
+
pendingAttachmentTtlMs: 'content',
|
|
329
|
+
pendingAttachmentLimit: 'content',
|
|
330
|
+
promptContextTimeoutMs: 'tools',
|
|
331
|
+
'feishuTools.attachmentFetch': 'tools',
|
|
332
|
+
'feishuTools.attachmentFetchMaxResources': 'tools',
|
|
333
|
+
'feishuTools.attachmentFetchMaxBytes': 'tools',
|
|
334
|
+
'feishuTools.attachmentFetchTimeoutMs': 'tools',
|
|
335
|
+
'feishuTools.topicMessages': 'tools',
|
|
336
|
+
'feishuTools.topicMessagesTimeoutMs': 'tools',
|
|
213
337
|
provider: 'model',
|
|
214
338
|
model: 'model',
|
|
215
339
|
reasoningEffort: 'model',
|
|
@@ -536,9 +660,11 @@ export class SettingsSurface {
|
|
|
536
660
|
const effective = this.target.configSnapshot()
|
|
537
661
|
const overlay = await this.store.load()
|
|
538
662
|
const running = this.target.runningConfigSnapshot()
|
|
539
|
-
const overriddenKeys = EDITABLE_KEYS.filter(
|
|
663
|
+
const overriddenKeys = EDITABLE_KEYS.filter(
|
|
664
|
+
key => overlayValueOf(overlay as Record<string, unknown>, key) !== undefined,
|
|
665
|
+
)
|
|
540
666
|
const restartPendingKeys = this.target.isRunning() && running !== undefined
|
|
541
|
-
? RESTART_KEYS.filter(key => effective
|
|
667
|
+
? RESTART_KEYS.filter(key => configValueOf(effective, key) !== configValueOf(running, key))
|
|
542
668
|
: []
|
|
543
669
|
const catalog = await this.catalogFor(effective.provider, effective.model)
|
|
544
670
|
const fields = withDirectoryBrowse(
|
|
@@ -611,7 +737,7 @@ export class SettingsSurface {
|
|
|
611
737
|
config: {
|
|
612
738
|
base: pickEditable(base) as Record<string, ConfigValue | undefined>,
|
|
613
739
|
effective: pickEditable(effective) as Record<string, ConfigValue | undefined>,
|
|
614
|
-
overlay: overlay
|
|
740
|
+
overlay: overlayValues(overlay),
|
|
615
741
|
overriddenKeys,
|
|
616
742
|
restartPendingKeys,
|
|
617
743
|
fields,
|
|
@@ -641,7 +767,7 @@ export class SettingsSurface {
|
|
|
641
767
|
clearedKeys.push(key)
|
|
642
768
|
continue
|
|
643
769
|
}
|
|
644
|
-
raw
|
|
770
|
+
setOverlayValue(raw, key, value)
|
|
645
771
|
}
|
|
646
772
|
|
|
647
773
|
const parsed = parseOverlay(raw)
|
|
@@ -670,9 +796,8 @@ export class SettingsSurface {
|
|
|
670
796
|
|
|
671
797
|
let merged: OverlayPatch
|
|
672
798
|
try {
|
|
673
|
-
merged =
|
|
674
|
-
for (const key of clearedKeys)
|
|
675
|
-
Object.assign(merged, parsed.patch)
|
|
799
|
+
merged = mergeOverlayPatch(await this.store.load(), parsed.patch)
|
|
800
|
+
for (const key of clearedKeys) deleteOverlayKey(merged, key)
|
|
676
801
|
} catch (error) {
|
|
677
802
|
return { ok: false, error: `写入覆盖配置失败:${messageOf(error)}` }
|
|
678
803
|
}
|
|
@@ -681,7 +806,7 @@ export class SettingsSurface {
|
|
|
681
806
|
// provider (or lone model) leaves the session without a complete route,
|
|
682
807
|
// so an edit to either side must end with both set or both cleared.
|
|
683
808
|
if (values.provider !== undefined || values.model !== undefined) {
|
|
684
|
-
const prospective =
|
|
809
|
+
const prospective = mergeOverlay(this.target.baseConfigSnapshot(), merged)
|
|
685
810
|
const hasProvider = (prospective.provider ?? '') !== ''
|
|
686
811
|
const hasModel = (prospective.model ?? '') !== ''
|
|
687
812
|
if (hasProvider !== hasModel) {
|
package/src/workspaces.ts
CHANGED
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
* those directories are kept verbatim for legacy routes and are only removed
|
|
15
15
|
* by the retention cleanup, never migrated.
|
|
16
16
|
*
|
|
17
|
-
* Attachments received before a topic exists
|
|
18
|
-
* `<baseDir>/_staging/<chatType>/<chat_id>/<open_id>/` and
|
|
19
|
-
* topic directory once its first Session is created.
|
|
17
|
+
* Attachments received before a topic exists were staged by older plugin
|
|
18
|
+
* versions below `<baseDir>/_staging/<chatType>/<chat_id>/<open_id>/` and
|
|
19
|
+
* adopted into the topic directory once its first Session is created. The v2
|
|
20
|
+
* pipeline stores inbound attachments in the DSH native attachment library and
|
|
21
|
+
* never writes staging, so this is a one-release compatibility path only: it is
|
|
22
|
+
* scheduled for removal in the release cycle after attachment_pipeline_v2 ships
|
|
23
|
+
* (see Roadmap decisions D12 and docs/attachment-pipeline-v2.md).
|
|
20
24
|
*
|
|
21
25
|
* Feishu ids (`ou_…`, `oc_…`) are safe path segments, but they are validated
|
|
22
26
|
* anyway so a hostile id can never escape `baseDir`.
|
|
@@ -173,10 +177,27 @@ export function legacyTopicWorkspaceRoot(baseDir: string): string {
|
|
|
173
177
|
return path.join(baseDir, 'topics')
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Legacy pre-topic attachment staging root.
|
|
182
|
+
*
|
|
183
|
+
* Only older plugin versions wrote here; the v2 native pipeline never does and
|
|
184
|
+
* the retention cleanup never deletes it. Scheduled for removal together with
|
|
185
|
+
* the one-time adoption in the release cycle after attachment_pipeline_v2
|
|
186
|
+
* (decisions D12).
|
|
187
|
+
*
|
|
188
|
+
* @deprecated legacy archive-upload path, kept for one release cycle only.
|
|
189
|
+
*/
|
|
190
|
+
export function stagingRoot(baseDir: string): string {
|
|
191
|
+
return path.join(baseDir, '_staging')
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
/**
|
|
177
195
|
* Staging directory for attachments that arrive before their topic (and so
|
|
178
196
|
* its topic directory) exists. Not a registered workspace: files move into the
|
|
179
197
|
* real topic directory as soon as the first Session of that topic is created.
|
|
198
|
+
*
|
|
199
|
+
* @deprecated v2 never writes staging; kept for one-time adoption of legacy
|
|
200
|
+
* files for one release cycle (decisions D12).
|
|
180
201
|
*/
|
|
181
202
|
export function stagingPathFor(
|
|
182
203
|
baseDir: string,
|
|
@@ -186,7 +207,7 @@ export function stagingPathFor(
|
|
|
186
207
|
): string {
|
|
187
208
|
const chat = safeSegment(chatId, 'chatId')
|
|
188
209
|
const user = safeSegment(senderOpenId, 'senderOpenId')
|
|
189
|
-
return path.join(baseDir,
|
|
210
|
+
return path.join(stagingRoot(baseDir), chatType, chat, user)
|
|
190
211
|
}
|
|
191
212
|
|
|
192
213
|
/** Human-friendly workspace title shown in the DSH workspace list. */
|