@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/settings/overlay.ts
CHANGED
|
@@ -20,10 +20,89 @@ import { configSchema } from '../config.ts'
|
|
|
20
20
|
import type { FeishuConfig } from '../config.ts'
|
|
21
21
|
import { normalizeWorkspaceBaseDir } from '../workspaces.ts'
|
|
22
22
|
import { EDITABLE_KEYS } from './contract.ts'
|
|
23
|
-
import type { ConfigValue, EditableKey } from './contract.ts'
|
|
23
|
+
import type { ConfigValue, EditableKey, EditableValues } from './contract.ts'
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/** Prefix of a nested panel key that addresses one `feishuTools` field. */
|
|
26
|
+
const FEISHU_TOOLS_PREFIX = 'feishuTools.'
|
|
27
|
+
|
|
28
|
+
/** Panel keys that address a nested `feishuTools` field (e.g. `feishuTools.topicMessages`). */
|
|
29
|
+
export type NestedEditableKey = Extract<EditableKey, `feishuTools.${string}`>
|
|
30
|
+
|
|
31
|
+
/** Panel keys that address a top-level config field. */
|
|
32
|
+
export type TopLevelEditableKey = Exclude<EditableKey, NestedEditableKey>
|
|
33
|
+
|
|
34
|
+
/** One partial `feishuTools` object inside the overlay layer. */
|
|
35
|
+
export type FeishuToolsOverlay = Partial<FeishuConfig['feishuTools']>
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The subset of `FeishuConfig` the Web panel may override. `feishuTools` stays
|
|
39
|
+
* partial: the overlay only records the sub-fields the operator actually
|
|
40
|
+
* changed, and the merge keeps every other value from the profile layer.
|
|
41
|
+
*/
|
|
42
|
+
export type OverlayPatch = Partial<Pick<FeishuConfig, TopLevelEditableKey>> & {
|
|
43
|
+
feishuTools?: FeishuToolsOverlay
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether one editable key addresses a nested `feishuTools` field. */
|
|
47
|
+
export function isNestedEditableKey(key: EditableKey): key is NestedEditableKey {
|
|
48
|
+
return key.startsWith(FEISHU_TOOLS_PREFIX)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `attachmentFetch` for the panel key `feishuTools.attachmentFetch`. */
|
|
52
|
+
function nestedFieldOf(key: NestedEditableKey): keyof FeishuConfig['feishuTools'] {
|
|
53
|
+
return key.slice(FEISHU_TOOLS_PREFIX.length) as keyof FeishuConfig['feishuTools']
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The `feishuTools` object of an untyped overlay/config record, when usable. */
|
|
57
|
+
function toolsRecordOf(container: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
58
|
+
const tools = container.feishuTools
|
|
59
|
+
if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) return undefined
|
|
60
|
+
return tools as Record<string, unknown>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Read one editable value out of an untyped overlay record (nested-aware). */
|
|
64
|
+
export function overlayValueOf(source: Record<string, unknown>, key: EditableKey): unknown {
|
|
65
|
+
if (!isNestedEditableKey(key)) return source[key]
|
|
66
|
+
return toolsRecordOf(source)?.[nestedFieldOf(key)]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Write one editable value into an untyped overlay record (nested-aware). */
|
|
70
|
+
export function setOverlayValue(target: Record<string, unknown>, key: EditableKey, value: unknown): void {
|
|
71
|
+
if (!isNestedEditableKey(key)) {
|
|
72
|
+
target[key] = value
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const tools = toolsRecordOf(target) ?? {}
|
|
76
|
+
tools[nestedFieldOf(key)] = value
|
|
77
|
+
target.feishuTools = tools
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read one editable value out of a resolved config (nested-aware). */
|
|
81
|
+
export function configValueOf(config: FeishuConfig, key: EditableKey): ConfigValue | undefined {
|
|
82
|
+
if (!isNestedEditableKey(key)) return config[key] as ConfigValue | undefined
|
|
83
|
+
return config.feishuTools[nestedFieldOf(key)] as ConfigValue | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Deep-merge one overlay patch over another (only `feishuTools` is nested). */
|
|
87
|
+
export function mergeOverlayPatch(base: OverlayPatch, addition: OverlayPatch): OverlayPatch {
|
|
88
|
+
const merged: OverlayPatch = { ...base, ...addition }
|
|
89
|
+
if (base.feishuTools !== undefined || addition.feishuTools !== undefined) {
|
|
90
|
+
merged.feishuTools = { ...base.feishuTools, ...addition.feishuTools }
|
|
91
|
+
}
|
|
92
|
+
return merged
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Remove one editable key from an overlay patch (nested-aware). */
|
|
96
|
+
export function deleteOverlayKey(patch: OverlayPatch, key: EditableKey): void {
|
|
97
|
+
if (!isNestedEditableKey(key)) {
|
|
98
|
+
delete patch[key as TopLevelEditableKey]
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const tools = patch.feishuTools
|
|
102
|
+
if (tools === undefined) return
|
|
103
|
+
delete tools[nestedFieldOf(key)]
|
|
104
|
+
if (Object.keys(tools).length === 0) delete patch.feishuTools
|
|
105
|
+
}
|
|
27
106
|
|
|
28
107
|
/** Fields whose empty string means "clear the override" (optional text config). */
|
|
29
108
|
const OPTIONAL_TEXT_KEYS = new Set<EditableKey>(['provider', 'model', 'reasoningEffort', 'agentPreset'])
|
|
@@ -43,24 +122,38 @@ export function parseOverlay(raw: unknown): { ok: true; patch: OverlayPatch } |
|
|
|
43
122
|
const issues: Array<{ path: string; message: string }> = []
|
|
44
123
|
const patch: Record<string, unknown> = {}
|
|
45
124
|
const shape = configSchema.shape as unknown as Record<string, z.ZodType>
|
|
125
|
+
const toolsShape = configSchema.shape.feishuTools
|
|
126
|
+
if (Object.hasOwn(source, 'feishuTools') && source.feishuTools !== undefined
|
|
127
|
+
&& toolsRecordOf(source) === undefined) {
|
|
128
|
+
issues.push({ path: 'feishuTools', message: '必须是 JSON 对象' })
|
|
129
|
+
}
|
|
46
130
|
for (const key of EDITABLE_KEYS) {
|
|
47
|
-
|
|
48
|
-
|
|
131
|
+
// Nested keys have no own property on the root object, so read through the
|
|
132
|
+
// shared accessor instead of `Object.hasOwn`.
|
|
133
|
+
const value = overlayValueOf(source, key)
|
|
49
134
|
if (value === undefined) continue
|
|
50
135
|
// `baseDir` needs location semantics the shared string schema cannot
|
|
51
136
|
// express (absolute only, `~` expansion, no filesystem root).
|
|
52
137
|
if (key === 'baseDir') {
|
|
53
138
|
const checked = normalizeWorkspaceBaseDir(value)
|
|
54
139
|
if (!checked.ok) issues.push({ path: key, message: checked.message })
|
|
55
|
-
else patch
|
|
140
|
+
else setOverlayValue(patch, key, checked.value)
|
|
56
141
|
continue
|
|
57
142
|
}
|
|
58
|
-
|
|
143
|
+
// Nested `feishuTools` values validate against the canonical sub-object
|
|
144
|
+
// schema; only the edited sub-field is copied out of the parsed result so
|
|
145
|
+
// schema defaults never leak into the overlay.
|
|
146
|
+
const parsed = isNestedEditableKey(key)
|
|
147
|
+
? toolsShape.safeParse({ [nestedFieldOf(key)]: value })
|
|
148
|
+
: shape[key]!.safeParse(value)
|
|
59
149
|
if (!parsed.success) {
|
|
60
150
|
issues.push({ path: key, message: parsed.error.issues[0]?.message ?? '非法值' })
|
|
61
151
|
continue
|
|
62
152
|
}
|
|
63
|
-
|
|
153
|
+
const normalizedValue = isNestedEditableKey(key)
|
|
154
|
+
? (parsed.data as FeishuConfig['feishuTools'])[nestedFieldOf(key)]
|
|
155
|
+
: parsed.data
|
|
156
|
+
setOverlayValue(patch, key, normalizedValue)
|
|
64
157
|
}
|
|
65
158
|
if (issues.length > 0) return { ok: false, issues }
|
|
66
159
|
return { ok: true, patch: normalizeOverlay(patch) }
|
|
@@ -86,26 +179,44 @@ export interface OverlayLogger {
|
|
|
86
179
|
export function normalizeOverlay(patch: Record<string, unknown>): OverlayPatch {
|
|
87
180
|
const normalized: Record<string, unknown> = {}
|
|
88
181
|
for (const key of EDITABLE_KEYS) {
|
|
89
|
-
const value = patch
|
|
182
|
+
const value = overlayValueOf(patch, key)
|
|
90
183
|
if (value === undefined) continue
|
|
91
184
|
if (OPTIONAL_TEXT_KEYS.has(key) && value === '') continue
|
|
92
|
-
normalized
|
|
185
|
+
setOverlayValue(normalized, key, value)
|
|
93
186
|
}
|
|
94
187
|
return normalized as OverlayPatch
|
|
95
188
|
}
|
|
96
189
|
|
|
97
190
|
/** Merge the Web overlay over the profile-provided base config. */
|
|
98
191
|
export function mergeOverlay(base: FeishuConfig, overlay: OverlayPatch): FeishuConfig {
|
|
99
|
-
|
|
192
|
+
const { feishuTools, ...topLevel } = overlay
|
|
193
|
+
const merged: FeishuConfig = { ...base, ...topLevel }
|
|
194
|
+
if (feishuTools !== undefined) merged.feishuTools = { ...base.feishuTools, ...feishuTools }
|
|
195
|
+
return merged
|
|
100
196
|
}
|
|
101
197
|
|
|
102
|
-
/** Project one config onto the editable-key subset (wire shape). */
|
|
198
|
+
/** Project one config onto the editable-key subset (wire shape, nested keys dotted). */
|
|
103
199
|
export function pickEditable(config: FeishuConfig): Record<EditableKey, ConfigValue | undefined> {
|
|
104
200
|
const picked: Record<string, ConfigValue | undefined> = {}
|
|
105
|
-
for (const key of EDITABLE_KEYS) picked[key] = config
|
|
201
|
+
for (const key of EDITABLE_KEYS) picked[key] = configValueOf(config, key)
|
|
106
202
|
return picked as Record<EditableKey, ConfigValue | undefined>
|
|
107
203
|
}
|
|
108
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Project an overlay patch onto flat editable keys (nested `feishuTools.*`
|
|
207
|
+
* values become dotted keys). The panel wire uses the same key space as
|
|
208
|
+
* `fields` / `overriddenKeys`, so the client never has to know the nested
|
|
209
|
+
* storage layout.
|
|
210
|
+
*/
|
|
211
|
+
export function overlayValues(patch: OverlayPatch): EditableValues {
|
|
212
|
+
const values: Record<string, ConfigValue> = {}
|
|
213
|
+
for (const key of EDITABLE_KEYS) {
|
|
214
|
+
const value = overlayValueOf(patch as Record<string, unknown>, key)
|
|
215
|
+
if (value !== undefined) values[key] = value as ConfigValue
|
|
216
|
+
}
|
|
217
|
+
return values as EditableValues
|
|
218
|
+
}
|
|
219
|
+
|
|
109
220
|
/**
|
|
110
221
|
* One JSON overlay file. Reads degrade to `{}` with a warning; writes are
|
|
111
222
|
* atomic and owner-readable only.
|
package/src/settings/routes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Web 设置面板的 HTTP 路由:status / panel / catalog / config(PUT,DELETE) /
|
|
3
|
-
* credentials(PUT,DELETE) / reconnect /
|
|
3
|
+
* credentials(PUT,DELETE) / reconnect / access。
|
|
4
4
|
*
|
|
5
5
|
* 路由注册进宿主可选的 `webServer` 服务(headless profile 没有它时面板自然
|
|
6
6
|
* 不存在,飞书通道不受影响)。写操作额外做同源校验 —— 本机非回环绑定下
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
ACCESS_ROUTE,
|
|
21
21
|
ACCESS_USERS_ROUTE,
|
|
22
22
|
CATALOG_ROUTE,
|
|
23
|
-
CLEANUP_ROUTE,
|
|
24
23
|
CONFIG_ROUTE,
|
|
25
24
|
CREDENTIALS_ROUTE,
|
|
26
25
|
PANEL_ROUTE,
|
|
@@ -310,19 +309,6 @@ export function registerSettingsRoutes(server: HostWebServer, surface: SettingsS
|
|
|
310
309
|
sendJson(res, result.ok ? 200 : (result.unconfigured === true ? 400 : 500), result)
|
|
311
310
|
}),
|
|
312
311
|
}),
|
|
313
|
-
server.register({
|
|
314
|
-
kind: 'exact',
|
|
315
|
-
path: CLEANUP_ROUTE,
|
|
316
|
-
handler: guarded(async (req, res) => {
|
|
317
|
-
if (!allowOnly(req, res, ['POST'])) return
|
|
318
|
-
if (!isSameOriginRequest(req)) {
|
|
319
|
-
forbiddenWrite(res)
|
|
320
|
-
return
|
|
321
|
-
}
|
|
322
|
-
const result = await surface.runCleanup()
|
|
323
|
-
sendJson(res, result.ok ? 200 : 500, result)
|
|
324
|
-
}),
|
|
325
|
-
}),
|
|
326
312
|
server.register({
|
|
327
313
|
kind: 'exact',
|
|
328
314
|
path: PICK_DIRECTORY_ROUTE,
|
|
@@ -404,8 +390,8 @@ export function registerSettingsRoutes(server: HostWebServer, surface: SettingsS
|
|
|
404
390
|
const payload = await readAccessBody(req, res)
|
|
405
391
|
if (payload === undefined) return
|
|
406
392
|
const result = methodOf(req) === 'DELETE'
|
|
407
|
-
? await surface.removeAccessUser(payload.
|
|
408
|
-
: await surface.saveAccessUser(payload.
|
|
393
|
+
? await surface.removeAccessUser(payload.userId)
|
|
394
|
+
: await surface.saveAccessUser(payload.userId, payload.name)
|
|
409
395
|
sendJson(res, result.ok ? 200 : 400, result)
|
|
410
396
|
}),
|
|
411
397
|
}),
|