@modesto_ma/dsh-channels 0.5.0-beta.5 → 0.5.0-beta.6
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/README.md +25 -8
- package/docs/attachment-pipeline-v2.md +1 -1
- package/docs/feishu-onboarding.md +5 -4
- package/lib/client.js +605 -55
- package/lib/client.js.map +1 -1
- package/lib/index.js +337 -6
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/api.ts +45 -2
- package/src/client/avatar.tsx +36 -0
- package/src/client/icon.tsx +25 -0
- package/src/client/index.ts +78 -23
- package/src/client/origin-cache.ts +45 -0
- package/src/client/origin.tsx +201 -0
- package/src/client/panel/access-tab.tsx +41 -13
- package/src/client/panel/index.tsx +23 -9
- package/src/client/panel/profiles.ts +73 -0
- package/src/client/panel/whitelist.tsx +17 -11
- package/src/client/panel/wiki-tab.tsx +10 -2
- package/src/client/styles.ts +26 -3
- package/src/domain.ts +42 -0
- package/src/feishu/scopes.ts +2 -1
- package/src/feishu-channel.ts +87 -0
- package/src/routing/inbound.ts +13 -5
- package/src/service.ts +146 -3
- package/src/session/routes.ts +45 -4
- package/src/settings/contract.ts +69 -1
- package/src/settings/routes.ts +47 -0
- package/src/settings/surface.ts +22 -0
package/lib/index.js
CHANGED
|
@@ -1337,6 +1337,31 @@ const analyzeRequestRecordSchema = z.object({
|
|
|
1337
1337
|
])
|
|
1338
1338
|
});
|
|
1339
1339
|
/**
|
|
1340
|
+
* Where one Feishu-routed Session came from, keyed by its DSH session id.
|
|
1341
|
+
* Written the first time a route record names the Session (new topic,
|
|
1342
|
+
* `/renew`, analyze API) and kept after the route retires, so the Web
|
|
1343
|
+
* client can still show the origin of an old Session. Names are resolved
|
|
1344
|
+
* live when read, never stored.
|
|
1345
|
+
*/
|
|
1346
|
+
const sessionOriginRecordSchema = z.object({
|
|
1347
|
+
/** DSH session id (also the table key). */
|
|
1348
|
+
key: z.string().min(1),
|
|
1349
|
+
/** Route key the Session was bound under (thread_id or chat:<chatId>). */
|
|
1350
|
+
routeKey: z.string().min(1),
|
|
1351
|
+
/** Topic-scoped (`thread`) or chat-scoped (`chat`) route. */
|
|
1352
|
+
kind: z.enum(["thread", "chat"]),
|
|
1353
|
+
/** Feishu chat the Session was started from. */
|
|
1354
|
+
chatId: z.string().min(1),
|
|
1355
|
+
/** `p2p` or `group`. */
|
|
1356
|
+
chatType: z.enum(["p2p", "group"]),
|
|
1357
|
+
/** `user_id` of the user who started the Session (the requester for analyze topics). */
|
|
1358
|
+
senderUserId: z.string().min(1),
|
|
1359
|
+
/** Epoch ms when the Session was bound to the route. */
|
|
1360
|
+
createdAt: z.number().int().nonnegative(),
|
|
1361
|
+
/** Group-shared route (log-platform analysis topic). */
|
|
1362
|
+
shared: z.literal(true).optional()
|
|
1363
|
+
});
|
|
1364
|
+
/**
|
|
1340
1365
|
* Domain declaration for the host's storage facility. Plain data plus zod
|
|
1341
1366
|
* schemas — deliberately not built with `defineDomain` so this module has no
|
|
1342
1367
|
* runtime import of `@deepseek-ai/dsh-storage-domain`.
|
|
@@ -1364,7 +1389,8 @@ const feishuDomain = {
|
|
|
1364
1389
|
access_users: { valueSchema: accessUserRecordSchema },
|
|
1365
1390
|
access_requests: { valueSchema: accessRequestRecordSchema },
|
|
1366
1391
|
access_wiki_users: { valueSchema: accessUserRecordSchema },
|
|
1367
|
-
analyze_requests: { valueSchema: analyzeRequestRecordSchema }
|
|
1392
|
+
analyze_requests: { valueSchema: analyzeRequestRecordSchema },
|
|
1393
|
+
session_origins: { valueSchema: sessionOriginRecordSchema }
|
|
1368
1394
|
}
|
|
1369
1395
|
};
|
|
1370
1396
|
/**
|
|
@@ -3367,6 +3393,7 @@ var RouteStore = class {
|
|
|
3367
3393
|
this.expired.set(key, record);
|
|
3368
3394
|
this.active.delete(key);
|
|
3369
3395
|
}
|
|
3396
|
+
for (const record of this.active.values()) this.recordOrigins(record);
|
|
3370
3397
|
}
|
|
3371
3398
|
/** One active route by key (cache-first, normalized). */
|
|
3372
3399
|
get(key) {
|
|
@@ -3390,7 +3417,13 @@ var RouteStore = class {
|
|
|
3390
3417
|
/** Persist one active route and refresh the cache. */
|
|
3391
3418
|
async put(key, record) {
|
|
3392
3419
|
await this.deps.domain()?.routes.put(key, record);
|
|
3393
|
-
|
|
3420
|
+
const normalized = normalizeRouteRecord(record);
|
|
3421
|
+
this.active.set(key, normalized);
|
|
3422
|
+
await this.recordOrigins(normalized);
|
|
3423
|
+
}
|
|
3424
|
+
/** Feishu origin of one Session any route ever named, retired routes included. */
|
|
3425
|
+
origin(sessionId) {
|
|
3426
|
+
return this.deps.domain()?.sessionOrigins.get(sessionId);
|
|
3394
3427
|
}
|
|
3395
3428
|
/** Delete one active route from the medium and the cache. */
|
|
3396
3429
|
async remove(key) {
|
|
@@ -3418,6 +3451,33 @@ var RouteStore = class {
|
|
|
3418
3451
|
this.active.clear();
|
|
3419
3452
|
this.expired.clear();
|
|
3420
3453
|
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Write an origin for every Session in the route's history that has none.
|
|
3456
|
+
* Existing origins are never rewritten: the first route naming a Session
|
|
3457
|
+
* is where it started. Failures are logged and never fail the route write.
|
|
3458
|
+
*/
|
|
3459
|
+
async recordOrigins(record) {
|
|
3460
|
+
const table = this.deps.domain()?.sessionOrigins;
|
|
3461
|
+
if (table === void 0) return;
|
|
3462
|
+
for (const entry of normalizeSessionHistory(record)) {
|
|
3463
|
+
if (table.get(entry.sessionId) !== void 0) continue;
|
|
3464
|
+
const origin = {
|
|
3465
|
+
key: entry.sessionId,
|
|
3466
|
+
routeKey: record.key,
|
|
3467
|
+
kind: record.kind,
|
|
3468
|
+
chatId: record.chatId,
|
|
3469
|
+
chatType: record.chatType,
|
|
3470
|
+
senderUserId: record.senderUserId,
|
|
3471
|
+
createdAt: entry.createdAt,
|
|
3472
|
+
...record.shared === true ? { shared: true } : {}
|
|
3473
|
+
};
|
|
3474
|
+
try {
|
|
3475
|
+
await table.put(entry.sessionId, origin);
|
|
3476
|
+
} catch (error) {
|
|
3477
|
+
this.deps.warn("dsh-channels: recording the origin of session %s failed: %s", entry.sessionId, String(error));
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3421
3481
|
/** Whether one active route has crossed the configured inactivity deadline. */
|
|
3422
3482
|
hasExpired(record, now) {
|
|
3423
3483
|
const ttlMs = this.deps.ttlMs();
|
|
@@ -3504,7 +3564,12 @@ const BASE_REQUIREMENTS = [
|
|
|
3504
3564
|
{
|
|
3505
3565
|
anyOf: ["im:chat:readonly", "im:chat"],
|
|
3506
3566
|
level: "optional",
|
|
3507
|
-
purpose: "
|
|
3567
|
+
purpose: "机器人入群后读取群信息、发送群信息卡;Web 页面显示群名和群头像"
|
|
3568
|
+
},
|
|
3569
|
+
{
|
|
3570
|
+
anyOf: ["contact:user.base:readonly"],
|
|
3571
|
+
level: "optional",
|
|
3572
|
+
purpose: "Web 页面显示用户真实姓名和头像"
|
|
3508
3573
|
}
|
|
3509
3574
|
];
|
|
3510
3575
|
/** Panel sync needs, applicable while `slashCommandPanel` is on. */
|
|
@@ -4279,6 +4344,19 @@ function thrownErrorFields(error) {
|
|
|
4279
4344
|
function assertFeishuOk(response) {
|
|
4280
4345
|
if (response.code !== void 0 && response.code !== 0) throw new FeishuApiError(`code=${response.code}${response.msg === void 0 ? "" : ` ${response.msg}`}`, response.code);
|
|
4281
4346
|
}
|
|
4347
|
+
/** Member-list pages `memberName()` scans before giving up (100 members each). */
|
|
4348
|
+
const MEMBER_NAME_PAGES = 5;
|
|
4349
|
+
/** `user_ids` per `contact.v3.user.batch` call (the API maximum). */
|
|
4350
|
+
const USER_BATCH_SIZE = 50;
|
|
4351
|
+
/** Keep a non-blank name and an `https:` avatar URL from one Feishu response. */
|
|
4352
|
+
function feishuProfile(name, avatar) {
|
|
4353
|
+
const trimmed = name?.trim();
|
|
4354
|
+
const avatarUrl = avatar !== void 0 && /^https:\/\/[^\s"'<>]+$/u.test(avatar) ? avatar : void 0;
|
|
4355
|
+
return {
|
|
4356
|
+
...trimmed === void 0 || trimmed === "" ? {} : { name: trimmed },
|
|
4357
|
+
...avatarUrl === void 0 ? {} : { avatarUrl }
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4282
4360
|
/** Run one raw request, mapping thrown and non-zero-code failures to {@link FeishuApiError}. */
|
|
4283
4361
|
async function feishuCall(call) {
|
|
4284
4362
|
let response;
|
|
@@ -4415,6 +4493,49 @@ var LarkChannelAdapter = class {
|
|
|
4415
4493
|
async getChatInfo(chatId) {
|
|
4416
4494
|
return this.channel.getChatInfo(chatId);
|
|
4417
4495
|
}
|
|
4496
|
+
async userProfiles(userIds) {
|
|
4497
|
+
const client = this.channel.rawClient;
|
|
4498
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
4499
|
+
for (let offset = 0; offset < userIds.length; offset += USER_BATCH_SIZE) {
|
|
4500
|
+
const response = await feishuCall(() => client.contact.v3.user.batch({ params: {
|
|
4501
|
+
user_ids: userIds.slice(offset, offset + USER_BATCH_SIZE),
|
|
4502
|
+
user_id_type: "user_id"
|
|
4503
|
+
} }));
|
|
4504
|
+
for (const item of response.data?.items ?? []) {
|
|
4505
|
+
if (item.user_id === void 0) continue;
|
|
4506
|
+
profiles.set(item.user_id, feishuProfile(item.name, item.avatar?.avatar_72));
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
return profiles;
|
|
4510
|
+
}
|
|
4511
|
+
async chatProfile(chatId) {
|
|
4512
|
+
const client = this.channel.rawClient;
|
|
4513
|
+
const response = await feishuCall(() => client.im.v1.chat.get({ path: { chat_id: chatId } }));
|
|
4514
|
+
return feishuProfile(response.data?.name, response.data?.avatar);
|
|
4515
|
+
}
|
|
4516
|
+
async memberName(userId, chatId) {
|
|
4517
|
+
const client = this.channel.rawClient;
|
|
4518
|
+
let pageToken;
|
|
4519
|
+
for (let page = 0; page < MEMBER_NAME_PAGES; page += 1) {
|
|
4520
|
+
let response;
|
|
4521
|
+
try {
|
|
4522
|
+
response = await feishuCall(() => client.im.v1.chatMembers.get({
|
|
4523
|
+
path: { chat_id: chatId },
|
|
4524
|
+
params: {
|
|
4525
|
+
member_id_type: "user_id",
|
|
4526
|
+
page_size: 100,
|
|
4527
|
+
...pageToken === void 0 ? {} : { page_token: pageToken }
|
|
4528
|
+
}
|
|
4529
|
+
}));
|
|
4530
|
+
} catch (_membersUnavailable) {
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
const hit = response.data?.items?.find((item) => item.member_id === userId)?.name?.trim();
|
|
4534
|
+
if (hit !== void 0 && hit !== "") return hit;
|
|
4535
|
+
pageToken = response.data?.page_token;
|
|
4536
|
+
if (response.data?.has_more !== true || pageToken === void 0) return void 0;
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4418
4539
|
botOpenId() {
|
|
4419
4540
|
return this.channel.botIdentity?.openId;
|
|
4420
4541
|
}
|
|
@@ -6727,7 +6848,7 @@ var InboundRouter = class {
|
|
|
6727
6848
|
} catch (error) {
|
|
6728
6849
|
this.ctx.logger?.warn?.("dsh-channels: recording processed message %s failed: %s", msg.messageId, String(error));
|
|
6729
6850
|
}
|
|
6730
|
-
if (content.startsWith("/")) {
|
|
6851
|
+
if (content.startsWith("/") && this.deps.commands.owns(content)) {
|
|
6731
6852
|
await this.deps.commands.handleSlash(msg, content);
|
|
6732
6853
|
return;
|
|
6733
6854
|
}
|
|
@@ -16256,6 +16377,14 @@ const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
|
|
|
16256
16377
|
* [`EditableFieldWire.browse`],保留手写输入。
|
|
16257
16378
|
*/
|
|
16258
16379
|
const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
|
|
16380
|
+
/** 会话的飞书来源(GET/HEAD,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
|
|
16381
|
+
const SESSION_ORIGIN_ROUTE = "/dsh-channels/session-origin";
|
|
16382
|
+
/**
|
|
16383
|
+
* `GET PROFILES_ROUTE?users=<user_id,...>&chats=<chat_id,...>`: live Feishu
|
|
16384
|
+
* names and avatars for the users and chats the panel shows. At most
|
|
16385
|
+
* {@link PROFILES_MAX_IDS} ids per list.
|
|
16386
|
+
*/
|
|
16387
|
+
const PROFILES_ROUTE = "/dsh-channels/profiles";
|
|
16259
16388
|
/**
|
|
16260
16389
|
* 可由 Web 面板覆盖的配置项。顶层键对应 `src/config.ts` 的字段;`feishuTools.*`
|
|
16261
16390
|
* 是嵌套字段(覆盖层里仍写成 `{ "feishuTools": { ... } }` 对象)。覆盖值写进
|
|
@@ -16457,7 +16586,10 @@ const PANEL_COPY = {
|
|
|
16457
16586
|
cleared: "已恢复为 profile 配置",
|
|
16458
16587
|
debugModeWarning: "调试模式已打开:应用可用范围内的任何人都能和机器人对话。用完请在「访问授权」关闭。",
|
|
16459
16588
|
accessAdminMissingWarning: "没有可用的管理员,所有对话都会被拒绝。请在下方填写管理员 user_id(私聊机器人发送 /userid 获取)。",
|
|
16460
|
-
|
|
16589
|
+
accessAdminCurrent: "当前管理员",
|
|
16590
|
+
accessAdminSourceConfig: "已配置",
|
|
16591
|
+
accessAdminSourceOwner: "应用所有者",
|
|
16592
|
+
profilesWarning: "部分飞书名字和头像未能读取:{reason}",
|
|
16461
16593
|
accessNotLoaded: "飞书连接建立后加载授权列表",
|
|
16462
16594
|
accessRequestsTitle: "待处理申请",
|
|
16463
16595
|
accessGroupsTitle: "已授权群",
|
|
@@ -16766,6 +16898,18 @@ const APP_OWNER_LOOKUP_TIMEOUT_MS = 1e4;
|
|
|
16766
16898
|
const SCOPE_CHECK_TIMEOUT_MS = 1e4;
|
|
16767
16899
|
/** Upper bound on the whole post-connect slash-command panel sync (one list plus per-command writes). */
|
|
16768
16900
|
const SLASH_COMMAND_SYNC_TIMEOUT_MS = 3e4;
|
|
16901
|
+
/**
|
|
16902
|
+
* How long one resolved user or chat profile is reused. Short enough that
|
|
16903
|
+
* every page entry shows current Feishu data; long enough that the session
|
|
16904
|
+
* header and its sidebar tab opening together cost one lookup.
|
|
16905
|
+
*/
|
|
16906
|
+
const PROFILE_TTL_MS = 30 * 1e3;
|
|
16907
|
+
/** The Feishu `code=<n> <msg>` of a failed API call, else the error text. */
|
|
16908
|
+
function feishuReason(error) {
|
|
16909
|
+
return error instanceof FeishuApiError ? error.detail : messageOf$3(error);
|
|
16910
|
+
}
|
|
16911
|
+
/** Feishu `chat_id`s the profile lookup accepts. */
|
|
16912
|
+
const FEISHU_CHAT_ID_PATTERN = /^oc_[A-Za-z0-9_-]+$/u;
|
|
16769
16913
|
/** Wrap the host's storage facility open behind a stable local type. */
|
|
16770
16914
|
async function openFeishuDomain(ctx) {
|
|
16771
16915
|
const domain = await ctx.storageDomain.open(feishuDomain);
|
|
@@ -16778,6 +16922,7 @@ async function openFeishuDomain(ctx) {
|
|
|
16778
16922
|
const accessRequestsRaw = domain.table("access_requests");
|
|
16779
16923
|
const accessWikiUsersRaw = domain.table("access_wiki_users");
|
|
16780
16924
|
const analyzeRequestsRaw = domain.table("analyze_requests");
|
|
16925
|
+
const sessionOriginsRaw = domain.table("session_origins");
|
|
16781
16926
|
return {
|
|
16782
16927
|
routes: {
|
|
16783
16928
|
get: (key) => table.get(key),
|
|
@@ -16832,6 +16977,10 @@ async function openFeishuDomain(ctx) {
|
|
|
16832
16977
|
put: (key, value) => analyzeRequestsRaw.put(key, value),
|
|
16833
16978
|
entries: () => analyzeRequestsRaw.entries()
|
|
16834
16979
|
},
|
|
16980
|
+
sessionOrigins: {
|
|
16981
|
+
get: (key) => sessionOriginsRaw.get(key),
|
|
16982
|
+
put: (key, value) => sessionOriginsRaw.put(key, value)
|
|
16983
|
+
},
|
|
16835
16984
|
getGlobal: () => domain.global.get() ?? {},
|
|
16836
16985
|
setGlobal: (value) => domain.global.set(value),
|
|
16837
16986
|
close: () => domain.close()
|
|
@@ -16978,6 +17127,8 @@ var FeishuService = class {
|
|
|
16978
17127
|
* is unset. Cleared on shutdown so a reconnect looks it up again.
|
|
16979
17128
|
*/
|
|
16980
17129
|
appOwner;
|
|
17130
|
+
/** Live-resolved user and chat profiles, by `user:`/`chat:` key; misses are kept as `{}`. */
|
|
17131
|
+
profileCache = /* @__PURE__ */ new Map();
|
|
16981
17132
|
/** Post-connect scope self-check; cleared on shutdown. */
|
|
16982
17133
|
scopeCheckState;
|
|
16983
17134
|
/** Post-connect slash-command panel sync; cleared on shutdown. */
|
|
@@ -17156,7 +17307,13 @@ var FeishuService = class {
|
|
|
17156
17307
|
streamer: () => this.streamer,
|
|
17157
17308
|
questionAnswerer: () => this.questionAnswerer,
|
|
17158
17309
|
approvalAnswerer: () => this.approvalAnswerer,
|
|
17159
|
-
commands: {
|
|
17310
|
+
commands: {
|
|
17311
|
+
owns: (line) => {
|
|
17312
|
+
const parsed = parseSlashCommand(line);
|
|
17313
|
+
return parsed !== void 0 && commandSpec(parsed.name) !== void 0;
|
|
17314
|
+
},
|
|
17315
|
+
handleSlash: (msg, line) => this.handleSlashMessage(msg, line)
|
|
17316
|
+
}
|
|
17160
17317
|
});
|
|
17161
17318
|
this.analyzeLauncher = new AnalyzeLauncher({
|
|
17162
17319
|
ctx: this.ctx,
|
|
@@ -17645,6 +17802,118 @@ var FeishuService = class {
|
|
|
17645
17802
|
};
|
|
17646
17803
|
}
|
|
17647
17804
|
}
|
|
17805
|
+
/** Panel: Feishu origin of one Session, or null when Feishu never routed it. */
|
|
17806
|
+
async sessionOrigin(sessionId) {
|
|
17807
|
+
const origin = this.routes.origin(sessionId);
|
|
17808
|
+
if (origin === void 0) return null;
|
|
17809
|
+
const route = this.routes.findBySession(sessionId);
|
|
17810
|
+
const bound = route !== void 0;
|
|
17811
|
+
const group = origin.chatType === "group";
|
|
17812
|
+
const profiles = await this.feishuProfiles([origin.senderUserId], group ? [origin.chatId] : []);
|
|
17813
|
+
const chat = profiles.chats[origin.chatId] ?? {};
|
|
17814
|
+
const sender = profiles.users[origin.senderUserId] ?? {};
|
|
17815
|
+
const senderName = sender.name ?? (group ? await this.memberName(origin.senderUserId, origin.chatId) : void 0);
|
|
17816
|
+
return {
|
|
17817
|
+
sessionId,
|
|
17818
|
+
chatType: origin.chatType,
|
|
17819
|
+
kind: origin.kind,
|
|
17820
|
+
chatId: origin.chatId,
|
|
17821
|
+
...chat.name === void 0 ? {} : { chatName: chat.name },
|
|
17822
|
+
...chat.avatarUrl === void 0 ? {} : { chatAvatarUrl: chat.avatarUrl },
|
|
17823
|
+
senderUserId: origin.senderUserId,
|
|
17824
|
+
...senderName === void 0 ? {} : { senderName },
|
|
17825
|
+
...sender.avatarUrl === void 0 ? {} : { senderAvatarUrl: sender.avatarUrl },
|
|
17826
|
+
shared: origin.shared === true,
|
|
17827
|
+
createdAt: origin.createdAt,
|
|
17828
|
+
bound,
|
|
17829
|
+
...bound ? { lastActivityAt: route.lastActivityAt } : {},
|
|
17830
|
+
chatLink: `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(origin.chatId)}`
|
|
17831
|
+
};
|
|
17832
|
+
}
|
|
17833
|
+
/**
|
|
17834
|
+
* Live Feishu names and avatars for the panel and the session-origin view,
|
|
17835
|
+
* through a {@link PROFILE_TTL_MS} cache. Ids that are not Feishu
|
|
17836
|
+
* `user_id`s / `chat_id`s are skipped; a disconnected channel and failed
|
|
17837
|
+
* lookups leave ids absent, and failures are reported in `warning` and not
|
|
17838
|
+
* cached.
|
|
17839
|
+
* @param userIds - user `user_id`s to resolve.
|
|
17840
|
+
* @param chatIds - chat `chat_id`s to resolve.
|
|
17841
|
+
* @returns the resolved profiles by id.
|
|
17842
|
+
*/
|
|
17843
|
+
async feishuProfiles(userIds, chatIds) {
|
|
17844
|
+
const result = {
|
|
17845
|
+
users: {},
|
|
17846
|
+
chats: {}
|
|
17847
|
+
};
|
|
17848
|
+
const warnings = [];
|
|
17849
|
+
const now = Date.now();
|
|
17850
|
+
for (const [key, cached] of this.profileCache) if (now - cached.at >= PROFILE_TTL_MS) this.profileCache.delete(key);
|
|
17851
|
+
const fresh = (key) => this.profileCache.get(key)?.profile;
|
|
17852
|
+
const keep = (target, id, profile) => {
|
|
17853
|
+
if (profile.name !== void 0 || profile.avatarUrl !== void 0) target[id] = profile;
|
|
17854
|
+
};
|
|
17855
|
+
const users = [...new Set(userIds)].filter((id) => FEISHU_USER_ID_PATTERN.test(id));
|
|
17856
|
+
const chats = [...new Set(chatIds)].filter((id) => FEISHU_CHAT_ID_PATTERN.test(id));
|
|
17857
|
+
const missingUsers = [];
|
|
17858
|
+
for (const id of users) {
|
|
17859
|
+
const cached = fresh(`user:${id}`);
|
|
17860
|
+
if (cached === void 0) missingUsers.push(id);
|
|
17861
|
+
else keep(result.users, id, cached);
|
|
17862
|
+
}
|
|
17863
|
+
const missingChats = [];
|
|
17864
|
+
for (const id of chats) {
|
|
17865
|
+
const cached = fresh(`chat:${id}`);
|
|
17866
|
+
if (cached === void 0) missingChats.push(id);
|
|
17867
|
+
else keep(result.chats, id, cached);
|
|
17868
|
+
}
|
|
17869
|
+
const channel = this.channel;
|
|
17870
|
+
if (channel === void 0) return result;
|
|
17871
|
+
const lookups = [];
|
|
17872
|
+
if (missingUsers.length > 0 && channel.userProfiles !== void 0) lookups.push(channel.userProfiles(missingUsers).then((found) => {
|
|
17873
|
+
for (const id of missingUsers) {
|
|
17874
|
+
const profile = found.get(id) ?? {};
|
|
17875
|
+
this.profileCache.set(`user:${id}`, {
|
|
17876
|
+
profile,
|
|
17877
|
+
at: now
|
|
17878
|
+
});
|
|
17879
|
+
keep(result.users, id, profile);
|
|
17880
|
+
}
|
|
17881
|
+
}, (error) => {
|
|
17882
|
+
warnings.push(`读取用户资料失败:${feishuReason(error)}`);
|
|
17883
|
+
}));
|
|
17884
|
+
const chatProfile = channel.chatProfile?.bind(channel);
|
|
17885
|
+
if (chatProfile !== void 0) {
|
|
17886
|
+
let chatError;
|
|
17887
|
+
for (const id of missingChats) lookups.push(chatProfile(id).then((profile) => {
|
|
17888
|
+
this.profileCache.set(`chat:${id}`, {
|
|
17889
|
+
profile,
|
|
17890
|
+
at: now
|
|
17891
|
+
});
|
|
17892
|
+
keep(result.chats, id, profile);
|
|
17893
|
+
}, (error) => {
|
|
17894
|
+
if (chatError !== void 0) return;
|
|
17895
|
+
chatError = `读取群信息失败:${feishuReason(error)}`;
|
|
17896
|
+
warnings.push(chatError);
|
|
17897
|
+
}));
|
|
17898
|
+
}
|
|
17899
|
+
await Promise.all(lookups);
|
|
17900
|
+
if (warnings.length > 0) {
|
|
17901
|
+
result.warning = warnings.join(";");
|
|
17902
|
+
this.ctx.logger?.debug?.("dsh-channels: Feishu profile lookup: %s", result.warning);
|
|
17903
|
+
}
|
|
17904
|
+
return result;
|
|
17905
|
+
}
|
|
17906
|
+
/** Chat-member-list name for a user the contact profile cannot name. */
|
|
17907
|
+
async memberName(userId, chatId) {
|
|
17908
|
+
const cached = this.profileCache.get(`member:${chatId}:${userId}`);
|
|
17909
|
+
if (cached !== void 0 && Date.now() - cached.at < PROFILE_TTL_MS) return cached.profile.name;
|
|
17910
|
+
const name = await this.channel?.memberName?.(userId, chatId);
|
|
17911
|
+
this.profileCache.set(`member:${chatId}:${userId}`, {
|
|
17912
|
+
profile: name === void 0 ? {} : { name },
|
|
17913
|
+
at: Date.now()
|
|
17914
|
+
});
|
|
17915
|
+
return name;
|
|
17916
|
+
}
|
|
17648
17917
|
/** Panel: decide one pending authorization request (same path as the card). */
|
|
17649
17918
|
async resolveAccessRequest(token, decision) {
|
|
17650
17919
|
const requests = this.accessRequests;
|
|
@@ -19364,6 +19633,11 @@ async function readJsonBody(req) {
|
|
|
19364
19633
|
throw new HttpError(400, "请求体不是合法 JSON");
|
|
19365
19634
|
}
|
|
19366
19635
|
}
|
|
19636
|
+
/** Distinct non-empty entries of one comma-separated id query parameter. */
|
|
19637
|
+
function idList(raw) {
|
|
19638
|
+
if (raw === null) return [];
|
|
19639
|
+
return [...new Set(raw.split(",").map((id) => id.trim()).filter((id) => id !== ""))];
|
|
19640
|
+
}
|
|
19367
19641
|
/**
|
|
19368
19642
|
* Parse one JSON-object body for an access write. HttpErrors are answered
|
|
19369
19643
|
* here (415/413/400); `undefined` means "already responded, stop".
|
|
@@ -19469,6 +19743,50 @@ function registerSettingsRoutes(server, gate, surface) {
|
|
|
19469
19743
|
sendJson(res, 200, await surface.catalogFor(url.searchParams.get("provider") ?? void 0, url.searchParams.get("model") ?? void 0));
|
|
19470
19744
|
})
|
|
19471
19745
|
}),
|
|
19746
|
+
server.register({
|
|
19747
|
+
kind: "exact",
|
|
19748
|
+
path: SESSION_ORIGIN_ROUTE,
|
|
19749
|
+
handler: guarded(async (req, res) => {
|
|
19750
|
+
if (!allowOnly(req, res, ["GET", "HEAD"])) return;
|
|
19751
|
+
if (methodOf(req) === "HEAD") {
|
|
19752
|
+
res.writeHead(200, { "cache-control": "no-store" });
|
|
19753
|
+
res.end();
|
|
19754
|
+
return;
|
|
19755
|
+
}
|
|
19756
|
+
const sessionId = new URL(req.url ?? "/", "http://dsh.local").searchParams.get("sessionId")?.trim();
|
|
19757
|
+
if (sessionId === void 0 || sessionId === "") {
|
|
19758
|
+
sendJson(res, 400, {
|
|
19759
|
+
ok: false,
|
|
19760
|
+
error: "缺少 sessionId 参数"
|
|
19761
|
+
});
|
|
19762
|
+
return;
|
|
19763
|
+
}
|
|
19764
|
+
sendJson(res, 200, await surface.sessionOrigin(sessionId));
|
|
19765
|
+
})
|
|
19766
|
+
}),
|
|
19767
|
+
server.register({
|
|
19768
|
+
kind: "exact",
|
|
19769
|
+
path: PROFILES_ROUTE,
|
|
19770
|
+
handler: guarded(async (req, res) => {
|
|
19771
|
+
if (!allowOnly(req, res, ["GET", "HEAD"])) return;
|
|
19772
|
+
if (methodOf(req) === "HEAD") {
|
|
19773
|
+
res.writeHead(200, { "cache-control": "no-store" });
|
|
19774
|
+
res.end();
|
|
19775
|
+
return;
|
|
19776
|
+
}
|
|
19777
|
+
const params = new URL(req.url ?? "/", "http://dsh.local").searchParams;
|
|
19778
|
+
const userIds = idList(params.get("users"));
|
|
19779
|
+
const chatIds = idList(params.get("chats"));
|
|
19780
|
+
if (userIds.length > 200 || chatIds.length > 200) {
|
|
19781
|
+
sendJson(res, 400, {
|
|
19782
|
+
ok: false,
|
|
19783
|
+
error: `每次最多查询 200 个用户和 200 个群`
|
|
19784
|
+
});
|
|
19785
|
+
return;
|
|
19786
|
+
}
|
|
19787
|
+
sendJson(res, 200, await surface.feishuProfiles(userIds, chatIds));
|
|
19788
|
+
})
|
|
19789
|
+
}),
|
|
19472
19790
|
server.register({
|
|
19473
19791
|
kind: "exact",
|
|
19474
19792
|
path: CONFIG_ROUTE,
|
|
@@ -20494,6 +20812,19 @@ var SettingsSurface = class {
|
|
|
20494
20812
|
* the draft provider/model changes). A broken lookup degrades to the
|
|
20495
20813
|
* free-text form instead of failing the panel.
|
|
20496
20814
|
*/
|
|
20815
|
+
/** Feishu origin of one Session for the conversation sidebar. */
|
|
20816
|
+
async sessionOrigin(sessionId) {
|
|
20817
|
+
return { origin: await this.target.sessionOrigin(sessionId) };
|
|
20818
|
+
}
|
|
20819
|
+
/**
|
|
20820
|
+
* Live Feishu names and avatars for the panel's users and chats.
|
|
20821
|
+
* @param userIds - user `user_id`s.
|
|
20822
|
+
* @param chatIds - chat `chat_id`s.
|
|
20823
|
+
* @returns the profiles Feishu resolved.
|
|
20824
|
+
*/
|
|
20825
|
+
async feishuProfiles(userIds, chatIds) {
|
|
20826
|
+
return this.target.feishuProfiles(userIds, chatIds);
|
|
20827
|
+
}
|
|
20497
20828
|
async catalogFor(provider, model) {
|
|
20498
20829
|
if (this.catalog === void 0) return emptyCatalog();
|
|
20499
20830
|
try {
|