@modesto_ma/dsh-channels 0.5.0-beta.6 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modesto_ma/dsh-channels",
3
- "version": "0.5.0-beta.6",
3
+ "version": "0.5.0",
4
4
  "description": "DeepSeek Harness (DSH) 飞书渠道插件:一个 dsh 实例对应一个飞书机器人,私聊/群聊/话题群 @ 触发会话,话题绑定 session,卡片 2.0 流式回复;Web 左侧栏提供「飞书渠道」状态与常用配置面板",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,6 +49,8 @@ export interface UserRequestCardInput {
49
49
  requesterName?: string
50
50
  /** Opaque request token. */
51
51
  token: string
52
+ /** Whether whitelisted users may also use private chats (`dmAllowWhitelist`). */
53
+ dmAllowWhitelist?: boolean
52
54
  /** Render timestamp (defaults to now). */
53
55
  now?: number
54
56
  }
@@ -158,7 +160,9 @@ export function buildUserRequestCard(input: UserRequestCardInput): object {
158
160
  }
159
161
  elements.push(field('申请时间', formatTime(input.now)))
160
162
  elements.push(divider())
161
- elements.push(note('授权后该用户可在所有已授权群里使用;私聊仍仅管理员可用。'))
163
+ elements.push(note(input.dmAllowWhitelist === true
164
+ ? '授权后该用户可在所有已授权群里使用,也可以私聊机器人。'
165
+ : '授权后该用户可在所有已授权群里使用;私聊仍仅管理员可用。'))
162
166
  elements.push(decisionButtons(input.token, '授权用户', '忽略'))
163
167
  return standaloneCard('**🙋 用户授权申请**', elements)
164
168
  }
@@ -6,14 +6,15 @@
6
6
  * matrix lives in `policy.ts`; this module owns the user-facing behavior on
7
7
  * denial:
8
8
  *
9
- * - private chats: non-administrators get one fixed notice (10 s per-sender
9
+ * - private chats: denied senders (non-administrators, or non-whitelisted
10
+ * users with `dmAllowWhitelist`) get one fixed notice (10 s per-sender
10
11
  * cooldown); with no administrator configured they get the bootstrap
11
12
  * notice instead;
12
13
  * - group chats: fully silent, except that addressed denials call back into
13
14
  * the authorization-request lifecycle so the administrator can be asked;
14
15
  * - slash commands: `/userid` stays available to every private sender, the
15
16
  * administrator's commands always pass (including inside an unauthorized
16
- * group), everything else must pass admission.
17
+ * group), everything else must pass admission (in private chats too).
17
18
  *
18
19
  * The controller never creates a Session or a workspace.
19
20
  *
@@ -30,6 +31,9 @@ import type { AccessStore, AccessStoreSnapshot } from './store.ts'
30
31
  /** Fixed private-chat notice for non-administrators (no model call). */
31
32
  export const DM_DENY_TEXT = '非管理员禁用私聊功能哦~如需使用,请在管理员授权的群里 @ 我;或联系管理员把你的 user_id 加入白名单。'
32
33
 
34
+ /** Fixed private-chat notice for non-whitelisted users while `dmAllowWhitelist` is on. */
35
+ export const DM_DENY_WHITELIST_TEXT = '你还不在白名单里,暂时不能私聊哦~发送 /userid 查看你的 user_id,请管理员加入白名单后即可使用。'
36
+
33
37
  /** Fixed private-chat notice while no administrator is configured. */
34
38
  export const DM_UNCONFIGURED_TEXT = '机器人尚未配置管理员。发送 /userid 可查看你的 user_id,交给部署者配置后即可启用。'
35
39
 
