@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.2

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 (66) hide show
  1. package/README.md +83 -57
  2. package/docs/access-control-design.md +8 -5
  3. package/docs/attachment-pipeline-v2.md +33 -41
  4. package/docs/feishu-onboarding.md +36 -33
  5. package/docs/persistence.md +6 -4
  6. package/docs/refactor-plan.md +1 -1
  7. package/docs/workspace-isolation-design.md +7 -8
  8. package/lib/client.js +444 -288
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +9017 -6794
  11. package/lib/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/access/cards.ts +9 -9
  14. package/src/access/controller.ts +10 -10
  15. package/src/access/index.ts +1 -0
  16. package/src/access/policy.ts +10 -10
  17. package/src/access/requests.ts +32 -31
  18. package/src/access/store.ts +43 -13
  19. package/src/access/targets.ts +56 -0
  20. package/src/cards/approval.ts +96 -26
  21. package/src/cards/index.ts +1 -0
  22. package/src/cards/onboarding.ts +1 -0
  23. package/src/cards/picker.ts +108 -0
  24. package/src/cards/streaming.ts +56 -7
  25. package/src/cards/wiki.ts +124 -0
  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 +96 -24
  32. package/src/client/panel/workspace-tab.tsx +2 -18
  33. package/src/client/string-list.ts +139 -0
  34. package/src/client/styles.ts +5 -0
  35. package/src/commands/registry.ts +8 -5
  36. package/src/config.ts +55 -54
  37. package/src/domain.ts +36 -123
  38. package/src/feishu/delivery.ts +34 -1
  39. package/src/feishu/inbound-attachments.ts +1 -1
  40. package/src/feishu/manifest.ts +8 -31
  41. package/src/feishu/message-read.ts +97 -24
  42. package/src/feishu/scopes.ts +194 -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 +72 -112
  46. package/src/feishu/wiki/api.ts +346 -0
  47. package/src/feishu/wiki/consent.ts +183 -0
  48. package/src/feishu/wiki/links.ts +122 -0
  49. package/src/feishu/wiki/scope.ts +66 -0
  50. package/src/feishu/wiki/tools.ts +984 -0
  51. package/src/feishu-channel.ts +280 -9
  52. package/src/index.ts +5 -4
  53. package/src/interactions/approvals.ts +187 -7
  54. package/src/interactions/pending.ts +3 -3
  55. package/src/routing/inbound.ts +73 -106
  56. package/src/service.ts +545 -440
  57. package/src/session/manager.ts +1 -1
  58. package/src/session/routes.ts +2 -3
  59. package/src/settings/contract.ts +110 -89
  60. package/src/settings/routes.ts +3 -17
  61. package/src/settings/surface.ts +144 -94
  62. package/src/streamer.ts +94 -5
  63. package/src/workspaces.ts +21 -29
  64. package/src/cleanup.ts +0 -546
  65. package/src/client/panel/cleanup-card.tsx +0 -106
  66. package/src/feishu/pending-attachments.ts +0 -562
