@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
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Post-connect scope self-check: compares the tenant scopes the Feishu app has
3
+ * been granted (`application.v6.scope.list`) against what the plugin uses, and
4
+ * builds developer-console deep links for the missing ones.
5
+ *
6
+ * Pure module: no I/O. The service fetches the grants and the app-owner
7
+ * lookup outcome, then calls {@link checkScopes}.
8
+ *
9
+ * @module dsh-channels/feishu/scopes
10
+ */
11
+
12
+ /** One scope row reported by `application.v6.scope.list`. */
13
+ export interface ScopeGrant {
14
+ /** Scope name, e.g. `im:message`. */
15
+ readonly name: string
16
+ /** Whether the tenant granted it (`grant_status === 1`). */
17
+ readonly granted: boolean
18
+ /** Identity the scope applies to; absent when Feishu omitted it. */
19
+ readonly type?: 'tenant' | 'user'
20
+ }
21
+
22
+ /** One plugin need, satisfied by any of its scopes. */
23
+ export interface ScopeRequirement {
24
+ /** Scope names, preferred first; any granted one satisfies the need. */
25
+ readonly anyOf: readonly string[]
26
+ /** `required` breaks a core path; `optional` only degrades a feature. */
27
+ readonly level: 'required' | 'optional'
28
+ /** What the plugin loses without it (shown in the panel and log). */
29
+ readonly purpose: string
30
+ }
31
+
32
+ /** Facts that decide which requirements apply to the running connection. */
33
+ export interface ScopeCheckContext {
34
+ /** Direct messages are accepted (`dmMode: 'open'`). */
35
+ readonly dmEnabled: boolean
36
+ /** Ordinary group messages need an @ mention (`requireMention`). */
37
+ readonly requireMention: boolean
38
+ /**
39
+ * The app-owner lookup failed while it backs the administrator (guarded
40
+ * mode without `adminUserId`).
41
+ */
42
+ readonly ownerLookupFailed: boolean
43
+ /** The native slash-command panel sync is enabled (`slashCommandPanel`). */
44
+ readonly slashCommandPanel: boolean
45
+ /** The `feishu_send_file` tool is enabled (`feishuTools.sendFile`). */
46
+ readonly sendFile: boolean
47
+ }
48
+
49
+ /** A requirement none of whose scopes is granted. */
50
+ export interface MissingScope {
51
+ readonly anyOf: readonly string[]
52
+ readonly level: 'required' | 'optional'
53
+ readonly purpose: string
54
+ /** Console permission page filtered to the preferred scope. */
55
+ readonly consoleUrl: string
56
+ }
57
+
58
+ /** Result of one self-check. */
59
+ export interface ScopeCheckResult {
60
+ /** Required needs first, then optional ones, each in table order. */
61
+ readonly missing: readonly MissingScope[]
62
+ /** Console permission page filtered to every missing preferred scope. */
63
+ readonly consoleUrl?: string
64
+ }
65
+
66
+ /** `grant_status` value Feishu uses for a granted scope. */
67
+ export const SCOPE_GRANTED_STATUS = 1
68
+
69
+ /** Messaging scopes that need no runtime context to apply. */
70
+ const BASE_REQUIREMENTS: readonly ScopeRequirement[] = [
71
+ { anyOf: ['im:message', 'im:message:send_as_bot'], level: 'required', purpose: '以机器人身份发送消息、下载消息附件' },
72
+ { anyOf: ['contact:user.employee_id:readonly'], level: 'required', purpose: '获取用户 user_id;缺失时所有用户消息都会被丢弃' },
73
+ { anyOf: ['im:message:update', 'im:message'], level: 'required', purpose: '流式更新回复卡片' },
74
+ { anyOf: ['im:message.reactions:write_only', 'im:message'], level: 'optional', purpose: '给消息添加/移除处理中表情' },
75
+ { anyOf: ['im:message:readonly', 'im:message'], level: 'optional', purpose: '读取引用消息、话题历史与 feishu_topic_messages' },
76
+ { anyOf: ['im:chat:readonly', 'im:chat'], level: 'optional', purpose: '机器人入群后读取群信息、发送群信息卡' },
77
+ ]
78
+
79
+ /** Panel sync needs, applicable while `slashCommandPanel` is on. */
80
+ const SLASH_COMMAND_REQUIREMENTS: readonly ScopeRequirement[] = [
81
+ { anyOf: ['application:app_slash_command:read'], level: 'optional', purpose: '读取飞书原生斜杠指令面板,连接后同步插件命令' },
82
+ { anyOf: ['application:app_slash_command:write'], level: 'optional', purpose: '写入飞书原生斜杠指令面板(输入 / 时列出插件命令)' },
83
+ ]
84
+
85
+ /** Upload need of `feishu_send_file`, applicable while `feishuTools.sendFile` is on. */
86
+ const SEND_FILE_REQUIREMENT: ScopeRequirement = {
87
+ anyOf: ['im:resource', 'im:resource:upload'], level: 'optional', purpose: '上传图片/文件,让模型用 feishu_send_file 把生成的文件发给用户',
88
+ }
89
+
90
+ /** Group receive needs while ordinary group messages require an @ mention. */
91
+ const MENTION_GROUP_REQUIREMENTS: readonly ScopeRequirement[] = [
92
+ { anyOf: ['im:message.group_at_msg:readonly', 'im:message.group_msg'], level: 'required', purpose: '接收群聊中 @ 机器人的消息' },
93
+ { anyOf: ['im:message.group_msg'], level: 'optional', purpose: '读取群/话题中未 @ 机器人的历史消息(敏感权限,需管理员审批)' },
94
+ ]
95
+
96
+ /** Group receive need while unmentioned group messages are answered (`requireMention: false`). */
97
+ const ALL_GROUP_REQUIREMENT: ScopeRequirement = {
98
+ anyOf: ['im:message.group_msg'], level: 'required', purpose: '接收群聊中所有消息(requireMention 已关闭;敏感权限,需管理员审批)',
99
+ }
100
+
101
+ /** Direct-message receive scope, applicable while DMs are accepted. */
102
+ const DM_REQUIREMENT: ScopeRequirement = {
103
+ anyOf: ['im:message.p2p_msg:readonly'], level: 'required', purpose: '接收用户发给机器人的私聊消息',
104
+ }
105
+
106
+ /**
107
+ * App-info scope for the owner-as-administrator fallback. Optional: a
108
+ * configured `adminUserId` replaces it. Its exact console name is not
109
+ * documented, so the lookup outcome is authoritative; the names only feed
110
+ * the deep link.
111
+ */
112
+ const APP_INFO_REQUIREMENT: ScopeRequirement = {
113
+ anyOf: ['admin:app.info:readonly', 'application:application:self_manage'],
114
+ level: 'optional',
115
+ purpose: '自动把应用所有者设为管理员;不开通时在「访问授权」手动填写管理员 user_id 即可',
116
+ }
117
+
118
+ /**
119
+ * Developer-console permission page for `appId`, searched for `scopes`.
120
+ *
121
+ * @param appId Resolved Feishu app id (`cli_...`).
122
+ * @param scopes Scope names to prefill in the console search box.
123
+ * @returns The `open.feishu.cn` permission-management URL.
124
+ */
125
+ export function scopeConsoleUrl(appId: string, scopes: readonly string[]): string {
126
+ const query = new URLSearchParams({ q: scopes.join(','), op_from: 'openapi', token_type: 'tenant' })
127
+ return `https://open.feishu.cn/app/${encodeURIComponent(appId)}/auth?${query.toString()}`
128
+ }
129
+
130
+ /**
131
+ * The requirements that apply under `context`, in display order.
132
+ *
133
+ * @param context Runtime facts (DM mode, mention requirement, send-file tool, panel sync, owner-fallback need).
134
+ * @returns Requirement table for this connection.
135
+ */
136
+ export function scopeRequirements(context: ScopeCheckContext): readonly ScopeRequirement[] {
137
+ return [
138
+ ...(context.dmEnabled ? [DM_REQUIREMENT] : []),
139
+ ...(context.requireMention ? MENTION_GROUP_REQUIREMENTS : [ALL_GROUP_REQUIREMENT]),
140
+ ...BASE_REQUIREMENTS,
141
+ ...(context.sendFile ? [SEND_FILE_REQUIREMENT] : []),
142
+ ...(context.slashCommandPanel ? SLASH_COMMAND_REQUIREMENTS : []),
143
+ ]
144
+ }
145
+
146
+ /**
147
+ * Compare `grants` with the applicable requirements. Only tenant (or untyped)
148
+ * grants count: the plugin always calls with the app identity.
149
+ *
150
+ * @param grants Rows from `application.v6.scope.list`.
151
+ * @param appId Resolved Feishu app id, for the deep links.
152
+ * @param context Runtime facts deciding which requirements apply.
153
+ * @returns Missing needs (required first) and an aggregate console link.
154
+ */
155
+ export function checkScopes(grants: readonly ScopeGrant[], appId: string, context: ScopeCheckContext): ScopeCheckResult {
156
+ const granted = new Set(grants.filter(grant => grant.granted && grant.type !== 'user').map(grant => grant.name))
157
+ const unmet = scopeRequirements(context).filter(requirement => !requirement.anyOf.some(scope => granted.has(scope)))
158
+ if (context.ownerLookupFailed) unmet.push(APP_INFO_REQUIREMENT)
159
+ const ordered = [
160
+ ...unmet.filter(requirement => requirement.level === 'required'),
161
+ ...unmet.filter(requirement => requirement.level === 'optional'),
162
+ ]
163
+ const missing = ordered.map((requirement): MissingScope => ({
164
+ anyOf: requirement.anyOf,
165
+ level: requirement.level,
166
+ purpose: requirement.purpose,
167
+ consoleUrl: scopeConsoleUrl(appId, [requirement.anyOf[0]!]),
168
+ }))
169
+ if (missing.length === 0) return { missing }
170
+ return { missing, consoleUrl: scopeConsoleUrl(appId, missing.map(item => item.anyOf[0]!)) }
171
+ }