@@ -123,8 +127,9 @@ export class AccessController {
123
127
  /**
124
128
  * Slash-command admission. Runs before any command card/topic/Session side
125
129
  * effect. `open` mode keeps the legacy behavior; otherwise private chats
126
- * stay administrator-only (with `/userid` as the bootstrap exception) and
127
- * group chats must pass the full admission matrix; the administrator is
130
+ * admit whoever the admission matrix admits (with `/userid` as the
131
+ * bootstrap exception) and group chats must pass the full admission
132
+ * matrix; the administrator is
128
133
  * never constrained by the whitelists, so all of their commands pass even
129
134
  * inside an unauthorized group (`/auth` is the in-conversation grant
130
135
  * entry).
@@ -136,7 +141,7 @@ export class AccessController {
136
141
  // `/userid` is the bootstrap entry for everyone: it only echoes the
137
142
  // sender's own user_id and has no storage/model side effect.
138
143
  if (commandName === 'userid') return true
139
- if (this.isAdmin(msg.senderId)) return true
144
+ if (this.decide(msg).kind === 'allow') return true
140
145
  await this.noticeDeniedDm(msg)
141
146
  return false
142
147
  }
@@ -207,7 +212,10 @@ export class AccessController {
207
212
  const last = this.dmNoticeAt.get(msg.senderId)
208
213
  if (last !== undefined && now - last < DM_NOTICE_COOLDOWN_MS) return
209
214
  this.dmNoticeAt.set(msg.senderId, now)
210
- const text = this.config().adminUserId === undefined ? DM_UNCONFIGURED_TEXT : DM_DENY_TEXT
215
+ const config = this.config()
216
+ const text = config.adminUserId === undefined
217
+ ? DM_UNCONFIGURED_TEXT
218
+ : config.dmAllowWhitelist ? DM_DENY_WHITELIST_TEXT : DM_DENY_TEXT
211
219
  await this.sendText(msg.chatId, text, msg.messageId)
212
220
  }
213
221
 
@@ -11,7 +11,9 @@
11
11
  * Access model:
12
12
  * - the administrator may start/continue a conversation in any private chat
13
13
  * or any group, regardless of the whitelists;
14
- * - private chats: non-administrators are refused;
14
+ * - private chats: non-administrators are refused, unless
15
+ * `dmAllowWhitelist` is on and their user_id is on the global user
16
+ * whitelist;
15
17
  * - group chats: non-administrators need the group to be authorized AND
16
18
  * their user_id to be on the global user whitelist.
17
19
  *
@@ -56,7 +58,7 @@ export type AccessDecision =
56
58
  }
57
59
 
58
60
  /** The configuration slice admission reads (allows easy fakes in tests). */
59
- export type AccessPolicyConfig = Pick<FeishuConfig, 'accessMode' | 'adminUserId'>
61
+ export type AccessPolicyConfig = Pick<FeishuConfig, 'accessMode' | 'adminUserId' | 'dmAllowWhitelist'>
60
62
 
61
63
  /** True when `userId` is exactly the configured administrator. */
62
64
  export function isAdminUserId(
@@ -90,6 +92,9 @@ export function decideAccess(
90
92
  if (subject.senderId.trim() === admin) return { kind: 'allow', reason: 'admin' }
91
93
 
92
94
  if (subject.chatType === 'p2p') {
95
+ if (config.dmAllowWhitelist && snapshot.users.has(subject.senderId)) {
96
+ return { kind: 'allow', reason: 'authorized-user' }
97
+ }
93
98
  return { kind: 'deny', reason: 'dm-non-admin' }
94
99
  }
95
100
 
@@ -350,6 +350,7 @@ export class AccessRequests {
350
350
  ...(groupName === undefined ? {} : { groupName }),
351
351
  ...(msg.senderName === undefined ? {} : { requesterName: msg.senderName }),
352
352
  token: record.token,
353
+ dmAllowWhitelist: this.config().dmAllowWhitelist,
353
354
  now,
354
355
  }), record)
355
356
  return true
package/src/config.ts CHANGED
@@ -384,13 +384,20 @@ export const configSchema = z.object({
384
384
  * Admission policy for inbound Feishu traffic.
385
385
  *
386
386
  * - `guarded` (default): only the configured administrator can talk in
387
- * private chats, and a group conversation additionally requires the group
388
- * to be authorized and the sender to be the administrator or an explicitly
387
+ * private chats (plus whitelisted users when `dmAllowWhitelist` is on),
388
+ * and a group conversation additionally requires the group to be
389
+ * authorized and the sender to be the administrator or an explicitly
389
390
  * authorized user.
390
391
  * - `open`: legacy behavior — any user inside the app's availability scope
391
392
  * can start a conversation. Debug/compatibility only; unsafe.
392
393
  */
393
394
  accessMode: z.enum(['open', 'guarded']).default('guarded'),
395
+ /**
396
+ * In `guarded` mode, also admit users on the global user whitelist
397
+ * (`/auth user`) in private chats. Off by default: private chats stay
398
+ * administrator-only. Each private-chat user already has an own workspace.
399
+ */
400
+ dmAllowWhitelist: z.boolean().default(false),
394
401
  /**
395
402
  * The unique administrator's Feishu `user_id`. Set it in the Web
396
403
  * settings panel (or the profile config). Get it by sending `/userid` to the
package/src/domain.ts CHANGED
@@ -217,8 +217,8 @@ export type AccessGroupRecord = z.infer<typeof accessGroupRecordSchema>
217
217
 
218
218
  /**
219
219
  * One authorized user (`user_id`), global across every authorized group.
220
- * Private chats are administrator-only, so this grant only affects group
221
- * conversations.
220
+ * Private chats are administrator-only unless `dmAllowWhitelist` is on, so
221
+ * by default this grant only affects group conversations.
222
222
  */
223
223
  export const accessUserRecordSchema = z.object({
224
224
  /** Feishu `user_id` — also the table key. */
package/src/service.ts CHANGED
@@ -1922,6 +1922,7 @@ export class FeishuService {
1922
1922
  await this.delivery.standaloneReply(msg, standaloneInfoCard('**🔐 访问授权状态**', [
1923
1923
  `**准入模式**:\`${this.config.accessMode}\``,
1924
1924
  `**管理员**:${this.adminStatusLine()}`,
1925
+ `**私聊**:${this.config.dmAllowWhitelist ? '管理员与白名单用户' : '仅管理员'}`,
1925
1926
  ...(msg.chatType === 'group'
1926
1927
  ? [`**本群**:${this.accessStore.hasGroup(msg.chatId) ? '已授权' : '未授权'}(\`${msg.chatId}\`)`]
1927
1928
  : []),
@@ -164,6 +164,7 @@ export const EDITABLE_KEYS = [
164
164
  'promptContextTimeoutMs',
165
165
  // 准入与授权(白名单由「访问授权」tab 的列表管理)
166
166
  'accessMode',
167
+ 'dmAllowWhitelist',
167
168
  'adminUserId',
168
169
  'authRequestTtlDays',
169
170
  // 知识库(顶层 `wikiRoots` 字符串列表;为空时不挂载任何知识库工具)
@@ -153,6 +153,7 @@ const BOOLEAN_KEYS = new Set<EditableKey>([
153
153
  'welcomeOnboarding',
154
154
  'slashCommandPanel',
155
155
  'allowPermissionSwitch',
156
+ 'dmAllowWhitelist',
156
157
  'acceptAttachments',
157
158
  'prefetchImages',
158
159
  'feishuTools.attachmentFetch',
@@ -401,7 +402,13 @@ const FIELD_META: Record<EditableKey, FieldMeta> = {
401
402
  label: '调试模式',
402
403
  group: 'access',
403
404
  summary: '打开后所有人都能聊,不安全,只在调试时用',
404
- help: '关闭时(默认):管理员随时可用;非管理员私聊会被拒绝;群聊要同时满足群已授权、发言人在用户白名单里。授权入口有三个:群里 /auth 命令、授权卡、本页。撤销立即生效。',
405
+ help: '关闭时(默认):管理员随时可用;非管理员私聊会被拒绝(打开「白名单用户可私聊」后白名单用户也可以);群聊要同时满足群已授权、发言人在用户白名单里。授权入口有三个:群里 /auth 命令、授权卡、本页。撤销立即生效。',
406
+ },
407
+ dmAllowWhitelist: {
408
+ label: '白名单用户可私聊',
409
+ group: 'access',
410
+ summary: '关闭时私聊仅管理员可用',
411
+ help: '打开后「已授权用户」里的人也可以私聊机器人,各自使用独立的私聊工作区,能力与群聊相同;不在白名单的人仍收到固定提示。撤销用户立即生效。',
405
412
  },
406
413
  adminUserId: {
407
414
  label: '管理员 user_id',