@@ -293,7 +293,7 @@ export class SessionManager {
293
293
  const workspacePath = record.workspacePath
294
294
  const workspace = await this.deps.ctx.workspaceRegistry.create(
295
295
  workspacePath,
296
- workspaceTitleFor(record.chatType, record.chatId, record.senderOpenId),
296
+ workspaceTitleFor(record.chatType, record.chatId, record.senderUserId),
297
297
  )
298
298
 
299
299
  let newHandle: AgentHandle | undefined
@@ -135,8 +135,7 @@ export class RouteStore {
135
135
 
136
136
  /**
137
137
  * Retire one active thread route into a persistent tombstone regardless of
138
- * the configured TTL. Used by the workspace-retention cleanup, which may
139
- * close topics earlier than the route TTL.
138
+ * the configured TTL.
140
139
  */
141
140
  async retire(key: string, now = Date.now()): Promise<ExpiredRouteRecord | undefined> {
142
141
  const record = this.get(key)
@@ -145,7 +144,7 @@ export class RouteStore {
145
144
  key,
146
145
  chatId: record.chatId,
147
146
  chatType: record.chatType,
148
- senderOpenId: record.senderOpenId,
147
+ senderUserId: record.senderUserId,
149
148
  expiredAt: now,
150
149
  workspacePath: record.workspacePath,
151
150
  ...(record.sessionDir === undefined ? {} : { sessionDir: record.sessionDir }),
@@ -20,13 +20,11 @@ export const CREDENTIALS_ROUTE = '/dsh-channels/credentials'
20
20
  export const RECONNECT_ROUTE = '/dsh-channels/reconnect'
21
21
  /** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
22
22
  export const CATALOG_ROUTE = '/dsh-channels/catalog'
23
- /** 手动执行一次过期目录清理(POST,无请求体)。 */
24
- export const CLEANUP_ROUTE = '/dsh-channels/cleanup'
25
23
  /** 访问与授权只读快照(GET/HEAD):准入模式、管理员、群/用户白名单、待处理申请。 */
26
24
  export const ACCESS_ROUTE = '/dsh-channels/access'
27
25
  /** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
28
26
  export const ACCESS_GROUPS_ROUTE = '/dsh-channels/access/groups'
29
- /** 用户白名单增删(POST/DELETE,body `{ openId, name? }` / `{ openId }`)。 */
27
+ /** 用户白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
30
28
  export const ACCESS_USERS_ROUTE = '/dsh-channels/access/users'
31
29
  /** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
32
30
  export const ACCESS_REQUESTS_ROUTE = '/dsh-channels/access/requests'
@@ -56,16 +54,15 @@ export const PICK_DIRECTORY_ROUTE = '/dsh-channels/pick-directory'
56
54
  export const EDITABLE_KEYS = [
57
55
  // 工作区
58
56
  'baseDir',
59
- 'workspaceRetention',
60
- 'workspaceRetentionAuto',
61
57
  // 行为
62
58
  'dmMode',
63
59
  'requireMention',
64
60
  'threadFallback',
65
61
  'welcomeOnboarding',
62
+ 'slashCommandPanel',
66
63
  'allowPermissionSwitch',
67
64
  // 准入与授权(管理员/准入模式/申请有效期;白名单由「访问与授权」卡片管理)
68
- 'adminOpenId',
65
+ 'adminUserId',
69
66
  'accessMode',
70
67
  'authRequestTtlDays',
71
68
  // 内容与附件(v2 管线;`feishuTools.*` 是嵌套配置,覆盖层仍写成嵌套对象)
@@ -76,8 +73,6 @@ export const EDITABLE_KEYS = [
76
73
  'prefetchImageMaxBytes',
77
74
  'prefetchTotalMaxBytes',
78
75
  'historyBootstrapLimit',
79
- 'pendingAttachmentTtlMs',
80
- 'pendingAttachmentLimit',
81
76
  // 模型工具(feishuTools 子项)
82
77
  'feishuTools.attachmentFetch',
83
78
  'feishuTools.attachmentFetchMaxResources',
@@ -85,6 +80,11 @@ export const EDITABLE_KEYS = [
85
80
  'feishuTools.attachmentFetchTimeoutMs',
86
81
  'feishuTools.topicMessages',
87
82
  'feishuTools.topicMessagesTimeoutMs',
83
+ 'feishuTools.sendFile',
84
+ 'feishuTools.wikiRead',
85
+ 'feishuTools.wikiWrite',
86
+ // 知识库范围(顶层 `wikiRoots` 字符串列表;为空时不挂载任何知识库工具)
87
+ 'wikiRoots',
88
88
  'promptContextTimeoutMs',
89
89
  // 模型
90
90
  'provider',
@@ -106,6 +106,7 @@ export type EditableKey = (typeof EDITABLE_KEYS)[number]
106
106
  export const RESTART_KEYS = [
107
107
  'dmMode',
108
108
  'requireMention',
109
+ 'slashCommandPanel',
109
110
  'refreshMs',
110
111
  'statusRefreshMs',
111
112
  'loggerLevel',
@@ -113,8 +114,21 @@ export const RESTART_KEYS = [
113
114
 
114
115
  export type RestartKey = (typeof RESTART_KEYS)[number]
115
116
 
116
- /** Render kinds the generic panel form understands. */
117
- export type FieldKind = 'boolean' | 'number' | 'text' | 'select'
117
+ /**
118
+ * Render kinds the generic panel form understands. `string-list` renders one
119
+ * single-line input per entry with a remove button each and an add button
120
+ * below; its value is a `readonly string[]`.
121
+ */
122
+ export type FieldKind = 'boolean' | 'number' | 'text' | 'select' | 'string-list'
123
+
124
+ /**
125
+ * Per-entry format of a `string-list` field. Host and Client both map the
126
+ * format onto the same entry parser, so an invalid entry is marked before
127
+ * saving and the Host re-validates it on save.
128
+ *
129
+ * - `wiki-root-link`: a Feishu wiki space link or wiki node link (`wikiRoots`).
130
+ */
131
+ export type StringListItemFormat = 'wiki-root-link'
118
132
 
119
133
  /**
120
134
  * Field groups partition the editable form. Each group is rendered inside one
@@ -142,7 +156,12 @@ export interface EditableFieldWire {
142
156
  min?: number
143
157
  max?: number
144
158
  step?: number
159
+ /** Input placeholder; for `string-list` it applies to every entry input. */
145
160
  placeholder?: string
161
+ /** `string-list` only: most non-empty entries the Host accepts. */
162
+ maxItems?: number
163
+ /** `string-list` only: format every non-empty entry must satisfy. */
164
+ itemFormat?: StringListItemFormat
146
165
  /** Render across the whole form width (long paths). */
147
166
  wide?: boolean
148
167
  /**
@@ -185,8 +204,13 @@ export interface CatalogWire {
185
204
  defaultPresetId?: string
186
205
  }
187
206
 
188
- /** Values the panel may submit. Empty strings for optional text fields clear the override. */
189
- export type ConfigValue = string | number | boolean
207
+ /**
208
+ * Values the panel may submit. Empty strings for optional text fields clear the
209
+ * override. A `string-list` field submits its entries as typed (untrimmed,
210
+ * empty inputs included) so Host issue indexes match the rendered inputs; the
211
+ * Host trims entries and drops empty ones before persisting.
212
+ */
213
+ export type ConfigValue = string | number | boolean | readonly string[]
190
214
  export type EditableValues = Partial<Record<EditableKey, ConfigValue>>
191
215
 
192
216
  /** Read-only facts shown in the advanced card. */
@@ -266,8 +290,40 @@ export interface ChannelStatusWire {
266
290
  overlayPath: string
267
291
  /** 正在连接或重连时为 true,此时面板的写操作按钮禁用。 */
268
292
  busy: boolean
293
+ /** 连接后的权限自检结果;未连接或自检尚未完成时缺省。 */
294
+ scopeCheck?: ScopeCheckWire
295
+ /** 连接后飞书原生斜杠指令面板的同步结果;未连接或同步尚未完成时缺省。 */
296
+ slashCommandSync?: SlashCommandSyncWire
269
297
  }
270
298
 
299
+ /** One plugin need whose scopes are all missing. */
300
+ export interface MissingScopeWire {
301
+ /** Scope names, preferred first; granting any one satisfies the need. */
302
+ scopes: string[]
303
+ /** `required` breaks a core path; `optional` only degrades a feature. */
304
+ level: 'required' | 'optional'
305
+ /** What the plugin loses without it. */
306
+ purpose: string
307
+ /** Developer-console permission page filtered to the preferred scope. */
308
+ consoleUrl: string
309
+ }
310
+
311
+ /** Post-connect scope self-check outcome. */
312
+ export type ScopeCheckWire =
313
+ /** Every applicable need is granted. */
314
+ | { state: 'ok'; checkedAt: number }
315
+ /** Some needs are missing; `consoleUrl` opens the console filtered to all of them. */
316
+ | { state: 'missing'; checkedAt: number; missing: MissingScopeWire[]; consoleUrl: string }
317
+ /** The grant list could not be read; `error` carries the Feishu reason. */
318
+ | { state: 'error'; checkedAt: number; error: string }
319
+
320
+ /** Post-connect native slash-command panel sync outcome. */
321
+ export type SlashCommandSyncWire =
322
+ /** The panel lists exactly the registry commands; counts are this sync's writes. */
323
+ | { state: 'ok'; checkedAt: number; created: number; updated: number; removed: number; total: number }
324
+ /** A Feishu call failed; `error` carries the Feishu reason. */
325
+ | { state: 'error'; checkedAt: number; error: string }
326
+
271
327
  /** The whole editable configuration surface, with provenance. */
272
328
  export interface PanelConfigWire {
273
329
  /** profile `cordis.patch.yml` 提供的值(覆盖层之下)。 */
@@ -298,9 +354,9 @@ export interface AccessGroupWire {
298
354
  addedAt: number
299
355
  }
300
356
 
301
- /** One authorized user `open_id` (global across every authorized group). */
357
+ /** One authorized user `user_id` (global across every authorized group). */
302
358
  export interface AccessUserWire {
303
- openId: string
359
+ userId: string
304
360
  /** Display name captured at authorization time. */
305
361
  name?: string
306
362
  /** Which authorization entry produced the grant. */
@@ -314,7 +370,7 @@ export interface AccessRequestWire {
314
370
  /** Opaque token; never derived from the target id. */
315
371
  token: string
316
372
  kind: 'group' | 'user'
317
- /** `chat_id` for a group request, `open_id` for a user request. */
373
+ /** `chat_id` for a group request, `user_id` for a user request. */
318
374
  targetId: string
319
375
  /** Display name of the requested target (group name / user name), when known. */
320
376
  name?: string
@@ -337,9 +393,14 @@ export interface AccessRequestWire {
337
393
  export interface AccessWire {
338
394
  mode: 'open' | 'guarded'
339
395
  admin: {
340
- openId?: string
396
+ /** The effective administrator: configured `adminUserId`, else the app owner. */
397
+ userId?: string
398
+ /** Where `userId` came from: explicit config or the app-owner fallback. */
399
+ source?: 'config' | 'owner'
341
400
  /** false while guarded mode would refuse every conversation (fail closed). */
342
401
  configured: boolean
402
+ /** Why the app-owner fallback is unavailable, when its lookup failed. */
403
+ ownerLookupError?: string
343
404
  }
344
405
  /**
345
406
  * Whether the persisted whitelists were loaded. While the channel holds no
@@ -361,51 +422,10 @@ export type AccessMutationResponse =
361
422
  export interface PanelWire {
362
423
  status: ChannelStatusWire
363
424
  config: PanelConfigWire
364
- cleanup: CleanupPreviewWire
365
425
  /** 访问与授权卡片的数据(准入模式、管理员、白名单、待处理申请)。 */
366
426
  access: AccessWire
367
427
  }
368
428
 
369
- /**
370
- * What the workspace-retention cleanup would delete right now. Counts are
371
- * zero and `error` is set when the Host cannot compute the preview (for
372
- * example the cleanup seam is unavailable); the panel stays usable.
373
- */
374
- export interface CleanupPreviewWire {
375
- /** Effective `workspaceRetention` value (`off` / `7d` / `30d` / `90d` / `180d`). */
376
- retention: string
377
- /** Retention threshold in milliseconds; absent when `retention` is `off`. */
378
- thresholdMs?: number
379
- /** Periodic cleanup runs at startup and once per day. */
380
- auto: boolean
381
- /** Plugin-created workspaces under the current baseDir (user projects). */
382
- workspaces: number
383
- /** Directories older than the threshold (legacy topic workspaces + per-topic dirs). */
384
- candidates: number
385
- /** Total bytes currently occupied by the candidates. */
386
- candidateBytes: number
387
- /** Why the preview could not be computed (counts are zero then). */
388
- error?: string
389
- }
390
-
391
- /** Outcome of one manual cleanup run (partial failures do not abort the run). */
392
- export interface CleanupRunWire {
393
- ok: boolean
394
- deletedWorkspaces: number
395
- /** Per-topic working directories removed (current layout). */
396
- deletedSessionDirs: number
397
- retiredRoutes: number
398
- freedBytes: number
399
- /** One `path: message` line per directory that could not be deleted. */
400
- failures: string[]
401
- error?: string
402
- }
403
-
404
- /** `POST /dsh-channels/cleanup` response. */
405
- export type CleanupResponse =
406
- | { ok: true; panel: PanelWire; result: CleanupRunWire }
407
- | { ok: false; error: string }
408
-
409
429
  /**
410
430
  * `POST /dsh-channels/pick-directory` response. `canceled` is a normal
411
431
  * outcome (the operator dismissed the OS dialog), not an error; `path` is an
@@ -425,6 +445,11 @@ export interface SaveConfigRequest {
425
445
  export interface FieldIssueWire {
426
446
  key: string
427
447
  message: string
448
+ /**
449
+ * `string-list` only: zero-based position of the rejected entry in the
450
+ * submitted array (empty inputs included). Absent for a whole-field issue.
451
+ */
452
+ index?: number
428
453
  }
429
454
 
430
455
  /** `PUT /dsh-channels/config` response. */
@@ -540,6 +565,21 @@ export const PANEL_COPY = {
540
565
  statusSummaryConnecting: '正在连接飞书长连接…',
541
566
  statusSummaryError: '最近一次连接飞书失败,请检查凭据与网络后重试。',
542
567
  lastError: '最近一次连接失败',
568
+ scopeCheckTitle: '权限自检',
569
+ scopeCheckOk: '已开通插件需要的全部飞书权限。',
570
+ scopeCheckMissing: '以下飞书权限未开通,开通后需在开发者后台发布新版本:',
571
+ scopeCheckOpenAll: '在开发者后台开通全部缺失权限',
572
+ scopeCheckOpenOne: '去开通',
573
+ scopeCheckOptional: '可选',
574
+ scopeCheckRequired: '必需',
575
+ scopeCheckError: '无法读取应用的权限授予状态:',
576
+ slashCommandSyncTitle: '斜杠指令面板',
577
+ slashCommandSyncOk: '插件命令已同步到飞书原生斜杠指令面板,在聊天框输入 / 即可选择(飞书客户端约 5 分钟后刷新)。',
578
+ slashCommandSyncTotal: '命令数',
579
+ slashCommandSyncCreated: '本次新增',
580
+ slashCommandSyncUpdated: '本次更新',
581
+ slashCommandSyncRemoved: '本次删除',
582
+ slashCommandSyncError: '同步飞书原生斜杠指令面板失败(不影响直接输入命令):',
543
583
  missingConfig: '缺少配置',
544
584
  statusHintUnconfigured: '配置完成后插件会自动连接;也可以稍后在本页手动连接。',
545
585
  connectionInfo: '连接信息',
@@ -588,9 +628,9 @@ export const PANEL_COPY = {
588
628
  credentialsPendingHint: '填写后点「保存凭据」;两项齐全时插件会自动连接飞书。',
589
629
  // ---- tabs / forms -------------------------------------------------------
590
630
  connectionTabHelp: '连接状态与飞书应用凭据。凭据不完整时不会连接飞书。',
591
- sessionsTabHelp: '私聊、群聊触发方式与入站内容:附件存储/预取、静默登记保留、模型工具开关与读取预算;「需重连」项在下次连接时读取。',
631
+ sessionsTabHelp: '私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。',
592
632
  modelTabHelp: '飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。',
593
- workspaceTabHelp: '每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录,也不会被这里的目录清理删除。',
633
+ workspaceTabHelp: '每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。',
594
634
  accessTabHelp: '准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。',
595
635
  advancedTabHelp: '流式刷新等低层参数、Web 覆盖文件与只读信息。',
596
636
  save: '保存更改',
@@ -599,6 +639,12 @@ export const PANEL_COPY = {
599
639
  discardTab: '放弃本页更改',
600
640
  saveHintClean: '没有未保存的更改。',
601
641
  dirtyCount: '未保存项',
642
+ listAdd: '添加',
643
+ listRemove: '移除',
644
+ listEntry: '第 {n} 项',
645
+ listTooMany: '最多 {max} 项(空输入框不计)',
646
+ listNotArray: '必须是字符串列表',
647
+ listEntryNotString: '必须是字符串',
602
648
  browseDirectory: '选择目录…',
603
649
  browsingDirectory: '选择中…',
604
650
  directoryPicked: '已选择目录',
@@ -637,37 +683,12 @@ export const PANEL_COPY = {
637
683
  advancedHelp: '这些值来自 profile 配置,不能在面板里编辑。',
638
684
  readOnlyCard: '部署级参数',
639
685
  // ---- model --------------------------------------------------------------
640
- // ---- cleanup ------------------------------------------------------------
641
- cleanupCard: '目录清理',
642
- cleanupCardHelp: '只清理工作区与 Agent 产物:删除超过保留期且不再活跃的飞书话题目录、旧版本按话题建立的工作区目录,以及这些目录内 Agent 产生的文件;不影响 Session 附件——入站图片/文件默认存于 DSH 原生附件库,不随目录清理删除。用户工作区本身与 DSH 会话日志也不会删除。删除不可恢复:相关旧话题会失效并提示已过期。',
643
- cleanupRetention: '保留期',
644
- cleanupAutoLabel: '自动清理',
645
- cleanupCandidates: '可清理目录',
646
- cleanupWorkspaces: '插件工作区',
647
- cleanupCandidateBytes: '预计释放空间',
648
- cleanupRetentionOff: '关闭(不删除)',
649
- cleanupAutoOn: '已开启(启动时 + 每天一次)',
650
- cleanupAutoOff: '已关闭(仅手动执行)',
651
- cleanupAutoInactive: '未执行(保留期为关闭)',
652
- cleanupNow: '立即清理',
653
- cleanupRunning: '清理中…',
654
- cleanupConfirm: '确定清理过期目录吗?只清理工作区与 Agent 产物,不影响 Session 附件;但这些目录内未提交的文件会被删除,且操作不可恢复。',
655
- cleanupConfirmHint: '删除前会再次确认;保留期为「关闭」或没有过期目录时按钮不可用。',
656
- cleanupDisabledOff: '保留策略为「关闭」:请先在上方「过期目录保留期」中选择保留天数。',
657
- cleanupDisabledEmpty: '没有超过保留期的过期目录。',
658
- cleanupPreviewFailed: '无法读取清理预览',
659
- cleanupDone: '清理完成',
660
- cleanupFailed: '清理失败',
661
- cleanupDeleted: '已删除工作区',
662
- cleanupDeletedDirs: '话题目录',
663
- cleanupRetired: '已清理话题路由',
664
- cleanupFreed: '释放空间',
665
- cleanupFailures: '以下目录未能删除:',
666
686
  // ---- access -------------------------------------------------------------
667
687
  accessCard: '访问与授权',
668
- accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 open_id。',
688
+ accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。',
669
689
  accessModeOpenWarning: '当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。',
670
- accessAdminMissingWarning: '当前准入=guarded 但未配置管理员,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 open_id。',
690
+ accessAdminMissingWarning: '当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。',
691
+ accessAdminOwnerNotice: '未配置管理员,已自动使用应用所有者作为管理员:',
671
692
  accessGroupsTitle: '已授权群',
672
693
  accessGroupsEmpty: '尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。',
673
694
  accessUsersTitle: '已授权用户',
@@ -676,7 +697,7 @@ export const PANEL_COPY = {
676
697
  accessRequestsEmpty: '没有待处理的授权申请。',
677
698
  accessGroupChatIdPlaceholder: '群 chat_id(oc_…)',
678
699
  accessGroupNamePlaceholder: '群名(可选)',
679
- accessUserOpenIdPlaceholder: '用户 open_id(ou_…)',
700
+ accessUserIdPlaceholder: '用户 user_id',
680
701
  accessUserNamePlaceholder: '昵称(可选)',
681
702
  accessAddGroup: '添加群',
682
703
  accessAddUser: '添加用户',
@@ -698,5 +719,5 @@ export const PANEL_COPY = {
698
719
  accessRequestAllowed: '已授权',
699
720
  accessRequestDenied: '已忽略',
700
721
  accessHints: '三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。',
701
- accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;open_id 可私聊机器人发送 /userid 获取。',
722
+ accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。',
702
723
  } as const
@@ -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
  }),