@modesto_ma/dsh-channels 0.1.0 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +89 -55
  2. package/docs/access-control-design.md +5 -5
  3. package/docs/feasibility-and-design.md +54 -26
  4. package/docs/feishu-onboarding.md +26 -20
  5. package/docs/persistence.md +4 -4
  6. package/docs/refactor-plan.md +6 -5
  7. package/docs/workspace-isolation-design.md +78 -0
  8. package/lib/client.js +1715 -882
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +1556 -680
  11. package/lib/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/cards/index.ts +1 -0
  14. package/src/cards/onboarding.ts +23 -0
  15. package/src/cards/primitives.ts +20 -0
  16. package/src/cards/streaming.ts +310 -291
  17. package/src/cleanup.ts +256 -119
  18. package/src/client/api.ts +20 -0
  19. package/src/client/catalog.ts +1 -1
  20. package/src/client/index.ts +1 -1
  21. package/src/client/panel/access-card.tsx +294 -0
  22. package/src/client/panel/access-tab.tsx +46 -0
  23. package/src/client/panel/advanced-tab.tsx +80 -0
  24. package/src/client/panel/cleanup-card.tsx +106 -0
  25. package/src/client/panel/connection-tab.tsx +214 -0
  26. package/src/client/panel/credentials-card.tsx +104 -0
  27. package/src/client/panel/index.tsx +862 -0
  28. package/src/client/panel/model-tab.tsx +23 -0
  29. package/src/client/panel/sessions-tab.tsx +21 -0
  30. package/src/client/panel/shared.tsx +488 -0
  31. package/src/client/panel/workspace-tab.tsx +40 -0
  32. package/src/client/styles.ts +24 -6
  33. package/src/config.ts +44 -12
  34. package/src/domain.ts +34 -3
  35. package/src/feishu/agent-context.ts +9 -5
  36. package/src/index.ts +32 -14
  37. package/src/routing/inbound.ts +88 -43
  38. package/src/service.ts +259 -65
  39. package/src/session/isolation.ts +277 -0
  40. package/src/session/manager.ts +63 -20
  41. package/src/session/repair.ts +2 -3
  42. package/src/session/routes.ts +2 -0
  43. package/src/session-commands.ts +41 -14
  44. package/src/settings/contract.ts +223 -47
  45. package/src/settings/directory-picker.ts +121 -0
  46. package/src/settings/routes.ts +49 -2
  47. package/src/settings/surface.ts +171 -25
  48. package/src/streamer.ts +70 -30
  49. package/src/workspaces.ts +50 -28
  50. package/src/client/panel.tsx +0 -1475
package/src/workspaces.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  /**
2
- * Workspace directory resolution: one DSH workspace per (user × chat).
2
+ * Workspace directory resolution: one DSH workspace per user, one working
3
+ * directory per Feishu topic.
3
4
  *
4
- * Every Feishu topic gets its own workspace, grouped by the (chat, user) pair:
5
- * - p2p: `<baseDir>/p2p/<open_id>/<topic>/`
6
- * - group: `<baseDir>/groups/<chat_id>/<open_id>/<topic>/`
5
+ * - p2p: workspace `<baseDir>/p2p/<open_id>/`
6
+ * - group: workspace `<baseDir>/groups/<chat_id>/<open_id>/`
7
+ * - topic: `<workspace>/topics/<topic-token>/` (the Session's effective cwd)
8
+ *
9
+ * The workspace root is the Session header cwd: it keeps the Session grouped
10
+ * under the user's project in the GUI and stays the sandbox write boundary.
11
+ * The per-topic directory is where the tools actually work (see
12
+ * `./session/isolation.ts`), so files produced by different topics never mix.
13
+ * Older versions registered one workspace per topic under `<baseDir>/topics/`;
14
+ * those directories are kept verbatim for legacy routes and are only removed
15
+ * by the retention cleanup, never migrated.
7
16
  *
8
17
  * Attachments received before a topic exists are staged below
9
18
  * `<baseDir>/_staging/<chatType>/<chat_id>/<open_id>/` and adopted into the
10
- * topic workspace once its first Session is created.
19
+ * topic directory once its first Session is created.
11
20
  *
12
21
  * Feishu ids (`ou_…`, `oc_…`) are safe path segments, but they are validated
13
22
  * anyway so a hostile id can never escape `baseDir`.
@@ -123,36 +132,51 @@ export function topicTokenFor(
123
132
  return kind === 'thread' ? safeSegment(routeKey, 'route key') : safeSegment(sessionId, 'session id')
124
133
  }
125
134
 
135
+ /** Git branch (and worktree directory) name for one topic token. */
136
+ export function worktreeBranchFor(topicToken: string): string {
137
+ return `feishu-${safeSegment(topicToken, 'topic token')}`
138
+ }
139
+
126
140
  /**
127
- * Deterministic workspace directory for one Feishu topic: every route gets its
128
- * own workspace below its (user × chat) grouping directory.
141
+ * Deterministic workspace directory for one user in one chat context: the
142
+ * durable project the user's Sessions are grouped under.
129
143
  *
130
- * - p2p: `<baseDir>/topics/p2p/<open_id>/<topic>/`
131
- * - group: `<baseDir>/topics/groups/<chat_id>/<open_id>/<topic>/`
144
+ * - p2p: `<baseDir>/p2p/<open_id>/`
145
+ * - group: `<baseDir>/groups/<chat_id>/<open_id>/`
132
146
  *
133
- * A separate `topics/` root keeps new workspaces out of the legacy (user ×
134
- * chat) workspace directories left by older versions, so cleaning one can
135
- * never delete the other.
147
+ * These are the same paths older versions used as their shared (user × chat)
148
+ * workspaces, so an upgraded deployment reuses the existing registry record
149
+ * instead of creating a second project for the same user.
136
150
  */
