@modesto_ma/dsh-channels 0.2.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.
Files changed (46) hide show
  1. package/README.md +115 -39
  2. package/docs/access-control-design.md +1 -1
  3. package/docs/attachment-pipeline-v2.md +318 -0
  4. package/docs/feasibility-and-design.md +54 -26
  5. package/docs/feishu-onboarding.md +123 -15
  6. package/docs/persistence.md +4 -4
  7. package/docs/refactor-plan.md +7 -6
  8. package/docs/workspace-isolation-design.md +81 -0
  9. package/lib/client.js +30 -15
  10. package/lib/client.js.map +1 -1
  11. package/lib/index.js +11008 -5132
  12. package/lib/index.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/cards/index.ts +1 -0
  15. package/src/cards/onboarding.ts +23 -0
  16. package/src/cards/primitives.ts +20 -0
  17. package/src/cards/streaming.ts +310 -291
  18. package/src/cleanup.ts +267 -119
  19. package/src/client/panel/cleanup-card.tsx +4 -2
  20. package/src/client/panel/workspace-tab.tsx +1 -1
  21. package/src/config.ts +221 -13
  22. package/src/domain.ts +141 -3
  23. package/src/feishu/agent-context.ts +66 -16
  24. package/src/feishu/inbound-attachments.ts +440 -3
  25. package/src/feishu/manifest.ts +270 -0
  26. package/src/feishu/message-read.ts +1333 -0
  27. package/src/feishu/native-attachments.ts +998 -0
  28. package/src/feishu/pending-attachments.ts +562 -0
  29. package/src/feishu/tools.ts +2345 -0
  30. package/src/feishu-channel.ts +55 -0
  31. package/src/host/agent.ts +55 -0
  32. package/src/host/attachments.ts +210 -0
  33. package/src/host/index.ts +1 -0
  34. package/src/index.ts +8 -6
  35. package/src/routing/inbound.ts +730 -81
  36. package/src/service.ts +536 -50
  37. package/src/session/isolation.ts +277 -0
  38. package/src/session/manager.ts +120 -23
  39. package/src/session/repair.ts +2 -3
  40. package/src/session/routes.ts +2 -0
  41. package/src/session-commands.ts +41 -14
  42. package/src/settings/contract.ts +43 -21
  43. package/src/settings/overlay.ts +124 -13
  44. package/src/settings/surface.ts +147 -14
  45. package/src/streamer.ts +70 -30
  46. package/src/workspaces.ts +74 -31
package/src/workspaces.ts CHANGED
@@ -1,13 +1,26 @@
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)
7
8
  *
8
- * Attachments received before a topic exists are staged below
9
- * `<baseDir>/_staging/<chatType>/<chat_id>/<open_id>/` and adopted into the
10
- * topic workspace once its first Session is created.
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.
16
+ *
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).
11
24
  *
12
25
  * Feishu ids (`ou_…`, `oc_…`) are safe path segments, but they are validated
13
26
  * anyway so a hostile id can never escape `baseDir`.
@@ -123,36 +136,68 @@ export function topicTokenFor(
123
136
  return kind === 'thread' ? safeSegment(routeKey, 'route key') : safeSegment(sessionId, 'session id')
124
137
  }
125
138
 
139
+ /** Git branch (and worktree directory) name for one topic token. */
140
+ export function worktreeBranchFor(topicToken: string): string {
141
+ return `feishu-${safeSegment(topicToken, 'topic token')}`
142
+ }
143
+
126
144
  /**
127
- * Deterministic workspace directory for one Feishu topic: every route gets its
128
- * own workspace below its (user × chat) grouping directory.
145
+ * Deterministic workspace directory for one user in one chat context: the
146
+ * durable project the user's Sessions are grouped under.
129
147
  *
130
- * - p2p: `<baseDir>/topics/p2p/<open_id>/<topic>/`
131
- * - group: `<baseDir>/topics/groups/<chat_id>/<open_id>/<topic>/`
148
+ * - p2p: `<baseDir>/p2p/<open_id>/`
149
+ * - group: `<baseDir>/groups/<chat_id>/<open_id>/`
132
150
  *
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.
151
+ * These are the same paths older versions used as their shared (user × chat)
152
+ * workspaces, so an upgraded deployment reuses the existing registry record
153
+ * instead of creating a second project for the same user.
136
154
  */
