@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.
Files changed (64) hide show
  1. package/README.md +123 -53
  2. package/docs/access-control-design.md +7 -5
  3. package/docs/attachment-pipeline-v2.md +310 -0
  4. package/docs/feasibility-and-design.md +1 -1
  5. package/docs/feishu-onboarding.md +127 -21
  6. package/docs/persistence.md +6 -4
  7. package/docs/refactor-plan.md +2 -2
  8. package/docs/workspace-isolation-design.md +13 -11
  9. package/lib/client.js +179 -288
  10. package/lib/client.js.map +1 -1
  11. package/lib/index.js +12125 -6382
  12. package/lib/index.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/access/cards.ts +9 -9
  15. package/src/access/controller.ts +10 -10
  16. package/src/access/index.ts +1 -0
  17. package/src/access/policy.ts +10 -10
  18. package/src/access/requests.ts +32 -31
  19. package/src/access/store.ts +12 -12
  20. package/src/access/targets.ts +56 -0
  21. package/src/cards/approval.ts +96 -26
  22. package/src/cards/index.ts +1 -0
  23. package/src/cards/onboarding.ts +1 -0
  24. package/src/cards/picker.ts +115 -0
  25. package/src/cards/streaming.ts +56 -7
  26. package/src/client/api.ts +5 -25
  27. package/src/client/panel/access-card.tsx +24 -15
  28. package/src/client/panel/access-tab.tsx +3 -3
  29. package/src/client/panel/connection-tab.tsx +70 -1
  30. package/src/client/panel/index.tsx +11 -62
  31. package/src/client/panel/shared.tsx +0 -23
  32. package/src/client/panel/workspace-tab.tsx +2 -18
  33. package/src/client/styles.ts +2 -0
  34. package/src/commands/registry.ts +2 -2
  35. package/src/config.ts +190 -32
  36. package/src/domain.ts +28 -18
  37. package/src/feishu/agent-context.ts +65 -19
  38. package/src/feishu/inbound-attachments.ts +440 -3
  39. package/src/feishu/manifest.ts +247 -0
  40. package/src/feishu/message-read.ts +1330 -0
  41. package/src/feishu/native-attachments.ts +998 -0
  42. package/src/feishu/scopes.ts +171 -0
  43. package/src/feishu/send-file.ts +543 -0
  44. package/src/feishu/slash-commands.ts +134 -0
  45. package/src/feishu/tools.ts +2305 -0
  46. package/src/feishu-channel.ts +322 -6
  47. package/src/host/agent.ts +55 -0
  48. package/src/host/attachments.ts +210 -0
  49. package/src/host/index.ts +1 -0
  50. package/src/index.ts +5 -4
  51. package/src/interactions/approvals.ts +187 -7
  52. package/src/interactions/pending.ts +3 -3
  53. package/src/routing/inbound.ts +620 -49
  54. package/src/service.ts +808 -375
  55. package/src/session/manager.ts +67 -13
  56. package/src/session/routes.ts +2 -3
  57. package/src/settings/contract.ts +90 -89
  58. package/src/settings/overlay.ts +124 -13
  59. package/src/settings/routes.ts +3 -17
  60. package/src/settings/surface.ts +156 -82
  61. package/src/streamer.ts +94 -5
  62. package/src/workspaces.ts +39 -26
  63. package/src/cleanup.ts +0 -535
  64. package/src/client/panel/cleanup-card.tsx +0 -106
@@ -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
- /** The subset of `FeishuConfig` the Web panel may override. */
26
- export type OverlayPatch = Partial<Pick<FeishuConfig, EditableKey>>
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
- if (!Object.hasOwn(source, key)) continue
48
- const value = source[key]
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[key] = checked.value
140
+ else setOverlayValue(patch, key, checked.value)
56
141
  continue
57
142
  }
58
- const parsed = shape[key]!.safeParse(value)
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
- patch[key] = parsed.data
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[key]
182
+ const value = overlayValueOf(patch, key)
90
183
  if (value === undefined) continue
91
184
  if (OPTIONAL_TEXT_KEYS.has(key) && value === '') continue
92
- normalized[key] = value
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
- return { ...base, ...overlay }
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[key] as ConfigValue | undefined
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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Web 设置面板的 HTTP 路由:status / panel / catalog / config(PUT,DELETE) /
3
- * credentials(PUT,DELETE) / reconnect / cleanup / access。
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.openId)
408
- : await surface.saveAccessUser(payload.openId, payload.name)
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
  }),