137
151
  export function workspacePathFor(
138
152
  baseDir: string,
139
153
  chatType: 'p2p' | 'group',
140
154
  chatId: string,
141
155
  senderOpenId: string,
142
- topicToken: string,
143
156
  ): string {
144
- const chat = safeSegment(chatId, 'chatId')
145
157
  const user = safeSegment(senderOpenId, 'senderOpenId')
146
- const topic = safeSegment(topicToken, 'topic token')
147
- return chatType === 'p2p'
148
- ? path.join(baseDir, 'topics', 'p2p', user, topic)
149
- : path.join(baseDir, 'topics', 'groups', chat, user, topic)
158
+ if (chatType === 'p2p') return path.join(baseDir, 'p2p', user)
159
+ return path.join(baseDir, 'groups', safeSegment(chatId, 'chatId'), user)
160
+ }
161
+
162
+ /** Per-topic working directory inside one user workspace. */
163
+ export function topicDirFor(workspacePath: string, topicToken: string): string {
164
+ return path.join(workspacePath, 'topics', safeSegment(topicToken, 'topic token'))
165
+ }
166
+
167
+ /**
168
+ * Root of the legacy (one workspace per topic) directories. Legacy workspaces
169
+ * live strictly below this path, which is how the retention cleanup tells them
170
+ * apart from user workspaces that must never be deleted.
171
+ */
172
+ export function legacyTopicWorkspaceRoot(baseDir: string): string {
173
+ return path.join(baseDir, 'topics')
150
174
  }
151
175
 
152
176
  /**
153
177
  * Staging directory for attachments that arrive before their topic (and so
154
- * its workspace) exists. Not a registered workspace: files move into the real
155
- * workspace as soon as the first Session of that topic is created.
178
+ * its topic directory) exists. Not a registered workspace: files move into the
179
+ * real topic directory as soon as the first Session of that topic is created.
156
180
  */
157
181
  export function stagingPathFor(
158
182
  baseDir: string,
@@ -170,17 +194,16 @@ export function workspaceTitleFor(
170
194
  chatType: 'p2p' | 'group',
171
195
  chatId: string,
172
196
  senderOpenId: string,
173
- topicToken?: string,
174
197
  ): string {
175
- const base = chatType === 'p2p'
198
+ return chatType === 'p2p'
176
199
  ? `飞书私聊 · ${senderOpenId}`
177
200
  : `飞书群聊 · ${chatId} · ${senderOpenId}`
178
- return topicToken === undefined ? base : `${base} · ${topicToken}`
179
201
  }
180
202
 
181
203
  /**
182
- * Ensure one topic's workspace directory exists and is registered with the
183
- * host's workspace registry. Idempotent: repeated calls return the same record.
204
+ * Ensure one user's workspace directory exists and is registered with the
205
+ * host's workspace registry. Idempotent: repeated calls return the same record,
206
+ * and an existing record keeps its (possibly renamed) title.
184
207
  */
185
208
  export async function ensureWorkspace(
186
209
  ctx: HostContext,
@@ -188,12 +211,11 @@ export async function ensureWorkspace(
188
211
  chatType: 'p2p' | 'group',
189
212
  chatId: string,
190
213
  senderOpenId: string,
191
- topicToken: string,
192
214
  ): Promise<Workspace> {
193
- const dir = workspacePathFor(baseDir, chatType, chatId, senderOpenId, topicToken)
215
+ const dir = workspacePathFor(baseDir, chatType, chatId, senderOpenId)
194
216
  await mkdir(dir, { recursive: true })
195
217
  return ctx.workspaceRegistry.create(
196
218
  dir,
197
- workspaceTitleFor(chatType, chatId, senderOpenId, topicToken),
219
+ workspaceTitleFor(chatType, chatId, senderOpenId),
198
220
  )
199
221
  }