137
155
  export function workspacePathFor(
138
156
  baseDir: string,
139
157
  chatType: 'p2p' | 'group',
140
158
  chatId: string,
141
159
  senderOpenId: string,
142
- topicToken: string,
143
160
  ): string {
144
- const chat = safeSegment(chatId, 'chatId')
145
161
  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)
162
+ if (chatType === 'p2p') return path.join(baseDir, 'p2p', user)
163
+ return path.join(baseDir, 'groups', safeSegment(chatId, 'chatId'), user)
164
+ }
165
+
166
+ /** Per-topic working directory inside one user workspace. */
167
+ export function topicDirFor(workspacePath: string, topicToken: string): string {
168
+ return path.join(workspacePath, 'topics', safeSegment(topicToken, 'topic token'))
169
+ }
170
+
171
+ /**
172
+ * Root of the legacy (one workspace per topic) directories. Legacy workspaces
173
+ * live strictly below this path, which is how the retention cleanup tells them
174
+ * apart from user workspaces that must never be deleted.
175
+ */
176
+ export function legacyTopicWorkspaceRoot(baseDir: string): string {
177
+ return path.join(baseDir, 'topics')
178
+ }
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')
150
192
  }
151
193
 
152
194
  /**
153
195
  * 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.
196
+ * its topic directory) exists. Not a registered workspace: files move into the
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).
156
201
  */
157
202
  export function stagingPathFor(
158
203
  baseDir: string,
@@ -162,7 +207,7 @@ export function stagingPathFor(
162
207
  ): string {
163
208
  const chat = safeSegment(chatId, 'chatId')
164
209
  const user = safeSegment(senderOpenId, 'senderOpenId')
165
- return path.join(baseDir, '_staging', chatType, chat, user)
210
+ return path.join(stagingRoot(baseDir), chatType, chat, user)
166
211
  }
167
212
 
168
213
  /** Human-friendly workspace title shown in the DSH workspace list. */
@@ -170,17 +215,16 @@ export function workspaceTitleFor(
170
215
  chatType: 'p2p' | 'group',
171
216
  chatId: string,
172
217
  senderOpenId: string,
173
- topicToken?: string,
174
218
  ): string {
175
- const base = chatType === 'p2p'
219
+ return chatType === 'p2p'
176
220
  ? `飞书私聊 · ${senderOpenId}`
177
221
  : `飞书群聊 · ${chatId} · ${senderOpenId}`
178
- return topicToken === undefined ? base : `${base} · ${topicToken}`
179
222
  }
180
223
 
181
224
  /**
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.
225
+ * Ensure one user's workspace directory exists and is registered with the
226
+ * host's workspace registry. Idempotent: repeated calls return the same record,
227
+ * and an existing record keeps its (possibly renamed) title.
184
228
  */
185
229
  export async function ensureWorkspace(
186
230
  ctx: HostContext,
@@ -188,12 +232,11 @@ export async function ensureWorkspace(
188
232
  chatType: 'p2p' | 'group',
189
233
  chatId: string,
190
234
  senderOpenId: string,
191
- topicToken: string,
192
235
  ): Promise<Workspace> {
193
- const dir = workspacePathFor(baseDir, chatType, chatId, senderOpenId, topicToken)
236
+ const dir = workspacePathFor(baseDir, chatType, chatId, senderOpenId)
194
237
  await mkdir(dir, { recursive: true })
195
238
  return ctx.workspaceRegistry.create(
196
239
  dir,
197
- workspaceTitleFor(chatType, chatId, senderOpenId, topicToken),
240
+ workspaceTitleFor(chatType, chatId, senderOpenId),
198
241
  )
199
242
  }