@downcity/plugins 1.0.153 → 1.0.156

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 (175) hide show
  1. package/bin/chat/ChatPlugin.d.ts +2 -10
  2. package/bin/chat/ChatPlugin.d.ts.map +1 -1
  3. package/bin/chat/ChatPlugin.js +3 -13
  4. package/bin/chat/ChatPlugin.js.map +1 -1
  5. package/bin/chat/PROMPT.direct.d.ts +1 -1
  6. package/bin/chat/PROMPT.direct.d.ts.map +1 -1
  7. package/bin/chat/PROMPT.direct.js +1 -1
  8. package/bin/chat/PROMPT.direct.js.map +1 -1
  9. package/bin/chat/access/ChatAccessActions.d.ts +11 -0
  10. package/bin/chat/access/ChatAccessActions.d.ts.map +1 -0
  11. package/bin/chat/access/ChatAccessActions.js +137 -0
  12. package/bin/chat/access/ChatAccessActions.js.map +1 -0
  13. package/bin/chat/access/ChatAccessRuntime.d.ts +17 -0
  14. package/bin/chat/access/ChatAccessRuntime.d.ts.map +1 -0
  15. package/bin/chat/access/ChatAccessRuntime.js +31 -0
  16. package/bin/chat/access/ChatAccessRuntime.js.map +1 -0
  17. package/bin/chat/access/ChatAccessSchema.d.ts +16 -0
  18. package/bin/chat/access/ChatAccessSchema.d.ts.map +1 -0
  19. package/bin/chat/access/ChatAccessSchema.js +99 -0
  20. package/bin/chat/access/ChatAccessSchema.js.map +1 -0
  21. package/bin/chat/access/ChatAccessService.d.ts +44 -0
  22. package/bin/chat/access/ChatAccessService.d.ts.map +1 -0
  23. package/bin/chat/access/ChatAccessService.js +281 -0
  24. package/bin/chat/access/ChatAccessService.js.map +1 -0
  25. package/bin/chat/access/ChatAccessStore.d.ts +67 -0
  26. package/bin/chat/access/ChatAccessStore.d.ts.map +1 -0
  27. package/bin/chat/access/ChatAccessStore.js +367 -0
  28. package/bin/chat/access/ChatAccessStore.js.map +1 -0
  29. package/bin/chat/accounts/Crypto.d.ts +1 -1
  30. package/bin/chat/accounts/Crypto.js +1 -1
  31. package/bin/chat/accounts/Store.d.ts +1 -1
  32. package/bin/chat/accounts/Store.js +1 -1
  33. package/bin/chat/channels/BaseChatChannel.d.ts +18 -21
  34. package/bin/chat/channels/BaseChatChannel.d.ts.map +1 -1
  35. package/bin/chat/channels/BaseChatChannel.js +48 -58
  36. package/bin/chat/channels/BaseChatChannel.js.map +1 -1
  37. package/bin/chat/channels/BaseChatChannelQueue.d.ts +1 -1
  38. package/bin/chat/channels/BaseChatChannelQueue.d.ts.map +1 -1
  39. package/bin/chat/channels/BaseChatChannelQueue.js +2 -14
  40. package/bin/chat/channels/BaseChatChannelQueue.js.map +1 -1
  41. package/bin/chat/channels/Configuration.d.ts +2 -2
  42. package/bin/chat/channels/Configuration.d.ts.map +1 -1
  43. package/bin/chat/channels/Configuration.js +3 -3
  44. package/bin/chat/channels/Configuration.js.map +1 -1
  45. package/bin/chat/channels/feishu/Configuration.js +4 -4
  46. package/bin/chat/channels/feishu/Configuration.js.map +1 -1
  47. package/bin/chat/channels/feishu/Feishu.d.ts +2 -0
  48. package/bin/chat/channels/feishu/Feishu.d.ts.map +1 -1
  49. package/bin/chat/channels/feishu/Feishu.js +8 -7
  50. package/bin/chat/channels/feishu/Feishu.js.map +1 -1
  51. package/bin/chat/channels/feishu/FeishuMessageHandler.d.ts +9 -17
  52. package/bin/chat/channels/feishu/FeishuMessageHandler.d.ts.map +1 -1
  53. package/bin/chat/channels/feishu/FeishuMessageHandler.js +6 -17
  54. package/bin/chat/channels/feishu/FeishuMessageHandler.js.map +1 -1
  55. package/bin/chat/channels/qq/Configuration.js +4 -4
  56. package/bin/chat/channels/qq/Configuration.js.map +1 -1
  57. package/bin/chat/channels/qq/QQ.d.ts +2 -0
  58. package/bin/chat/channels/qq/QQ.d.ts.map +1 -1
  59. package/bin/chat/channels/qq/QQ.js +8 -7
  60. package/bin/chat/channels/qq/QQ.js.map +1 -1
  61. package/bin/chat/channels/qq/QQMessageHandler.d.ts +9 -17
  62. package/bin/chat/channels/qq/QQMessageHandler.d.ts.map +1 -1
  63. package/bin/chat/channels/qq/QQMessageHandler.js +7 -25
  64. package/bin/chat/channels/qq/QQMessageHandler.js.map +1 -1
  65. package/bin/chat/channels/telegram/Bot.d.ts +2 -0
  66. package/bin/chat/channels/telegram/Bot.d.ts.map +1 -1
  67. package/bin/chat/channels/telegram/Bot.js +8 -7
  68. package/bin/chat/channels/telegram/Bot.js.map +1 -1
  69. package/bin/chat/channels/telegram/Configuration.js +4 -4
  70. package/bin/chat/channels/telegram/Configuration.js.map +1 -1
  71. package/bin/chat/channels/telegram/TelegramMessageHandler.d.ts +9 -17
  72. package/bin/chat/channels/telegram/TelegramMessageHandler.d.ts.map +1 -1
  73. package/bin/chat/channels/telegram/TelegramMessageHandler.js +5 -16
  74. package/bin/chat/channels/telegram/TelegramMessageHandler.js.map +1 -1
  75. package/bin/chat/runtime/ChatChannelActions.d.ts +3 -2
  76. package/bin/chat/runtime/ChatChannelActions.d.ts.map +1 -1
  77. package/bin/chat/runtime/ChatChannelActions.js +57 -0
  78. package/bin/chat/runtime/ChatChannelActions.js.map +1 -1
  79. package/bin/chat/runtime/ChatChannelConfig.d.ts +2 -2
  80. package/bin/chat/runtime/ChatChannelConfig.js +3 -3
  81. package/bin/chat/runtime/ChatChannelConfig.js.map +1 -1
  82. package/bin/chat/runtime/ChatChannelCore.d.ts +2 -2
  83. package/bin/chat/runtime/ChatChannelCore.js +1 -1
  84. package/bin/chat/runtime/ChatChannelLifecycle.d.ts +1 -1
  85. package/bin/chat/runtime/ChatChannelLifecycle.js +1 -1
  86. package/bin/chat/runtime/PluginPoints.d.ts +0 -24
  87. package/bin/chat/runtime/PluginPoints.d.ts.map +1 -1
  88. package/bin/chat/runtime/PluginPoints.js +0 -24
  89. package/bin/chat/runtime/PluginPoints.js.map +1 -1
  90. package/bin/chat/runtime/QueuedUserMessage.d.ts.map +1 -1
  91. package/bin/chat/runtime/QueuedUserMessage.js +0 -2
  92. package/bin/chat/runtime/QueuedUserMessage.js.map +1 -1
  93. package/bin/chat/types/ChannelConfiguration.d.ts +4 -4
  94. package/bin/chat/types/ChannelConfiguration.d.ts.map +1 -1
  95. package/bin/chat/types/ChatAccess.d.ts +300 -0
  96. package/bin/chat/types/ChatAccess.d.ts.map +1 -0
  97. package/bin/chat/types/ChatAccess.js +22 -0
  98. package/bin/chat/types/ChatAccess.js.map +1 -0
  99. package/bin/chat/types/ChatPromptContext.d.ts +0 -15
  100. package/bin/chat/types/ChatPromptContext.d.ts.map +1 -1
  101. package/bin/index.d.ts +4 -4
  102. package/bin/index.d.ts.map +1 -1
  103. package/bin/index.js +3 -3
  104. package/bin/index.js.map +1 -1
  105. package/bin/skill/Config.d.ts +1 -1
  106. package/bin/skill/Config.js +1 -1
  107. package/bin/skill/runtime/Paths.d.ts +1 -1
  108. package/bin/skill/runtime/Paths.js +1 -1
  109. package/package.json +6 -4
  110. package/scripts/chat-access.test.mjs +90 -0
  111. package/scripts/chat-channel-config.test.mjs +82 -0
  112. package/src/chat/ChatPlugin.ts +4 -24
  113. package/src/chat/PROMPT.direct.ts +1 -1
  114. package/src/chat/PROMPT.direct.ts.txt +1 -1
  115. package/src/chat/access/ChatAccessActions.ts +159 -0
  116. package/src/chat/access/ChatAccessRuntime.ts +41 -0
  117. package/src/chat/access/ChatAccessSchema.ts +104 -0
  118. package/src/chat/access/ChatAccessService.ts +311 -0
  119. package/src/chat/access/ChatAccessStore.ts +462 -0
  120. package/src/chat/accounts/Crypto.ts +1 -1
  121. package/src/chat/accounts/Store.ts +1 -1
  122. package/src/chat/channels/BaseChatChannel.ts +59 -80
  123. package/src/chat/channels/BaseChatChannelQueue.ts +2 -14
  124. package/src/chat/channels/Configuration.ts +3 -3
  125. package/src/chat/channels/feishu/Configuration.ts +4 -4
  126. package/src/chat/channels/feishu/Feishu.ts +11 -9
  127. package/src/chat/channels/feishu/FeishuMessageHandler.ts +16 -40
  128. package/src/chat/channels/qq/Configuration.ts +4 -4
  129. package/src/chat/channels/qq/QQ.ts +11 -9
  130. package/src/chat/channels/qq/QQMessageHandler.ts +17 -49
  131. package/src/chat/channels/telegram/Bot.ts +11 -9
  132. package/src/chat/channels/telegram/Configuration.ts +4 -4
  133. package/src/chat/channels/telegram/TelegramMessageHandler.ts +15 -39
  134. package/src/chat/runtime/ChatChannelActions.ts +72 -1
  135. package/src/chat/runtime/ChatChannelConfig.ts +3 -3
  136. package/src/chat/runtime/ChatChannelCore.ts +1 -1
  137. package/src/chat/runtime/ChatChannelLifecycle.ts +1 -1
  138. package/src/chat/runtime/PluginPoints.ts +0 -24
  139. package/src/chat/runtime/QueuedUserMessage.ts +0 -2
  140. package/src/chat/types/ChannelConfiguration.ts +4 -4
  141. package/src/chat/types/ChatAccess.ts +335 -0
  142. package/src/chat/types/ChatPromptContext.ts +0 -17
  143. package/src/index.ts +27 -31
  144. package/src/skill/Config.ts +1 -1
  145. package/src/skill/runtime/Paths.ts +1 -1
  146. package/bin/auth/runtime/AuthorizationConfig.d.ts +0 -41
  147. package/bin/auth/runtime/AuthorizationConfig.d.ts.map +0 -1
  148. package/bin/auth/runtime/AuthorizationConfig.js +0 -185
  149. package/bin/auth/runtime/AuthorizationConfig.js.map +0 -1
  150. package/bin/auth/runtime/AuthorizationPolicy.d.ts +0 -28
  151. package/bin/auth/runtime/AuthorizationPolicy.d.ts.map +0 -1
  152. package/bin/auth/runtime/AuthorizationPolicy.js +0 -124
  153. package/bin/auth/runtime/AuthorizationPolicy.js.map +0 -1
  154. package/bin/auth/runtime/AuthorizationStore.d.ts +0 -29
  155. package/bin/auth/runtime/AuthorizationStore.d.ts.map +0 -1
  156. package/bin/auth/runtime/AuthorizationStore.js +0 -179
  157. package/bin/auth/runtime/AuthorizationStore.js.map +0 -1
  158. package/bin/auth/types/AuthPlugin.d.ts +0 -403
  159. package/bin/auth/types/AuthPlugin.d.ts.map +0 -1
  160. package/bin/auth/types/AuthPlugin.js +0 -111
  161. package/bin/auth/types/AuthPlugin.js.map +0 -1
  162. package/bin/chat/runtime/ChatAuthorizationRuntime.d.ts +0 -22
  163. package/bin/chat/runtime/ChatAuthorizationRuntime.d.ts.map +0 -1
  164. package/bin/chat/runtime/ChatAuthorizationRuntime.js +0 -230
  165. package/bin/chat/runtime/ChatAuthorizationRuntime.js.map +0 -1
  166. package/bin/chat/runtime/PluginDispatch.d.ts +0 -43
  167. package/bin/chat/runtime/PluginDispatch.d.ts.map +0 -1
  168. package/bin/chat/runtime/PluginDispatch.js +0 -57
  169. package/bin/chat/runtime/PluginDispatch.js.map +0 -1
  170. package/src/auth/runtime/AuthorizationConfig.ts +0 -247
  171. package/src/auth/runtime/AuthorizationPolicy.ts +0 -167
  172. package/src/auth/runtime/AuthorizationStore.ts +0 -220
  173. package/src/auth/types/AuthPlugin.ts +0 -540
  174. package/src/chat/runtime/ChatAuthorizationRuntime.ts +0 -270
  175. package/src/chat/runtime/PluginDispatch.ts +0 -89
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Chat Access SQLite Store。
3
+ *
4
+ * 关键点(中文)
5
+ * - 该 Store 是当前 Agent Chat Access 的唯一持久化入口。
6
+ * - 所有字段先归一化再写入,避免平台适配器差异污染查询键。
7
+ * - Service 负责业务编排,Store 只负责结构化读写和事务。
8
+ */
9
+
10
+ import Database from "better-sqlite3";
11
+ import fs from "fs-extra";
12
+ import path from "node:path";
13
+ import { generateId } from "@downcity/agent";
14
+ import { ensure_chat_access_schema } from "@/chat/access/ChatAccessSchema.js";
15
+ import type {
16
+ ChatAccessEffect,
17
+ ChatAccessGrant,
18
+ ChatAccessPrincipal,
19
+ ChatAccessPrincipalView,
20
+ ChatAccessRequest,
21
+ ChatAccessRequestStatus,
22
+ ChatAccessRequestView,
23
+ ChatAccessScope,
24
+ CreateChatAccessRequestInput,
25
+ CreateChatAccessRequestResult,
26
+ InsertChatAccessAuditEventInput,
27
+ ResolveChatAccessRequestStoreInput,
28
+ ResolvePendingChatAccessRequestsInput,
29
+ UpsertChatAccessGrantInput,
30
+ UpsertChatAccessPrincipalInput,
31
+ } from "@/chat/types/ChatAccess.js";
32
+ import type { ChatDispatchChannel } from "@/chat/types/ChatDispatcher.js";
33
+
34
+ type SqlRow = Record<string, unknown>;
35
+
36
+ /**
37
+ * 返回项目级 Chat Access 数据库路径。
38
+ */
39
+ export function get_chat_access_db_path(project_root: string): string {
40
+ return path.join(path.resolve(project_root), ".downcity", "chat", "access.db");
41
+ }
42
+
43
+ function normalize_text(value: unknown): string {
44
+ return String(value || "").trim();
45
+ }
46
+
47
+ function optional_text(value: unknown): string | undefined {
48
+ const text = normalize_text(value);
49
+ return text || undefined;
50
+ }
51
+
52
+ function now_iso(): string {
53
+ return new Date().toISOString();
54
+ }
55
+
56
+ function to_principal(row: SqlRow | undefined): ChatAccessPrincipal | null {
57
+ if (!row) return null;
58
+ const principal_id = normalize_text(row.principal_id);
59
+ const channel = normalize_text(row.channel) as ChatDispatchChannel;
60
+ const issuer = normalize_text(row.issuer);
61
+ const subject_id = normalize_text(row.subject_id);
62
+ if (!principal_id || !channel || !issuer || !subject_id) return null;
63
+ return {
64
+ principal_id,
65
+ channel,
66
+ issuer,
67
+ subject_id,
68
+ ...(optional_text(row.display_name)
69
+ ? { display_name: optional_text(row.display_name) }
70
+ : {}),
71
+ first_seen_at: normalize_text(row.first_seen_at),
72
+ last_seen_at: normalize_text(row.last_seen_at),
73
+ ...(optional_text(row.last_chat_id)
74
+ ? { last_chat_id: optional_text(row.last_chat_id) }
75
+ : {}),
76
+ ...(optional_text(row.last_chat_type)
77
+ ? { last_chat_type: optional_text(row.last_chat_type) }
78
+ : {}),
79
+ };
80
+ }
81
+
82
+ function to_grant(row: SqlRow | undefined): ChatAccessGrant | null {
83
+ if (!row) return null;
84
+ const grant_id = normalize_text(row.grant_id);
85
+ const principal_id = normalize_text(row.principal_id);
86
+ const scope = normalize_text(row.scope) as ChatAccessScope;
87
+ const effect = normalize_text(row.effect) as ChatAccessEffect;
88
+ if (!grant_id || !principal_id || !scope || !effect) return null;
89
+ return {
90
+ grant_id,
91
+ principal_id,
92
+ scope,
93
+ effect,
94
+ created_by: normalize_text(row.created_by),
95
+ created_at: normalize_text(row.created_at),
96
+ updated_at: normalize_text(row.updated_at),
97
+ };
98
+ }
99
+
100
+ function to_request(row: SqlRow | undefined): ChatAccessRequest | null {
101
+ if (!row) return null;
102
+ const request_id = normalize_text(row.request_id);
103
+ const principal_id = normalize_text(row.principal_id);
104
+ const scope = normalize_text(row.scope) as ChatAccessScope;
105
+ const status = normalize_text(row.status) as ChatAccessRequestStatus;
106
+ if (!request_id || !principal_id || !scope || !status) return null;
107
+ return {
108
+ request_id,
109
+ principal_id,
110
+ scope,
111
+ chat_id: normalize_text(row.chat_id),
112
+ chat_type: normalize_text(row.chat_type),
113
+ status,
114
+ ...(optional_text(row.resolved_by)
115
+ ? { resolved_by: optional_text(row.resolved_by) }
116
+ : {}),
117
+ created_at: normalize_text(row.created_at),
118
+ last_requested_at: normalize_text(row.last_requested_at),
119
+ ...(optional_text(row.resolved_at)
120
+ ? { resolved_at: optional_text(row.resolved_at) }
121
+ : {}),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * 当前 Agent 的 Chat Access SQLite Store。
127
+ */
128
+ export class ChatAccessStore {
129
+ private readonly database: Database.Database;
130
+
131
+ /**
132
+ * 打开当前项目 Chat Access 数据库。
133
+ */
134
+ constructor(project_root: string) {
135
+ const database_path = get_chat_access_db_path(project_root);
136
+ fs.ensureDirSync(path.dirname(database_path), { mode: 0o700 });
137
+ this.database = new Database(database_path);
138
+ ensure_chat_access_schema(this.database);
139
+ try {
140
+ fs.chmodSync(database_path, 0o600);
141
+ } catch {
142
+ // 某些平台不支持 POSIX 权限,SQLite 仍可正常工作。
143
+ }
144
+ }
145
+
146
+ /** 关闭数据库连接。 */
147
+ close(): void {
148
+ this.database.close();
149
+ }
150
+
151
+ /** 在单个 SQLite 事务中执行操作。 */
152
+ transaction<T>(operation: () => T): T {
153
+ return this.database.transaction(operation)();
154
+ }
155
+
156
+ /** 读取 Store 元信息。 */
157
+ get_meta(key: string): string | null {
158
+ const row = this.database
159
+ .prepare("SELECT value FROM chat_access_meta WHERE key = ? LIMIT 1")
160
+ .get(normalize_text(key)) as SqlRow | undefined;
161
+ return row ? normalize_text(row.value) : null;
162
+ }
163
+
164
+ /** 写入 Store 元信息。 */
165
+ set_meta(key: string, value: string): void {
166
+ const current_time = now_iso();
167
+ this.database.prepare(`
168
+ INSERT INTO chat_access_meta (key, value, updated_at)
169
+ VALUES (?, ?, ?)
170
+ ON CONFLICT(key) DO UPDATE SET
171
+ value = excluded.value,
172
+ updated_at = excluded.updated_at
173
+ `).run(normalize_text(key), normalize_text(value), current_time);
174
+ }
175
+
176
+ /** 判断是否已经存在业务数据。 */
177
+ has_access_data(): boolean {
178
+ const row = this.database.prepare(`
179
+ SELECT
180
+ (SELECT COUNT(*) FROM chat_access_principals) +
181
+ (SELECT COUNT(*) FROM chat_access_grants) +
182
+ (SELECT COUNT(*) FROM chat_access_requests) AS total
183
+ `).get() as { total?: unknown } | undefined;
184
+ return Number(row?.total || 0) > 0;
185
+ }
186
+
187
+ /** 读取指定平台身份对应主体。 */
188
+ get_principal_by_identity(input: {
189
+ channel: ChatDispatchChannel;
190
+ issuer: string;
191
+ subject_id: string;
192
+ }): ChatAccessPrincipal | null {
193
+ const row = this.database.prepare(`
194
+ SELECT * FROM chat_access_principals
195
+ WHERE channel = ? AND issuer = ? AND subject_id = ?
196
+ LIMIT 1
197
+ `).get(
198
+ input.channel,
199
+ normalize_text(input.issuer),
200
+ normalize_text(input.subject_id),
201
+ ) as SqlRow | undefined;
202
+ return to_principal(row);
203
+ }
204
+
205
+ /** 按 Principal ID 读取主体。 */
206
+ get_principal(principal_id: string): ChatAccessPrincipal | null {
207
+ const row = this.database.prepare(`
208
+ SELECT * FROM chat_access_principals
209
+ WHERE principal_id = ?
210
+ LIMIT 1
211
+ `).get(normalize_text(principal_id)) as SqlRow | undefined;
212
+ return to_principal(row);
213
+ }
214
+
215
+ /** 新增或更新观测主体。 */
216
+ upsert_principal(input: UpsertChatAccessPrincipalInput): ChatAccessPrincipal {
217
+ const existing = this.get_principal_by_identity(input);
218
+ const current_time = now_iso();
219
+ const principal_id = existing?.principal_id || `principal_${generateId()}`;
220
+ const first_seen_at = normalize_text(input.first_seen_at) || existing?.first_seen_at || current_time;
221
+ const last_seen_at = normalize_text(input.last_seen_at) || current_time;
222
+ this.database.prepare(`
223
+ INSERT INTO chat_access_principals (
224
+ principal_id, channel, issuer, subject_id, display_name,
225
+ first_seen_at, last_seen_at, last_chat_id, last_chat_type
226
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
227
+ ON CONFLICT(channel, issuer, subject_id) DO UPDATE SET
228
+ display_name = COALESCE(excluded.display_name, chat_access_principals.display_name),
229
+ last_seen_at = excluded.last_seen_at,
230
+ last_chat_id = COALESCE(excluded.last_chat_id, chat_access_principals.last_chat_id),
231
+ last_chat_type = COALESCE(excluded.last_chat_type, chat_access_principals.last_chat_type)
232
+ `).run(
233
+ principal_id,
234
+ input.channel,
235
+ normalize_text(input.issuer),
236
+ normalize_text(input.subject_id),
237
+ optional_text(input.display_name) || null,
238
+ first_seen_at,
239
+ last_seen_at,
240
+ optional_text(input.chat_id) || null,
241
+ optional_text(input.chat_type) || null,
242
+ );
243
+ const principal = this.get_principal_by_identity(input);
244
+ if (!principal) throw new Error("Failed to persist Chat Access principal");
245
+ return principal;
246
+ }
247
+
248
+ /** 读取指定主体和范围的 Grant。 */
249
+ get_grant(principal_id: string, scope: ChatAccessScope): ChatAccessGrant | null {
250
+ const row = this.database.prepare(`
251
+ SELECT * FROM chat_access_grants
252
+ WHERE principal_id = ? AND scope = ?
253
+ LIMIT 1
254
+ `).get(normalize_text(principal_id), scope) as SqlRow | undefined;
255
+ return to_grant(row);
256
+ }
257
+
258
+ /** 新增或更新 Grant。 */
259
+ upsert_grant(input: UpsertChatAccessGrantInput): ChatAccessGrant {
260
+ const existing = this.get_grant(input.principal_id, input.scope);
261
+ const current_time = now_iso();
262
+ const grant_id = existing?.grant_id || `grant_${generateId()}`;
263
+ this.database.prepare(`
264
+ INSERT INTO chat_access_grants (
265
+ grant_id, principal_id, scope, effect, created_by, created_at, updated_at
266
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
267
+ ON CONFLICT(principal_id, scope) DO UPDATE SET
268
+ effect = excluded.effect,
269
+ created_by = excluded.created_by,
270
+ updated_at = excluded.updated_at
271
+ `).run(
272
+ grant_id,
273
+ normalize_text(input.principal_id),
274
+ input.scope,
275
+ input.effect,
276
+ normalize_text(input.operator) || "unknown",
277
+ existing?.created_at || current_time,
278
+ current_time,
279
+ );
280
+ const grant = this.get_grant(input.principal_id, input.scope);
281
+ if (!grant) throw new Error("Failed to persist Chat Access grant");
282
+ return grant;
283
+ }
284
+
285
+ /** 删除指定主体和范围的 Grant。 */
286
+ delete_grant(principal_id: string, scope: ChatAccessScope): boolean {
287
+ const result = this.database.prepare(`
288
+ DELETE FROM chat_access_grants
289
+ WHERE principal_id = ? AND scope = ?
290
+ `).run(normalize_text(principal_id), scope);
291
+ return result.changes > 0;
292
+ }
293
+
294
+ /** 读取指定主体全部 Grant。 */
295
+ list_grants(principal_id: string): ChatAccessGrant[] {
296
+ const rows = this.database.prepare(`
297
+ SELECT * FROM chat_access_grants
298
+ WHERE principal_id = ?
299
+ ORDER BY scope ASC
300
+ `).all(normalize_text(principal_id)) as SqlRow[];
301
+ return rows.map((row) => to_grant(row)).filter((item): item is ChatAccessGrant => !!item);
302
+ }
303
+
304
+ /** 读取 Request。 */
305
+ get_request(request_id: string): ChatAccessRequest | null {
306
+ const row = this.database.prepare(`
307
+ SELECT * FROM chat_access_requests
308
+ WHERE request_id = ?
309
+ LIMIT 1
310
+ `).get(normalize_text(request_id)) as SqlRow | undefined;
311
+ return to_request(row);
312
+ }
313
+
314
+ /** 创建或复用 pending Request。 */
315
+ create_or_touch_request(input: CreateChatAccessRequestInput): CreateChatAccessRequestResult {
316
+ const pending_row = this.database.prepare(`
317
+ SELECT * FROM chat_access_requests
318
+ WHERE principal_id = ? AND scope = ? AND status = 'pending'
319
+ LIMIT 1
320
+ `).get(normalize_text(input.principal_id), input.scope) as SqlRow | undefined;
321
+ const pending = to_request(pending_row);
322
+ const current_time = now_iso();
323
+ if (pending) {
324
+ this.database.prepare(`
325
+ UPDATE chat_access_requests
326
+ SET chat_id = ?, chat_type = ?, last_requested_at = ?
327
+ WHERE request_id = ?
328
+ `).run(
329
+ normalize_text(input.chat_id),
330
+ normalize_text(input.chat_type),
331
+ current_time,
332
+ pending.request_id,
333
+ );
334
+ const updated = this.get_request(pending.request_id);
335
+ if (!updated) throw new Error("Failed to update Chat Access request");
336
+ return { request: updated, created: false };
337
+ }
338
+
339
+ const request_id = `req_${generateId()}`;
340
+ this.database.prepare(`
341
+ INSERT INTO chat_access_requests (
342
+ request_id, principal_id, scope, chat_id, chat_type, status,
343
+ created_at, last_requested_at
344
+ ) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)
345
+ `).run(
346
+ request_id,
347
+ normalize_text(input.principal_id),
348
+ input.scope,
349
+ normalize_text(input.chat_id),
350
+ normalize_text(input.chat_type),
351
+ current_time,
352
+ current_time,
353
+ );
354
+ const request = this.get_request(request_id);
355
+ if (!request) throw new Error("Failed to create Chat Access request");
356
+ return { request, created: true };
357
+ }
358
+
359
+ /** 解决指定 Request。 */
360
+ resolve_request(input: ResolveChatAccessRequestStoreInput): ChatAccessRequest {
361
+ const current_time = now_iso();
362
+ const result = this.database.prepare(`
363
+ UPDATE chat_access_requests
364
+ SET status = ?, resolved_by = ?, resolved_at = ?, last_requested_at = ?
365
+ WHERE request_id = ? AND status = 'pending'
366
+ `).run(
367
+ input.status,
368
+ normalize_text(input.operator) || "unknown",
369
+ current_time,
370
+ current_time,
371
+ normalize_text(input.request_id),
372
+ );
373
+ if (result.changes === 0) {
374
+ throw new Error(`Pending Chat Access request not found: ${input.request_id}`);
375
+ }
376
+ const request = this.get_request(input.request_id);
377
+ if (!request) throw new Error(`Chat Access request not found: ${input.request_id}`);
378
+ return request;
379
+ }
380
+
381
+ /** 解决指定主体和范围内的全部 pending Request。 */
382
+ resolve_pending_requests(input: ResolvePendingChatAccessRequestsInput): void {
383
+ if (input.scopes.length === 0) return;
384
+ const current_time = now_iso();
385
+ const placeholders = input.scopes.map(() => "?").join(", ");
386
+ this.database.prepare(`
387
+ UPDATE chat_access_requests
388
+ SET status = ?, resolved_by = ?, resolved_at = ?, last_requested_at = ?
389
+ WHERE principal_id = ?
390
+ AND status = 'pending'
391
+ AND scope IN (${placeholders})
392
+ `).run(
393
+ input.status,
394
+ normalize_text(input.operator) || "unknown",
395
+ current_time,
396
+ current_time,
397
+ normalize_text(input.principal_id),
398
+ ...input.scopes,
399
+ );
400
+ }
401
+
402
+ /** 列出 Request 及主体详情。 */
403
+ list_requests(status?: ChatAccessRequestStatus): ChatAccessRequestView[] {
404
+ const rows = status
405
+ ? this.database.prepare(`
406
+ SELECT * FROM chat_access_requests
407
+ WHERE status = ?
408
+ ORDER BY last_requested_at DESC
409
+ `).all(status) as SqlRow[]
410
+ : this.database.prepare(`
411
+ SELECT * FROM chat_access_requests
412
+ ORDER BY last_requested_at DESC
413
+ `).all() as SqlRow[];
414
+ const views: ChatAccessRequestView[] = [];
415
+ for (const row of rows) {
416
+ const request = to_request(row);
417
+ if (!request) continue;
418
+ const principal = this.get_principal(request.principal_id);
419
+ if (!principal) continue;
420
+ views.push({ ...request, principal });
421
+ }
422
+ return views;
423
+ }
424
+
425
+ /** 列出所有主体和 Grant。 */
426
+ list_principals(): ChatAccessPrincipalView[] {
427
+ const rows = this.database.prepare(`
428
+ SELECT * FROM chat_access_principals
429
+ ORDER BY last_seen_at DESC
430
+ `).all() as SqlRow[];
431
+ const views: ChatAccessPrincipalView[] = [];
432
+ for (const row of rows) {
433
+ const principal = to_principal(row);
434
+ if (!principal) continue;
435
+ views.push({
436
+ principal,
437
+ grants: this.list_grants(principal.principal_id),
438
+ });
439
+ }
440
+ return views;
441
+ }
442
+
443
+ /** 写入审计事件。 */
444
+ insert_audit_event(input: InsertChatAccessAuditEventInput): void {
445
+ this.database.prepare(`
446
+ INSERT INTO chat_access_audit_events (
447
+ event_id, principal_id, request_id, action, scope,
448
+ decision, operator, detail_json, created_at
449
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
450
+ `).run(
451
+ `audit_${generateId()}`,
452
+ optional_text(input.principal_id) || null,
453
+ optional_text(input.request_id) || null,
454
+ normalize_text(input.action),
455
+ input.scope || null,
456
+ optional_text(input.decision) || null,
457
+ optional_text(input.operator) || null,
458
+ input.detail ? JSON.stringify(input.detail) : null,
459
+ now_iso(),
460
+ );
461
+ }
462
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 职责说明(中文)
5
5
  * - 负责默认 chat account 存储中的敏感字段加解密。
6
- * - 默认复用平台级 `model-db.key`,保证和现有 `~/.downcity/downcity.db` 数据兼容。
6
+ * - 复用 `DC_PLATFORM_ROOT` 下的平台级 `model-db.key`。
7
7
  *
8
8
  * 边界说明(中文)
9
9
  * - 这里只处理“字符串 <-> 密文”的转换,不负责数据库读写。
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 职责说明(中文)
5
5
  * - 提供 `chat` plugin 默认使用的全局 chat account 读写能力。
6
- * - 默认直接操作 `~/.downcity/downcity.db` 里的 `channel_accounts` 表。
6
+ * - 直接操作 `DC_PLATFORM_ROOT` 下 `downcity.db` `channel_accounts` 表。
7
7
  * - 对 `city` 而言,这里也是统一的默认账号池实现来源。
8
8
  *
9
9
  * 边界说明(中文)
@@ -4,7 +4,7 @@
4
4
  * 关键点(中文)
5
5
  * - 通过 context 显式注入 runtime 依赖。
6
6
  * - 统一注册 dispatcher,暴露 sendText/sendAction 能力。
7
- * - 基类只保留授权、工具发送与入站编排;存储/队列细节已下沉到辅助模块。
7
+ * - 基类只保留 Chat Access、工具发送与入站编排;存储/队列细节已下沉到辅助模块。
8
8
  */
9
9
 
10
10
  import { registerChatSender } from "@/chat/runtime/ChatSendRegistry.js";
@@ -19,9 +19,10 @@ import type { AgentContext } from "@downcity/agent";
19
19
  import { resolveChatQueueStore } from "@/chat/runtime/ChatQueue.js";
20
20
  import { deleteChatSessionById } from "@/chat/runtime/ChatSessionDelete.js";
21
21
  import {
22
- guardIncomingChat,
23
- observeIncomingChatPrincipal,
24
- } from "@/chat/runtime/PluginDispatch.js";
22
+ create_chat_access_service,
23
+ resolve_chat_access_issuer,
24
+ } from "@/chat/access/ChatAccessRuntime.js";
25
+ import type { ChatAccessDecision } from "@/chat/types/ChatAccess.js";
25
26
  import {
26
27
  appendToolOutboundChannelHistory,
27
28
  resolveChannelSessionId,
@@ -108,9 +109,9 @@ export type IncomingChatMessage = {
108
109
  };
109
110
 
110
111
  /**
111
- * 入站授权判定输入。
112
+ * 入站 Chat Access 判定输入。
112
113
  */
113
- export type IncomingAuthorizationParams = {
114
+ export type IncomingChatAccessParams = {
114
115
  chatId: string;
115
116
  chatType?: string;
116
117
  userId?: string;
@@ -119,12 +120,9 @@ export type IncomingAuthorizationParams = {
119
120
  };
120
121
 
121
122
  /**
122
- * 入站授权判定结果。
123
+ * 入站 Chat Access 判定结果。
123
124
  */
124
- export type IncomingAuthorizationResult = {
125
- decision: "allow" | "block";
126
- reason: string;
127
- };
125
+ export type IncomingChatAccessResult = ChatAccessDecision;
128
126
 
129
127
  /**
130
128
  * Chat channel 基类。
@@ -134,6 +132,7 @@ export abstract class BaseChatChannel {
134
132
  protected readonly context: AgentContext;
135
133
  protected readonly rootPath: string;
136
134
  protected readonly logger: Logger;
135
+ private readonly access_notice_sent_at = new Map<string, number>();
137
136
 
138
137
  protected constructor(params: {
139
138
  channel: ChatDispatchChannel;
@@ -159,22 +158,35 @@ export abstract class BaseChatChannel {
159
158
  params: ChannelSendTextParams,
160
159
  ): Promise<void>;
161
160
 
161
+ /**
162
+ * SDK 未绑定 City Chat Account 时的平台稳定 issuer 回退值。
163
+ */
164
+ protected getAccessIssuerFallback(): string {
165
+ return "";
166
+ }
167
+
162
168
  protected sendActionToPlatform?(
163
169
  params: ChannelSendActionParams,
164
170
  ): Promise<void>;
165
171
 
166
172
  /**
167
- * 发送授权提示文本。
173
+ * 发送 Chat Access 提示文本。
168
174
  *
169
175
  * 关键点(中文)
170
176
  * - 一律按普通消息发送,不挂 reply,避免把“权限提示”误挂到某条消息下面。
171
177
  */
172
- protected async sendAuthorizationText(params: {
178
+ protected async sendAccessText(params: {
173
179
  chatId: string;
174
180
  text: string;
175
181
  chatType?: string;
176
182
  messageThreadId?: number;
177
183
  }): Promise<void> {
184
+ const notice_key = `${params.chatId}:${params.text}`;
185
+ const current_time = Date.now();
186
+ const last_sent_at = this.access_notice_sent_at.get(notice_key) || 0;
187
+ if (current_time - last_sent_at < 60_000) return;
188
+ if (this.access_notice_sent_at.size >= 1_000) this.access_notice_sent_at.clear();
189
+ this.access_notice_sent_at.set(notice_key, current_time);
178
190
  await this.sendTextToPlatform({
179
191
  chatId: params.chatId,
180
192
  text: params.text,
@@ -186,83 +198,50 @@ export abstract class BaseChatChannel {
186
198
  }
187
199
 
188
200
  /**
189
- * 记录入站观测主体。
201
+ * 执行入站 Chat Access 判定。
190
202
  */
191
- protected async observeIncomingAuthorization(
192
- params: IncomingAuthorizationParams,
193
- ): Promise<void> {
194
- await observeIncomingChatPrincipal({
195
- context: this.context,
203
+ protected async evaluateIncomingAccess(
204
+ params: IncomingChatAccessParams,
205
+ ): Promise<IncomingChatAccessResult> {
206
+ const issuer =
207
+ resolve_chat_access_issuer(this.context, this.channel) ||
208
+ String(this.getAccessIssuerFallback() || "").trim();
209
+ return create_chat_access_service(this.context).evaluate({
196
210
  channel: this.channel,
197
- chatId: params.chatId,
198
- chatType: params.chatType,
199
- chatTitle: params.chatTitle,
200
- userId: params.userId,
201
- username: params.username,
211
+ issuer,
212
+ subject_id: String(params.userId || "").trim(),
213
+ display_name: String(params.username || "").trim() || undefined,
214
+ chat_id: String(params.chatId || "").trim(),
215
+ chat_type: String(params.chatType || "").trim() || undefined,
216
+ chat_title: String(params.chatTitle || "").trim() || undefined,
202
217
  });
203
218
  }
204
219
 
205
220
  /**
206
- * 执行入站授权判定。
221
+ * 生成 Chat Access 拒绝提示文案。
207
222
  */
208
- protected async evaluateIncomingAuthorization(
209
- params: IncomingAuthorizationParams,
210
- ): Promise<IncomingAuthorizationResult> {
211
- try {
212
- await guardIncomingChat({
213
- context: this.context,
214
- channel: this.channel,
215
- input: {
216
- channel: this.channel,
217
- chatId: params.chatId,
218
- chatType: params.chatType,
219
- userId: params.userId,
220
- username: params.username,
221
- chatTitle: params.chatTitle,
222
- },
223
- });
224
- return {
225
- decision: "allow",
226
- reason: "allow",
227
- };
228
- } catch (error) {
229
- return {
230
- decision: "block",
231
- reason: String(error || "blocked"),
232
- };
233
- }
234
- }
235
-
236
- /**
237
- * 生成未授权提示文案。
238
- */
239
- protected buildUnauthorizedBlockedText(params?: {
240
- userId?: string;
241
- chatId?: string;
242
- chatType?: string;
223
+ protected buildAccessBlockedText(params: {
224
+ result: IncomingChatAccessResult;
243
225
  }): string {
244
- const userId = String(params?.userId || "").trim();
245
- const chatId = String(params?.chatId || "").trim();
246
- const chatType = String(params?.chatType || "").trim();
247
- const lines = [
248
- "当前会话权限不足,已拒绝处理。请联系管理员调整你的角色。",
249
- ];
250
-
251
- if (userId) {
252
- lines.push(
253
- "",
254
- "请把下面命令发给管理员:",
255
- `downcity chat auth set ${this.channel}:${userId}`,
256
- );
226
+ const agent_id = String(this.context.config.id || "agent").trim() || "agent";
227
+ if (params.result.reason === "identity_missing") {
228
+ return "当前平台身份无法识别,请联系管理员检查 Chat 账号配置。";
257
229
  }
258
-
259
- if (chatId || chatType) {
260
- lines.push("", "来源信息:");
261
- if (chatId) lines.push(`chatId: ${chatId}`);
262
- if (chatType) lines.push(`chatType: ${chatType}`);
230
+ if (params.result.reason === "grant_denied") {
231
+ return `当前账号未获准访问 Agent "${agent_id}"。`;
263
232
  }
264
-
265
- return lines.join("\n");
233
+ const request_id = String(params.result.request_id || "").trim();
234
+ if (!request_id) {
235
+ return `当前账号尚未获准访问 Agent "${agent_id}"。`;
236
+ }
237
+ return [
238
+ `当前账号尚未获准访问 Agent "${agent_id}"。`,
239
+ "",
240
+ `访问请求:${request_id}`,
241
+ "",
242
+ "请将下面命令发送给管理员:",
243
+ `downcity chat access approve ${request_id} --agent ${agent_id}`,
244
+ ].join("\n");
266
245
  }
267
246
 
268
247
  /**