@modesto_ma/dsh-channels 0.5.0-beta.4 → 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.
Files changed (41) hide show
  1. package/README.md +49 -28
  2. package/docs/attachment-pipeline-v2.md +1 -1
  3. package/docs/feishu-onboarding.md +6 -5
  4. package/lib/client.js +1874 -1510
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +736 -353
  7. package/lib/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/client/api.ts +45 -2
  10. package/src/client/avatar.tsx +36 -0
  11. package/src/client/icon.tsx +25 -0
  12. package/src/client/index.ts +78 -23
  13. package/src/client/number-field.ts +97 -0
  14. package/src/client/origin-cache.ts +45 -0
  15. package/src/client/origin.tsx +201 -0
  16. package/src/client/panel/access-tab.tsx +90 -21
  17. package/src/client/panel/advanced-tab.tsx +59 -68
  18. package/src/client/panel/connection-tab.tsx +147 -164
  19. package/src/client/panel/credentials-card.tsx +124 -87
  20. package/src/client/panel/fields-tab.tsx +27 -0
  21. package/src/client/panel/index.tsx +77 -54
  22. package/src/client/panel/profiles.ts +73 -0
  23. package/src/client/panel/shared.tsx +358 -253
  24. package/src/client/panel/whitelist.tsx +203 -0
  25. package/src/client/panel/wiki-tab.tsx +33 -39
  26. package/src/client/styles.ts +132 -92
  27. package/src/domain.ts +42 -0
  28. package/src/feishu/scopes.ts +2 -1
  29. package/src/feishu-channel.ts +87 -0
  30. package/src/index.ts +1 -1
  31. package/src/routing/inbound.ts +13 -5
  32. package/src/service.ts +146 -3
  33. package/src/session/routes.ts +45 -4
  34. package/src/settings/contract.ts +265 -152
  35. package/src/settings/routes.ts +47 -0
  36. package/src/settings/surface.ts +201 -197
  37. package/src/client/panel/access-card.tsx +0 -329
  38. package/src/client/panel/analyze-token-card.tsx +0 -70
  39. package/src/client/panel/model-tab.tsx +0 -23
  40. package/src/client/panel/sessions-tab.tsx +0 -24
  41. package/src/client/panel/workspace-tab.tsx +0 -24
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
- this.active.set(key, normalizeRouteRecord(record));
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
  }
@@ -16221,7 +16342,7 @@ function resolveConfig(raw) {
16221
16342
  * Web 设置面板的线上契约:Host 半侧的路由/面板数据与 Browser 半侧的渲染共用。
16222
16343
  *
16223
16344
  * 本模块**没有运行时依赖**:Host 与 Client bundle 都直接内联它。因此这里只放
16224
- * 常量、字段清单与可序列化的 wire 类型,不 import zod / node / DSH 内部包。
16345
+ * 常量、字段清单、可序列化的 wire 类型与无依赖的纯文案函数,不 import zod / node / DSH 内部包。
16225
16346
  *
16226
16347
  * @module dsh-channels/settings/contract
16227
16348
  */
@@ -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": { ... } }` 对象)。覆盖值写进
@@ -16269,43 +16398,50 @@ const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
16269
16398
  * `baseDir`(工作区根目录 = 模型/工具产生文件的落点)可覆盖,但面板只接受
16270
16399
  * **绝对路径**(`~` 会展开);已有 Session / 目录不迁移,只影响之后新建的
16271
16400
  * 会话。校验与保存时建目录见 `src/workspaces.ts`。
16401
+ *
16402
+ * 数组顺序就是面板里同一分组内字段的显示顺序。
16272
16403
  */
16273
16404
  const EDITABLE_KEYS = [
16274
- "baseDir",
16275
16405
  "dmMode",
16276
16406
  "requireMention",
16277
16407
  "threadFallback",
16408
+ "baseDir",
16409
+ "topicIsolation",
16410
+ "routeTtlMs",
16411
+ "approvalTimeoutMs",
16412
+ "questionTimeoutMs",
16413
+ "refreshMs",
16414
+ "statusRefreshMs",
16278
16415
  "welcomeOnboarding",
16279
16416
  "slashCommandPanel",
16280
16417
  "allowPermissionSwitch",
16281
- "analyzeListen",
16282
- "adminUserId",
16283
- "accessMode",
16284
- "authRequestTtlDays",
16418
+ "provider",
16419
+ "model",
16420
+ "reasoningEffort",
16421
+ "maxTokens",
16422
+ "agentPreset",
16285
16423
  "acceptAttachments",
16286
16424
  "attachmentStore",
16287
16425
  "prefetchImages",
16426
+ "historyBootstrapLimit",
16427
+ "feishuTools.attachmentFetch",
16428
+ "feishuTools.topicMessages",
16429
+ "feishuTools.sendFile",
16288
16430
  "prefetchTimeoutMs",
16289
16431
  "prefetchImageMaxBytes",
16290
16432
  "prefetchTotalMaxBytes",
16291
- "historyBootstrapLimit",
16292
- "feishuTools.attachmentFetch",
16293
16433
  "feishuTools.attachmentFetchMaxResources",
16294
16434
  "feishuTools.attachmentFetchMaxBytes",
16295
16435
  "feishuTools.attachmentFetchTimeoutMs",
16296
- "feishuTools.topicMessages",
16297
16436
  "feishuTools.topicMessagesTimeoutMs",
16298
- "feishuTools.sendFile",
16437
+ "promptContextTimeoutMs",
16438
+ "accessMode",
16439
+ "adminUserId",
16440
+ "authRequestTtlDays",
16299
16441
  "feishuTools.wikiRead",
16300
16442
  "feishuTools.wikiWrite",
16301
16443
  "wikiRoots",
16302
- "promptContextTimeoutMs",
16303
- "provider",
16304
- "model",
16305
- "reasoningEffort",
16306
- "agentPreset",
16307
- "refreshMs",
16308
- "statusRefreshMs",
16444
+ "analyzeListen",
16309
16445
  "loggerLevel"
16310
16446
  ];
16311
16447
  /**
@@ -16321,66 +16457,57 @@ const RESTART_KEYS = [
16321
16457
  "statusRefreshMs",
16322
16458
  "loggerLevel"
16323
16459
  ];
16460
+ /** `unit` of byte-count fields; the panel adds a MiB reading next to them. */
16461
+ const BYTES_UNIT = "字节";
16324
16462
  /** Display labels for the panel header/cards (kept on the wire so the client owns no copy). */
16325
16463
  const PANEL_COPY = {
16326
16464
  title: "飞书渠道",
16327
- description: "飞书长连接、应用凭据与渠道配置。凭据只写入 DSH 凭据库且不回显;标注「需重连」的项在连接建立时读取。",
16328
- statusCard: "连接状态",
16329
16465
  phaseUnconfigured: "待配置",
16330
16466
  phaseIdle: "未连接",
16331
16467
  phaseConnecting: "连接中",
16332
16468
  phaseConnected: "已连接",
16333
16469
  phaseError: "连接失败",
16334
- configuredBadge: "凭据已配置",
16335
- unconfiguredBadge: "凭据未配置",
16336
- statusSummaryConnected: "飞书长连接已建立,机器人可以收发消息。",
16337
- statusSummaryUnconfigured: "尚未配置飞书应用凭据:插件已加载,但不会连接飞书。填写凭据并保存后会自动连接。",
16338
- statusSummaryIdle: "飞书应用凭据已就绪,但当前没有建立长连接。",
16339
- statusSummaryConnecting: "正在连接飞书长连接…",
16340
- statusSummaryError: "最近一次连接飞书失败,请检查凭据与网络后重试。",
16470
+ statusSummaryConnected: "机器人在线,可以收发消息",
16471
+ statusSummaryUnconfigured: "还没有填写飞书应用凭据,插件不会连接飞书",
16472
+ statusSummaryIdle: "凭据已就绪,还没有建立连接",
16473
+ statusSummaryConnecting: "正在连接飞书…",
16474
+ statusSummaryError: "连接飞书失败,请检查凭据与网络后重试",
16475
+ uptimePrefix: "运行",
16341
16476
  lastError: "最近一次连接失败",
16342
- scopeCheckTitle: "权限自检",
16343
- scopeCheckOk: "已开通插件需要的全部飞书权限。",
16344
- scopeCheckMissing: "以下飞书权限未开通,开通后需在开发者后台发布新版本:",
16345
- scopeCheckOpenAll: "在开发者后台开通全部缺失权限",
16477
+ checksTitle: "自检",
16478
+ scopeCheckOk: "插件需要的飞书权限都已开通",
16479
+ scopeCheckMissing: "{n} 项权限未开通",
16480
+ scopeCheckMissingRequired: "{n} 项必需权限未开通,相关功能无法使用",
16481
+ scopeCheckMissingOptional: "不影响收发消息,只有相关功能不可用。开通后需在开发者后台发布新版本。",
16482
+ scopeCheckPublish: "开通后需在开发者后台发布新版本。",
16483
+ scopeCheckOpenAll: "全部开通",
16346
16484
  scopeCheckOpenOne: "去开通",
16347
- scopeCheckOptional: "可选",
16348
16485
  scopeCheckRequired: "必需",
16349
- scopeCheckError: "无法读取应用的权限授予状态:",
16486
+ scopeCheckError: "无法读取权限开通情况",
16350
16487
  slashCommandSyncTitle: "斜杠指令面板",
16351
- slashCommandSyncOk: "插件命令已同步到飞书原生斜杠指令面板,在聊天框输入 / 即可选择(飞书客户端约 5 分钟后刷新)。",
16352
- slashCommandSyncTotal: "命令数",
16353
- slashCommandSyncCreated: "本次新增",
16354
- slashCommandSyncUpdated: "本次更新",
16355
- slashCommandSyncRemoved: "本次删除",
16356
- slashCommandSyncError: "同步飞书原生斜杠指令面板失败(不影响直接输入命令):",
16488
+ slashCommandSyncOk: "已同步 {total} 条命令,飞书客户端约 5 分钟后刷新",
16489
+ slashCommandSyncError: "同步失败,直接输入命令不受影响",
16357
16490
  missingConfig: "缺少配置",
16358
- statusHintUnconfigured: "配置完成后插件会自动连接;也可以稍后在本页手动连接。",
16359
- connectionInfo: "连接信息",
16360
- connectionStateLabel: "飞书长连接",
16361
- botOpenId: "Bot open_id",
16362
- uptime: "运行时长",
16363
16491
  routeCount: "话题路由",
16364
16492
  liveAgentCount: "活动会话",
16365
16493
  inflightTurnCount: "在途回复",
16494
+ botOpenId: "Bot open_id",
16366
16495
  healthReasons: "健康判定",
16367
16496
  unknownValue: "—",
16368
16497
  connect: "连接飞书",
16369
16498
  connecting: "连接中…",
16370
16499
  onboardingTitle: "开始使用飞书渠道",
16371
- onboardingIntro: "只需三步:在飞书开放平台创建自建应用,把 App ID / App Secret 填入本页,保存后自动连接。",
16372
16500
  onboardingStep1Title: "创建飞书自建应用",
16373
- onboardingStep1Text: "在飞书开放平台创建企业自建应用,开启机器人能力;事件订阅选择「长连接」,订阅 im.message.receive_v1(接收消息)。",
16501
+ onboardingStep1Text: "在 open.feishu.cn/app 创建企业自建应用,开启机器人能力;事件订阅选「长连接」,订阅 im.message.receive_v1。",
16374
16502
  onboardingStep2Title: "填写应用凭据",
16375
- onboardingStep2Text: "把应用的 App ID(cli_ 开头)与 App Secret 填入下方「飞书应用凭据」,值会写入 DSH 凭据库。",
16376
- onboardingStep3Title: "保存并等待连接",
16377
- onboardingStep3Text: "两项凭据都保存后插件会自动连接;状态变为「已连接」即可在飞书里私聊机器人。",
16378
- onboardingDocsHint: "开放平台地址:open.feishu.cn/app;应用需要开通 im:message、im:message:send_as_bot 等消息权限。",
16379
- credentialsCard: "飞书应用凭据",
16380
- credentialsCardHelp: "凭据值经 DSH 凭据服务写入本机凭据库;面板只显示「已配置 / 未配置」,永不回显已存值。两项都配置后才会连接飞书。",
16381
- appIdLabel: "飞书 App ID",
16382
- appSecretLabel: "飞书 App Secret",
16383
- credentialsRef: "凭据引用",
16503
+ onboardingStep2Text: "把 App ID(cli_ 开头)和 App Secret 填到下方「应用凭据」。",
16504
+ onboardingStep3Title: "保存后自动连接",
16505
+ onboardingStep3Text: "两项都保存后插件会自动连接,状态变成「已连接」就能在飞书里私聊机器人。",
16506
+ credentialsTitle: "应用凭据",
16507
+ credentialsHint: "值只存在本机凭据库,面板不回显。",
16508
+ appIdLabel: "App ID",
16509
+ appSecretLabel: "App Secret",
16510
+ credentialStore: "凭据库",
16384
16511
  saveCredentials: "保存凭据",
16385
16512
  savingCredentials: "保存中…",
16386
16513
  credentialSaved: "凭据已保存",
@@ -16390,27 +16517,29 @@ const PANEL_COPY = {
16390
16517
  credentialAutoConnectFailed: "凭据已保存,但自动连接失败",
16391
16518
  credentialRemoved: "已移除存储的凭据",
16392
16519
  credentialRemovedPending: "已移除存储的凭据,重启飞书连接后生效",
16393
- removeCredential: "移除已存值",
16520
+ replaceCredential: "更换",
16521
+ setCredential: "设置",
16522
+ cancel: "取消",
16523
+ removeCredential: "移除",
16394
16524
  removingCredential: "移除中…",
16395
16525
  credentialConfigured: "已配置",
16396
16526
  credentialMissing: "未配置",
16397
16527
  credentialReadOnly: "当前凭据存储不可写",
16398
16528
  appIdPlaceholder: "输入 App ID(cli_…)",
16399
16529
  appSecretPlaceholder: "输入 App Secret",
16400
- credentialsPendingHint: "填写后点「保存凭据」;两项齐全时插件会自动连接飞书。",
16401
- connectionTabHelp: "连接状态与飞书应用凭据。凭据不完整时不会连接飞书。",
16402
- sessionsTabHelp: "私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。",
16403
- modelTabHelp: "飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。",
16404
- workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。",
16405
- accessTabHelp: "准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。",
16406
- wikiTabHelp: "知识库工具以应用身份访问「知识库范围」内的文档、电子表格和多维表格。只有管理员或知识库白名单里的用户发起的话题能用;每次打开、新建、修改仍需话题发起人在授权卡上确认。",
16407
- advancedTabHelp: "流式刷新等低层参数、Web 覆盖文件与只读信息。",
16408
- save: "保存更改",
16530
+ credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
16531
+ save: "保存",
16409
16532
  saving: "保存中…",
16410
- saveHintDirty: "只保存改动过的字段;标注「需重连」的项在下次连接时生效。",
16411
- discardTab: "放弃本页更改",
16412
- saveHintClean: "没有未保存的更改。",
16413
- dirtyCount: "未保存项",
16533
+ discard: "放弃",
16534
+ dirtyCount: "{n} 项未保存",
16535
+ dirtyRestart: ",其中 {n} 项需要重连后生效",
16536
+ details: "详情",
16537
+ expand: "展开",
16538
+ collapse: "收起",
16539
+ collapsedCount: "{n} 项",
16540
+ collapsedClean: "一般不用改",
16541
+ collapsedChanged: "{n} 项已在面板修改",
16542
+ restartTag: "需重连",
16414
16543
  listAdd: "添加",
16415
16544
  listRemove: "移除",
16416
16545
  listEntry: "第 {n} 项",
@@ -16422,62 +16551,58 @@ const PANEL_COPY = {
16422
16551
  directoryPicked: "已选择目录",
16423
16552
  directoryPickFailed: "无法打开系统目录选择器",
16424
16553
  saved: "已保存",
16425
- cleared: "已清除 Web 覆盖,恢复为 profile 配置",
16426
- clear: "清除 Web 覆盖",
16427
- clearing: "清除中…",
16428
- noOverrides: "当前没有 Web 覆盖",
16429
- overridden: "Web 覆盖",
16430
- inherited: "继承 profile",
16431
- restartTag: "需重连",
16432
- restartBanner: "以下配置需重启飞书连接后生效:",
16433
- restartWarning: "重启会断开当前飞书长连接并中断进行中的回复,请确认空闲时操作。",
16434
- reconnect: "重启飞书连接",
16554
+ saveInvalid: "请先修正标红的配置项",
16555
+ numberInvalid: "请输入数字",
16556
+ numberMin: "不得小于 {n}",
16557
+ numberMax: "不得大于 {n}",
16558
+ modelFootnote: "对之后新建的会话生效。",
16559
+ catalogFailed: "模型目录加载失败,已退回手填。",
16560
+ catalogLoading: "正在读取宿主机选项…",
16561
+ catalogUnavailable: "宿主机未提供可选项,请手填。",
16562
+ restartBanner: "以下配置要重启飞书连接后生效:",
16563
+ restartWarning: "重启会断开当前飞书连接并中断进行中的回复,确定现在重启?",
16564
+ reconnect: "重启连接",
16435
16565
  reconnecting: "重连中…",
16436
- refresh: "刷新状态",
16566
+ refresh: "刷新",
16437
16567
  retry: "重试",
16438
16568
  pollFailed: "状态刷新失败",
16439
16569
  connectDone: "已连接飞书",
16440
16570
  reconnectDone: "已重启飞书连接",
16441
- saveInvalid: "请先修正标红的配置项",
16442
- enabled: "已启用",
16443
- disabled: "已关闭",
16444
- credentialRemoveConfirm: "确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。",
16445
- analyzeTokenCard: "日志分析接口令牌",
16446
- analyzeTokenCardHelp: "日志平台调用分析接口时 Authorization: Bearer 后面的值。经 DSH 凭据服务写入本机凭据库,面板永不回显;保存或移除后下一次请求即生效,无需重连。未配置时接口对所有请求返回 503。",
16571
+ loading: "加载中…",
16572
+ loadFailed: "设置面板加载失败",
16447
16573
  analyzeTokenLabel: "访问令牌",
16574
+ analyzeTokenSummary: "请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求",
16448
16575
  analyzeTokenPlaceholder: "填写新令牌(建议 32 位以上随机串)",
16449
- saveAnalyzeToken: "保存令牌",
16450
16576
  analyzeTokenSaved: "令牌已保存,下一次请求生效",
16451
16577
  analyzeTokenRemoved: "令牌已移除,接口将拒绝所有请求",
16452
- accessNotLoaded: "飞书连接建立后加载授权列表",
16453
- loading: "加载中…",
16454
- loadFailed: "设置面板加载失败",
16455
- catalogFailed: "模型目录加载失败,已退回手填。",
16456
- catalogLoading: "正在读取宿主机选项…",
16457
- catalogUnavailable: "宿主机未提供可选项,请手填。",
16458
- version: "插件版本",
16578
+ overlayTitle: "Web 覆盖",
16459
16579
  overlayPath: "覆盖文件",
16460
- overlayPathHelp: "可以直接编辑该 JSON 文件;面板保存会整体重写它,「清除 Web 覆盖」会删除它并回落到 profile 配置。",
16461
- advancedCard: "只读与高级信息",
16462
- advancedHelp: "这些值来自 profile 配置,不能在面板里编辑。",
16463
- readOnlyCard: "部署级参数",
16464
- accessCard: "访问与授权",
16465
- accessCardHelp: "准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。",
16466
- accessModeOpenWarning: "当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。",
16467
- accessAdminMissingWarning: "当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。",
16468
- accessAdminOwnerNotice: "未配置管理员,已自动使用应用所有者作为管理员:",
16580
+ overlayCount: "{n} 项配置由面板覆盖",
16581
+ noOverrides: "当前没有面板覆盖的配置",
16582
+ overlayPathHelp: "面板保存的配置写在这个 JSON 文件里,优先级高于 profile。可以直接编辑;面板保存会整体重写它。",
16583
+ clear: "恢复 profile 配置",
16584
+ clearing: "恢复中…",
16585
+ clearConfirm: "确定删除所有面板覆盖,恢复为 profile 配置?",
16586
+ cleared: "已恢复为 profile 配置",
16587
+ debugModeWarning: "调试模式已打开:应用可用范围内的任何人都能和机器人对话。用完请在「访问授权」关闭。",
16588
+ accessAdminMissingWarning: "没有可用的管理员,所有对话都会被拒绝。请在下方填写管理员 user_id(私聊机器人发送 /userid 获取)。",
16589
+ accessAdminCurrent: "当前管理员",
16590
+ accessAdminSourceConfig: "已配置",
16591
+ accessAdminSourceOwner: "应用所有者",
16592
+ profilesWarning: "部分飞书名字和头像未能读取:{reason}",
16593
+ accessNotLoaded: "飞书连接建立后加载授权列表",
16594
+ accessRequestsTitle: "待处理申请",
16469
16595
  accessGroupsTitle: "已授权群",
16470
- accessGroupsEmpty: "尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。",
16471
16596
  accessUsersTitle: "已授权用户",
16472
- accessUsersEmpty: "尚未授权任何用户。已授权用户可在所有已授权群里与机器人对话(私聊仍只开给管理员)。",
16473
- accessRequestsTitle: "待处理申请",
16474
- accessRequestsEmpty: "没有待处理的授权申请。",
16475
- accessGroupChatIdPlaceholder: "群 chat_id(oc_…)",
16597
+ accessGroupsEmpty: "还没有授权任何群",
16598
+ accessUsersEmpty: "还没有授权任何用户",
16599
+ accessGroupChatIdPlaceholder: "chat_id(oc_…,群里发 /groupid 获取)",
16476
16600
  accessGroupNamePlaceholder: "群名(可选)",
16477
- accessUserIdPlaceholder: "用户 user_id",
16601
+ accessUserIdPlaceholder: "user_id(私聊机器人发 /userid 获取)",
16478
16602
  accessUserNamePlaceholder: "昵称(可选)",
16479
16603
  accessAddGroup: "添加群",
16480
16604
  accessAddUser: "添加用户",
16605
+ accessAdd: "添加",
16481
16606
  accessRemove: "移除",
16482
16607
  accessAllow: "授权",
16483
16608
  accessDeny: "忽略",
@@ -16485,23 +16610,22 @@ const PANEL_COPY = {
16485
16610
  accessSourceCommand: "对话命令",
16486
16611
  accessSourceCard: "授权卡",
16487
16612
  accessSourcePanel: "设置面板",
16488
- accessRequestGroup: "群申请",
16489
- accessRequestUser: "用户申请",
16490
- accessRequestRequester: "申请人",
16491
- accessRequestExpires: "有效期至",
16613
+ accessRequestGroup: "申请授权群",
16614
+ accessRequestUser: "申请使用",
16615
+ accessRequestIn: "在「{group}」",
16616
+ accessRequestExpires: "{date} 前有效",
16492
16617
  accessGroupAdded: "已添加群授权",
16493
16618
  accessGroupRemoved: "已移除群授权",
16494
16619
  accessUserAdded: "已添加用户授权",
16495
16620
  accessUserRemoved: "已移除用户授权",
16496
16621
  accessRequestAllowed: "已授权",
16497
16622
  accessRequestDenied: "已忽略",
16498
- accessHints: "三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。",
16499
- wikiUsersCard: "知识库白名单",
16500
- wikiUsersHelp: "与「访问授权」里的用户白名单相互独立:这里的用户(以及管理员)发起的话题可以使用知识库工具。也可以在飞书里发送 /auth wiki add @用户 或 /auth wiki remove @用户;移除立即生效。",
16501
- wikiUsersEmpty: "知识库白名单为空:目前只有管理员发起的话题能使用知识库工具。",
16623
+ wikiUsersTitle: "可使用的人",
16624
+ wikiUsersAdminPlus: "管理员 + {n}",
16625
+ wikiUsersEmpty: "目前只有管理员发起的话题能用知识库工具",
16626
+ wikiUsersHint: "也可以在飞书发 /auth wiki add @用户",
16502
16627
  wikiUserAdded: "已加入知识库白名单",
16503
- wikiUserRemoved: "已移出知识库白名单",
16504
- accessChatIdHint: "chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。"
16628
+ wikiUserRemoved: "已移出知识库白名单"
16505
16629
  };
16506
16630
  //#endregion
16507
16631
  //#region src/settings/overlay.ts
@@ -16774,6 +16898,18 @@ const APP_OWNER_LOOKUP_TIMEOUT_MS = 1e4;
16774
16898
  const SCOPE_CHECK_TIMEOUT_MS = 1e4;
16775
16899
  /** Upper bound on the whole post-connect slash-command panel sync (one list plus per-command writes). */
16776
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;
16777
16913
  /** Wrap the host's storage facility open behind a stable local type. */
16778
16914
  async function openFeishuDomain(ctx) {
16779
16915
  const domain = await ctx.storageDomain.open(feishuDomain);
@@ -16786,6 +16922,7 @@ async function openFeishuDomain(ctx) {
16786
16922
  const accessRequestsRaw = domain.table("access_requests");
16787
16923
  const accessWikiUsersRaw = domain.table("access_wiki_users");
16788
16924
  const analyzeRequestsRaw = domain.table("analyze_requests");
16925
+ const sessionOriginsRaw = domain.table("session_origins");
16789
16926
  return {
16790
16927
  routes: {
16791
16928
  get: (key) => table.get(key),
@@ -16840,6 +16977,10 @@ async function openFeishuDomain(ctx) {
16840
16977
  put: (key, value) => analyzeRequestsRaw.put(key, value),
16841
16978
  entries: () => analyzeRequestsRaw.entries()
16842
16979
  },
16980
+ sessionOrigins: {
16981
+ get: (key) => sessionOriginsRaw.get(key),
16982
+ put: (key, value) => sessionOriginsRaw.put(key, value)
16983
+ },
16843
16984
  getGlobal: () => domain.global.get() ?? {},
16844
16985
  setGlobal: (value) => domain.global.set(value),
16845
16986
  close: () => domain.close()
@@ -16986,6 +17127,8 @@ var FeishuService = class {
16986
17127
  * is unset. Cleared on shutdown so a reconnect looks it up again.
16987
17128
  */
16988
17129
  appOwner;
17130
+ /** Live-resolved user and chat profiles, by `user:`/`chat:` key; misses are kept as `{}`. */
17131
+ profileCache = /* @__PURE__ */ new Map();
16989
17132
  /** Post-connect scope self-check; cleared on shutdown. */
16990
17133
  scopeCheckState;
16991
17134
  /** Post-connect slash-command panel sync; cleared on shutdown. */
@@ -17164,7 +17307,13 @@ var FeishuService = class {
17164
17307
  streamer: () => this.streamer,
17165
17308
  questionAnswerer: () => this.questionAnswerer,
17166
17309
  approvalAnswerer: () => this.approvalAnswerer,
17167
- commands: { handleSlash: (msg, line) => this.handleSlashMessage(msg, line) }
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
+ }
17168
17317
  });
17169
17318
  this.analyzeLauncher = new AnalyzeLauncher({
17170
17319
  ctx: this.ctx,
@@ -17653,6 +17802,118 @@ var FeishuService = class {
17653
17802
  };
17654
17803
  }
17655
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
+ }
17656
17917
  /** Panel: decide one pending authorization request (same path as the card). */
17657
17918
  async resolveAccessRequest(token, decision) {
17658
17919
  const requests = this.accessRequests;
@@ -19372,6 +19633,11 @@ async function readJsonBody(req) {
19372
19633
  throw new HttpError(400, "请求体不是合法 JSON");
19373
19634
  }
19374
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
+ }
19375
19641
  /**
19376
19642
  * Parse one JSON-object body for an access write. HttpErrors are answered
19377
19643
  * here (415/413/400); `undefined` means "already responded, stop".
@@ -19477,6 +19743,50 @@ function registerSettingsRoutes(server, gate, surface) {
19477
19743
  sendJson(res, 200, await surface.catalogFor(url.searchParams.get("provider") ?? void 0, url.searchParams.get("model") ?? void 0));
19478
19744
  })
19479
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
+ }),
19480
19790
  server.register({
19481
19791
  kind: "exact",
19482
19792
  path: CONFIG_ROUTE,
@@ -19735,12 +20045,13 @@ const CREDENTIAL_FIELDS = [
19735
20045
  "appSecret",
19736
20046
  "analyzeToken"
19737
20047
  ];
19738
- /** Optional text fields: an empty string in the panel clears the override. */
20048
+ /** Optional fields: an empty string in the panel clears the override. */
19739
20049
  const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
19740
20050
  "provider",
19741
20051
  "model",
19742
20052
  "reasoningEffort",
19743
20053
  "agentPreset",
20054
+ "maxTokens",
19744
20055
  "adminUserId",
19745
20056
  "analyzeListen"
19746
20057
  ]);
@@ -19829,26 +20140,47 @@ function normalizeStringList(key, raw, spec) {
19829
20140
  };
19830
20141
  }
19831
20142
  const SELECT_OPTIONS = {
19832
- attachmentStore: [{
19833
- value: "native",
19834
- label: "DSH 原生附件库(默认)"
19835
- }, {
19836
- value: "archive",
19837
- label: "工作区 attachments/(旧版)"
19838
- }],
19839
20143
  dmMode: [{
19840
20144
  value: "open",
19841
- label: "开放私聊"
20145
+ label: "开启"
19842
20146
  }, {
19843
20147
  value: "disabled",
19844
- label: "禁用私聊"
20148
+ label: "关闭"
19845
20149
  }],
19846
20150
  threadFallback: [{
19847
20151
  value: "degrade",
19848
- label: "话题被拒时降级为会话级"
20152
+ label: "降级为会话级"
19849
20153
  }, {
19850
20154
  value: "strict",
19851
- label: "话题被拒时直接失败"
20155
+ label: "直接失败"
20156
+ }],
20157
+ topicIsolation: [
20158
+ {
20159
+ value: "auto",
20160
+ label: "自动(推荐)"
20161
+ },
20162
+ {
20163
+ value: "subdir",
20164
+ label: "普通子目录"
20165
+ },
20166
+ {
20167
+ value: "worktree",
20168
+ label: "Git worktree"
20169
+ }
20170
+ ],
20171
+ attachmentStore: [{
20172
+ value: "native",
20173
+ label: "DSH 原生附件库"
20174
+ }, {
20175
+ value: "archive",
20176
+ label: "话题目录 attachments/"
20177
+ }],
20178
+ accessMode: [{
20179
+ value: "guarded",
20180
+ label: "关闭(管理员与白名单)"
20181
+ }, {
20182
+ value: "open",
20183
+ label: "打开(所有人可聊)"
19852
20184
  }],
19853
20185
  loggerLevel: [
19854
20186
  {
@@ -19867,260 +20199,341 @@ const SELECT_OPTIONS = {
19867
20199
  value: "error",
19868
20200
  label: "error"
19869
20201
  }
19870
- ],
19871
- accessMode: [{
19872
- value: "guarded",
19873
- label: "守卫模式(仅管理员与白名单)"
19874
- }, {
19875
- value: "open",
19876
- label: "开放(不安全,仅调试)"
19877
- }]
20202
+ ]
19878
20203
  };
19879
- /** Panel copy per editable key: label, help, and (for numbers) bounds. */
20204
+ /** Two-value selects rendered as switches: the stored value while on / off. */
20205
+ const TOGGLES = {
20206
+ dmMode: {
20207
+ on: "open",
20208
+ off: "disabled"
20209
+ },
20210
+ threadFallback: {
20211
+ on: "degrade",
20212
+ off: "strict"
20213
+ },
20214
+ attachmentStore: {
20215
+ on: "native",
20216
+ off: "archive"
20217
+ },
20218
+ accessMode: {
20219
+ on: "open",
20220
+ off: "guarded"
20221
+ }
20222
+ };
20223
+ const DAY_MS = 864e5;
19880
20224
  const FIELD_META = {
20225
+ dmMode: {
20226
+ label: "私聊",
20227
+ group: "trigger",
20228
+ summary: "关闭后私聊不再触发会话",
20229
+ help: "群聊和已绑定的话题不受影响。"
20230
+ },
20231
+ requireMention: {
20232
+ label: "普通群需要 @ 机器人",
20233
+ group: "trigger",
20234
+ summary: "话题内始终不需要 @",
20235
+ help: "关闭后普通群里的每条消息都会触发会话。"
20236
+ },
20237
+ threadFallback: {
20238
+ label: "话题被拒时降级为会话级",
20239
+ group: "trigger",
20240
+ summary: "飞书不允许建话题时改在会话里回复,关闭则直接报错"
20241
+ },
19881
20242
  baseDir: {
19882
- label: "工作区根目录(文件落点)",
19883
- help: "每个用户/群成员一个工作区:私聊 <baseDir>/p2p/<user_id>/,群聊 <baseDir>/groups/<chat_id>/<user_id>/;每个话题在其中拥有独立工作目录 <工作区>/topics/<话题>/(Git 仓库工作区可自动改用 worktree)。必须是绝对路径(~ 开头按家目录展开),保存时会创建该目录;只影响之后新建立的会话 —— 已有话题即使 /renew 也沿用原目录,旧文件不会迁移。",
20243
+ label: "工作区根目录",
20244
+ group: "workspace",
20245
+ summary: "模型和工具生成文件的位置",
20246
+ help: "每个用户一个工作区:私聊 <根目录>/p2p/<user_id>/,群聊 <根目录>/groups/<chat_id>/<user_id>/,每个话题在里面有独立目录。必须是绝对路径(~ 按家目录展开),保存时会创建该目录。只影响之后新建的话题,旧文件不迁移。",
19884
20247
  placeholder: "/Users/you/.dsh/dsh-channels/workspaces",
19885
20248
  wide: true
19886
20249
  },
19887
- dmMode: {
19888
- label: "私聊模式",
19889
- help: "禁用后私聊消息不再触发会话;群聊与已绑定话题不受影响。"
20250
+ topicIsolation: {
20251
+ label: "话题工作目录",
20252
+ group: "workspace",
20253
+ summary: "每个话题在工作区里怎么隔离",
20254
+ help: "自动:工作区是 Git 仓库时给每个话题建一个 worktree,否则用普通子目录。选 Git worktree 但宿主不支持时退回普通子目录。只影响之后新建的话题,已有话题保持原来的目录。"
19890
20255
  },
19891
- requireMention: {
19892
- label: "普通群需 @ 机器人",
19893
- help: "已绑定的主题内始终不需要 @;关闭后普通群的每条消息都会触发。"
20256
+ routeTtlMs: {
20257
+ label: "话题过期",
20258
+ group: "cards",
20259
+ summary: "闲置超过这么久的话题不再续聊,0 为永不过期",
20260
+ help: "过期话题里的新消息会被明确拒绝并提示新开话题,而不是悄悄开一个没有上下文的会话;Session 和工作区文件都保留。调小后,下一次清理(启动时和定期)就会让更多旧话题过期。",
20261
+ min: 0,
20262
+ step: DAY_MS,
20263
+ unit: "天",
20264
+ scale: DAY_MS
19894
20265
  },
19895
- threadFallback: {
19896
- label: "话题被拒时的兜底",
19897
- help: "degrade 降级为会话级路由;strict 直接让回复失败。"
20266
+ approvalTimeoutMs: {
20267
+ label: "审批卡片等待",
20268
+ group: "cards",
20269
+ summary: "超时自动拒绝,等待期间回复暂停",
20270
+ min: 1e4,
20271
+ max: 36e5,
20272
+ step: 1e3,
20273
+ unit: "秒",
20274
+ scale: 1e3
20275
+ },
20276
+ questionTimeoutMs: {
20277
+ label: "提问卡片等待",
20278
+ group: "cards",
20279
+ summary: "模型向用户提问时最多等多久",
20280
+ min: 1e4,
20281
+ max: 36e5,
20282
+ step: 1e3,
20283
+ unit: "秒",
20284
+ scale: 1e3
20285
+ },
20286
+ refreshMs: {
20287
+ label: "流式刷新间隔",
20288
+ group: "cards",
20289
+ summary: "回复卡片多久更新一次",
20290
+ min: 200,
20291
+ max: 1e4,
20292
+ step: 100,
20293
+ unit: "ms"
20294
+ },
20295
+ statusRefreshMs: {
20296
+ label: "状态行保活间隔",
20297
+ group: "cards",
20298
+ summary: "没有文字输出时,计时多久刷新一次",
20299
+ min: 1e3,
20300
+ max: 6e4,
20301
+ step: 500,
20302
+ unit: "ms"
19898
20303
  },
19899
20304
  welcomeOnboarding: {
19900
- label: "首次私聊引导卡",
19901
- help: "每个私聊会话第一次触达时发送一张命令与用法引导卡,只发一次。"
20305
+ label: "首次私聊发引导卡",
20306
+ group: "features",
20307
+ summary: "每个私聊只发一次"
19902
20308
  },
19903
20309
  slashCommandPanel: {
19904
- label: "同步飞书原生斜杠指令面板",
19905
- help: "连接时把插件命令同步到飞书输入 / 时弹出的面板;需开通 application:app_slash_command:read 与 write 权限,部分企业不允许应用添加斜杠指令。关闭时不读写面板,直接输入命令不受影响。"
20310
+ label: "同步斜杠指令面板",
20311
+ group: "features",
20312
+ summary: "在飞书输入 / 时列出插件命令",
20313
+ help: "需要 application:app_slash_command:read 和 application:app_slash_command:write 权限。部分企业不允许应用添加斜杠指令;关闭时不读写面板,直接输入命令不受影响。"
19906
20314
  },
19907
20315
  allowPermissionSwitch: {
19908
- label: "允许飞书侧切换权限预设",
19909
- help: "安全敏感:开启后话题所有者可用 /permissions 切换沙箱与审批预设。"
20316
+ label: "允许在飞书里切换权限预设",
20317
+ group: "features",
20318
+ summary: "话题所有者可以用 /permissions 改沙箱和审批"
19910
20319
  },
19911
- analyzeListen: {
19912
- label: "日志分析接口监听地址",
19913
- help: "日志平台调用 POST /dsh-channels/analyze 的独立端口,格式 host:port(如 0.0.0.0:3090);留空 = 关闭接口;需重连生效。请求需带 Authorization: Bearer <令牌>,令牌在本页下方「日志分析接口令牌」卡片填写。",
19914
- placeholder: "留空 = 关闭"
20320
+ provider: {
20321
+ label: "服务商",
20322
+ group: "model",
20323
+ summary: "留空使用部署默认",
20324
+ placeholder: "留空 = 部署默认"
19915
20325
  },
19916
- adminUserId: {
19917
- label: "管理员 user_id",
19918
- help: "唯一管理员。私聊机器人发送 /userid 获取(不是 ou_ 开头的 open_id,也不是邮箱);留空 = 清除 Web 覆盖。未配置时自动使用飞书应用所有者(需可选的「获取应用信息」权限,开不了就在这里手动填写);两者都没有时 guarded 模式会拒绝所有对话。",
19919
- placeholder: "user_id"
20326
+ model: {
20327
+ label: "模型",
20328
+ group: "model",
20329
+ summary: "留空使用部署默认",
20330
+ placeholder: "留空 = 部署默认"
19920
20331
  },
19921
- accessMode: {
19922
- label: "准入模式",
19923
- help: "guarded(默认):私聊仅管理员,群聊需群授权且发送者是管理员或已授权用户;open:应用可用范围内的任何人都能对话,不安全,仅调试。"
20332
+ reasoningEffort: {
20333
+ label: "推理档位",
20334
+ group: "model",
20335
+ summary: "模型声明的档位 id,留空使用适配器默认",
20336
+ placeholder: "留空 = 适配器默认"
19924
20337
  },
19925
- authRequestTtlDays: {
19926
- label: "授权申请有效期(天)",
19927
- help: "入群/用户授权申请卡的有效期,1–30 天;过期后卡片需重新触发申请(也可在本页下方「待处理申请」中处理)。",
20338
+ maxTokens: {
20339
+ label: "单次输出上限",
20340
+ group: "model",
20341
+ summary: "留空使用模型默认",
19928
20342
  min: 1,
19929
- max: 30,
19930
- step: 1
20343
+ step: 1,
20344
+ unit: "tokens",
20345
+ optional: true,
20346
+ placeholder: "模型默认"
20347
+ },
20348
+ agentPreset: {
20349
+ label: "Agent 预设",
20350
+ group: "model",
20351
+ summary: "留空使用 roster 默认",
20352
+ placeholder: "留空 = roster 默认"
19931
20353
  },
19932
20354
  acceptAttachments: {
19933
- label: "接收入站附件",
19934
- help: "接收飞书图片、文件与合并转发记录。native(默认)下入站附件存入 DSH 原生附件库:触发消息的图片预取进首轮、文件保持懒取,纯附件消息不回复、不落地,需要时请引用该消息(多个文件可合并转发后引用)再提问;archive 下按旧版行为下载到工作区 attachments/。关闭后附件消息不保存、不触发模型、也不列入清单。"
20355
+ label: "接收图片和文件",
20356
+ group: "attachments",
20357
+ summary: "关闭后附件消息不保存、不触发模型",
20358
+ help: "纯附件消息不回复;需要时引用该消息再提问,多个文件可以合并转发后引用。也接收合并转发的聊天记录。"
19935
20359
  },
19936
20360
  attachmentStore: {
19937
- label: "入站附件存储后端",
19938
- help: "native(默认):图片与文件存入 DSH 原生附件库(不可变、内容寻址去重、读取校验、随 Session 回放;不自动清理)。archive:旧版下载到话题工作区 attachments/(可清理但会阻塞会话)。宿主未挂载附件服务时 native 自动降级 archive。"
20361
+ label: "使用 DSH 原生附件库",
20362
+ group: "attachments",
20363
+ summary: "推荐开启;关闭则下载到话题目录 attachments/",
20364
+ help: "原生附件库:不可变、内容寻址去重、随 Session 回放,不自动清理。话题目录:旧版行为,可以手动清理,但下载会阻塞会话。宿主没有附件服务时自动退回话题目录。"
19939
20365
  },
19940
20366
  prefetchImages: {
19941
- label: "预取触发消息的图片",
19942
- help: "native 下把触发消息的图片预取为首轮 ImageBlock,会话/话题/卡片不等下载;失败、超限或超时的图片降级为清单中的懒取项。关闭后图片也需用 feishu_attachment_fetch 获取。"
20367
+ label: "预取触发消息里的图片",
20368
+ group: "attachments",
20369
+ summary: "首轮直接带图,失败的留给模型按需取"
20370
+ },
20371
+ historyBootstrapLimit: {
20372
+ label: "首轮带上话题历史",
20373
+ group: "attachments",
20374
+ summary: "新会话自动附带最近几条消息,0 为关闭",
20375
+ help: "更早的消息由模型用 feishu_topic_messages 工具翻页读取。缺权限或读取失败时静默省略。",
20376
+ min: 0,
20377
+ max: 20,
20378
+ step: 1,
20379
+ unit: "条"
20380
+ },
20381
+ "feishuTools.attachmentFetch": {
20382
+ label: "读取附件",
20383
+ group: "tools",
20384
+ summary: "按需下载已授权消息里的图片和文件",
20385
+ help: "工具名 feishu_attachment_fetch。关闭后文件无法按需读取,入站文件直接下载到话题目录。对新会话生效。"
20386
+ },
20387
+ "feishuTools.topicMessages": {
20388
+ label: "读取话题消息",
20389
+ group: "tools",
20390
+ summary: "翻看引用消息和话题里更早的内容",
20391
+ help: "工具名 feishu_topic_messages。只影响模型主动读取;引用摘录和首轮历史不受影响。"
20392
+ },
20393
+ "feishuTools.sendFile": {
20394
+ label: "发送文件",
20395
+ group: "tools",
20396
+ summary: "把生成的图片或文件发回会话,需 im:resource 权限",
20397
+ help: "工具名 feishu_send_file。图片不超过 10 MB,文件不超过 30 MB。对新会话生效。"
19943
20398
  },
19944
20399
  prefetchTimeoutMs: {
19945
- label: "图片预取整段超时(ms)",
19946
- help: "一次触发消息图片预取的总超时(默认 10000 ms,100–120000);到期后已取回的图片保留,其余降级为清单,不阻塞回合。",
20400
+ label: "图片预取超时",
20401
+ group: "limits",
19947
20402
  min: 100,
19948
20403
  max: 12e4,
19949
- step: 100
20404
+ step: 100,
20405
+ unit: "ms"
19950
20406
  },
19951
20407
  prefetchImageMaxBytes: {
19952
- label: "单张预取图片上限(字节)",
19953
- help: "单张图片超过该字节数即不入首轮图片、留在清单懒取(默认 20971520 = 20 MiB)。",
20408
+ label: "单张预取图片上限",
20409
+ group: "limits",
19954
20410
  min: 1,
19955
20411
  max: 200 * 1024 * 1024,
19956
- step: 1
20412
+ step: 1,
20413
+ unit: BYTES_UNIT
19957
20414
  },
19958
20415
  prefetchTotalMaxBytes: {
19959
- label: "单次预取总量上限(字节)",
19960
- help: "一次触发消息图片预取的总字节预算(默认 52428800 = 50 MiB);用尽后剩余图片降级为清单懒取。",
20416
+ label: "单次预取总量",
20417
+ group: "limits",
19961
20418
  min: 1,
19962
20419
  max: 1024 * 1024 * 1024,
19963
- step: 1
19964
- },
19965
- historyBootstrapLimit: {
19966
- label: "首轮话题历史条数",
19967
- help: "新 Session 首轮自动附带本话题最近 N 条消息摘要(0 = 关闭,上限 20);被动读取使用短预算,缺权限/失败时静默省略。更早的消息用 feishu_topic_messages 工具翻页。",
19968
- min: 0,
19969
- max: 20,
19970
- step: 1
19971
- },
19972
- promptContextTimeoutMs: {
19973
- label: "被动读取预算(ms)",
19974
- help: "引用摘录与首轮话题历史这类被动 prompt 增强的整段预算(默认 3000 ms,100–10000);超时即省略对应块、回合照跑。模型主动调用工具的读取超时另由 feishu_tools 超时控制。",
19975
- min: 100,
19976
- max: 1e4,
19977
- step: 100
19978
- },
19979
- "feishuTools.attachmentFetch": {
19980
- label: "启用 feishu_attachment_fetch 工具",
19981
- help: "让模型按 message_id/file_key 懒取已授权消息的图片/文件。关闭后文件无法懒取,插件整体回退 archive 后端(入站文件直接下载到工作区)。对新会话生效。"
20420
+ step: 1,
20421
+ unit: BYTES_UNIT
19982
20422
  },
19983
20423
  "feishuTools.attachmentFetchMaxResources": {
19984
- label: "单次取回资源数上限",
19985
- help: "一次工具调用最多取回的附件数量(默认 10,1–50);超出会返回可读原因,模型可按 file_key 拆分重试。",
20424
+ label: "读取附件:数量上限",
20425
+ group: "limits",
19986
20426
  min: 1,
19987
20427
  max: 50,
19988
- step: 1
20428
+ step: 1,
20429
+ unit: "个"
19989
20430
  },
19990
20431
  "feishuTools.attachmentFetchMaxBytes": {
19991
- label: "单次取回字节预算(字节)",
19992
- help: "一次工具调用的总下载字节预算(默认 104857600 = 100 MiB,不设上限);文件流式写入原生库,不整文件缓冲,几 GiB 的文件只占磁盘。",
20432
+ label: "读取附件:总字节",
20433
+ group: "limits",
19993
20434
  min: 1,
19994
- step: 1
20435
+ step: 1,
20436
+ unit: BYTES_UNIT
19995
20437
  },
19996
20438
  "feishuTools.attachmentFetchTimeoutMs": {
19997
- label: "单次取回超时(ms)",
19998
- help: "一次 feishu_attachment_fetch 的整段超时(默认 120000 ms,最小 1000,最大 2147483647 ≈ 24.8 天);到期取消下载并逐项报告可读原因。大文件请按带宽同步调大。",
20439
+ label: "读取附件:超时",
20440
+ group: "limits",
19999
20441
  min: 1e3,
20000
20442
  max: MAX_TIMER_DELAY_MS,
20001
- step: 1e3
20002
- },
20003
- "feishuTools.topicMessages": {
20004
- label: "启用 feishu_topic_messages 工具",
20005
- help: "让模型按需读取引用消息与话题内更早的消息/附件。关闭只影响模型工具;被动的引用摘录与首轮历史仍由 historyBootstrapLimit 控制。"
20443
+ step: 1e3,
20444
+ unit: "ms"
20006
20445
  },
20007
20446
  "feishuTools.topicMessagesTimeoutMs": {
20008
- label: "单次话题读取超时(ms)",
20009
- help: "一次 feishu_topic_messages 的整段超时(默认 20000 ms,1000–120000);被动摘录使用独立的短预算。",
20447
+ label: "读取话题消息:超时",
20448
+ group: "limits",
20010
20449
  min: 1e3,
20011
20450
  max: 12e4,
20012
- step: 1e3
20451
+ step: 1e3,
20452
+ unit: "ms"
20013
20453
  },
20014
- "feishuTools.sendFile": {
20015
- label: "启用 feishu_send_file 工具",
20016
- help: "让模型把当前话题工作目录里生成的图片/文件发回当前会话(图片 ≤10 MB,文件 ≤30 MB)。需开通 im:resource 权限。对新会话生效。"
20454
+ promptContextTimeoutMs: {
20455
+ label: "引用摘录与历史:超时",
20456
+ group: "limits",
20457
+ min: 100,
20458
+ max: 1e4,
20459
+ step: 100,
20460
+ unit: "ms"
20461
+ },
20462
+ accessMode: {
20463
+ label: "调试模式",
20464
+ group: "access",
20465
+ summary: "打开后所有人都能聊,不安全,只在调试时用",
20466
+ help: "关闭时(默认):管理员随时可用;非管理员私聊会被拒绝;群聊要同时满足群已授权、发言人在用户白名单里。授权入口有三个:群里 /auth 命令、授权卡、本页。撤销立即生效。"
20467
+ },
20468
+ adminUserId: {
20469
+ label: "管理员 user_id",
20470
+ group: "access",
20471
+ summary: "私聊机器人发送 /userid 获取;留空使用应用所有者",
20472
+ help: "不是 ou_ 开头的 open_id,也不是邮箱。私聊关闭时 /userid 不可用,可以在飞书管理后台的成员详情里查看。自动使用应用所有者需要可选的「获取应用信息」权限。",
20473
+ placeholder: "user_id"
20474
+ },
20475
+ authRequestTtlDays: {
20476
+ label: "申请有效期",
20477
+ group: "access",
20478
+ summary: "授权申请卡多久内可以处理",
20479
+ min: 1,
20480
+ max: 30,
20481
+ step: 1,
20482
+ unit: "天"
20017
20483
  },
20018
20484
  "feishuTools.wikiRead": {
20019
- label: "启用知识库读取工具",
20020
- help: "让模型浏览「知识库范围」内的知识库节点,并读取 docx 文档、电子表格和多维表格(feishu_wiki_list / feishu_wiki_read / feishu_wiki_sheet_read / feishu_wiki_base_read),以应用身份访问。需开通 wiki:wiki、docx:document(或对应的只读权限),电子表格需 sheets:spreadsheet:read,多维表格需 base:table:read、base:field:read、base:record:read(旧版 sheets:spreadsheet、bitable:app 不生效)。只有话题所有者是管理员或在知识库白名单(/auth wiki add @成员)中才能使用;打开文档前会在授权卡上征得话题所有者同意,每个话题一次。知识库范围为空时不挂载。对新会话生效。"
20485
+ label: "读取知识库",
20486
+ group: "wiki",
20487
+ summary: "浏览节点,读取文档、电子表格、多维表格",
20488
+ help: "以应用身份访问,工具 feishu_wiki_list / feishu_wiki_read / feishu_wiki_sheet_read / feishu_wiki_base_read。打开文档前在授权卡上征得话题发起人同意,每个话题一次。只有管理员或下方「可使用的人」发起的话题能用(/auth wiki add @成员)。需要权限 wiki:wiki、docx:document(或只读)、sheets:spreadsheet:read、base:table:read、base:field:read、base:record:read;旧版 sheets:spreadsheet、bitable:app 不生效。对新会话生效。"
20021
20489
  },
20022
20490
  "feishuTools.wikiWrite": {
20023
- label: "启用知识库写入工具",
20024
- help: "让模型在「知识库范围」内新建 docx 文档、电子表格和多维表格,编辑本应用拥有的文档和电子表格,增改删本应用拥有的多维表格记录、向其他多维表格新增记录(feishu_wiki_create / feishu_wiki_edit / feishu_wiki_sheet_edit / feishu_wiki_base_edit),以应用身份访问;每次新建或编辑都需话题所有者在审批卡上同意。需开通 wiki:wiki、docx:document、sheets:spreadsheet:write_only、base:record:create、base:record:update、base:record:delete 权限,以及读取工具的权限;使用条件同读取工具(管理员或知识库白名单)。知识库范围为空时不挂载。对新会话生效。"
20491
+ label: "编辑知识库",
20492
+ group: "wiki",
20493
+ summary: "新建和修改文档、表格,每次都需审批",
20494
+ help: "工具 feishu_wiki_create / feishu_wiki_edit / feishu_wiki_sheet_edit / feishu_wiki_base_edit。只能编辑本应用拥有的文档和表格;其他多维表格只能新增记录。需要读取工具的权限,以及 sheets:spreadsheet:write_only、base:record:create、base:record:update、base:record:delete。对新会话生效。"
20025
20495
  },
20026
20496
  wikiRoots: {
20027
- label: "知识库范围(wikiRoots)",
20028
- help: "知识库工具只能访问这些位置,每个输入框填一个链接:整个知识空间 https://<租户>.feishu.cn/wiki/space/<space_id>,或节点 https://<租户>.feishu.cn/wiki/<node_token>(该节点及其下所有节点);最多 20 项,空输入框保存时忽略。留空 = 不挂载任何知识库工具(即使上面的开关已开启)。对新会话生效。",
20497
+ label: "可访问范围",
20498
+ group: "wiki",
20499
+ summary: "每行一个知识空间或节点链接,为空时不挂载知识库工具",
20500
+ help: "整个知识空间 https://<租户>.feishu.cn/wiki/space/<space_id>,或节点 https://<租户>.feishu.cn/wiki/<node_token>(含其下所有节点)。最多 20 项,空行保存时忽略。对新会话生效。",
20029
20501
  placeholder: "https://<租户>.feishu.cn/wiki/space/<space_id>",
20030
20502
  wide: true
20031
20503
  },
20032
- provider: {
20033
- label: "模型服务商",
20034
- help: "留空 = 使用部署默认;例如 deepseek-official。",
20035
- placeholder: "留空 = 部署默认"
20036
- },
20037
- model: {
20038
- label: "模型 id",
20039
- help: "留空 = 使用部署默认;例如 deepseek-v4-flash。",
20040
- placeholder: "留空 = 部署默认"
20041
- },
20042
- reasoningEffort: {
20043
- label: "推理档位",
20044
- help: "模型声明的精确档位 id(如 high / max);留空 = 适配器默认。",
20045
- placeholder: "留空 = 适配器默认"
20046
- },
20047
- agentPreset: {
20048
- label: "Agent 预设",
20049
- help: "新会话使用的预设 id(如 standard);留空 = roster 默认。",
20050
- placeholder: "留空 = roster 默认"
20051
- },
20052
- refreshMs: {
20053
- label: "流式刷新间隔(ms)",
20054
- help: "流式卡片的 patch 节流间隔,200–10000;需重连生效。",
20055
- min: 200,
20056
- max: 1e4,
20057
- step: 100
20058
- },
20059
- statusRefreshMs: {
20060
- label: "状态行保活间隔(ms)",
20061
- help: "无文本输出时活动状态行的计时刷新间隔,1000–60000;需重连生效。",
20062
- min: 1e3,
20063
- max: 6e4,
20064
- step: 500
20504
+ analyzeListen: {
20505
+ label: "监听地址",
20506
+ group: "analyze",
20507
+ summary: "日志平台调用分析接口的 host:port,留空关闭",
20508
+ help: "接口路径 POST /dsh-channels/analyze,请求需带 Authorization: Bearer <访问令牌>。",
20509
+ placeholder: "例如 0.0.0.0:3090"
20065
20510
  },
20066
20511
  loggerLevel: {
20067
20512
  label: "SDK 日志级别",
20068
- help: "官方 SDK 的日志级别;需重连生效。"
20513
+ group: "logging"
20069
20514
  }
20070
20515
  };
20071
- const GROUP_OF = {
20072
- baseDir: "workspace",
20073
- dmMode: "behavior",
20074
- requireMention: "behavior",
20075
- threadFallback: "behavior",
20076
- welcomeOnboarding: "behavior",
20077
- slashCommandPanel: "behavior",
20078
- allowPermissionSwitch: "behavior",
20079
- analyzeListen: "behavior",
20080
- adminUserId: "access",
20081
- accessMode: "access",
20082
- authRequestTtlDays: "access",
20083
- acceptAttachments: "content",
20084
- attachmentStore: "content",
20085
- prefetchImages: "content",
20086
- prefetchTimeoutMs: "content",
20087
- prefetchImageMaxBytes: "content",
20088
- prefetchTotalMaxBytes: "content",
20089
- historyBootstrapLimit: "content",
20090
- promptContextTimeoutMs: "tools",
20091
- "feishuTools.attachmentFetch": "tools",
20092
- "feishuTools.attachmentFetchMaxResources": "tools",
20093
- "feishuTools.attachmentFetchMaxBytes": "tools",
20094
- "feishuTools.attachmentFetchTimeoutMs": "tools",
20095
- "feishuTools.topicMessages": "tools",
20096
- "feishuTools.topicMessagesTimeoutMs": "tools",
20097
- "feishuTools.sendFile": "tools",
20098
- "feishuTools.wikiRead": "wiki",
20099
- "feishuTools.wikiWrite": "wiki",
20100
- wikiRoots: "wiki",
20101
- provider: "model",
20102
- model: "model",
20103
- reasoningEffort: "model",
20104
- agentPreset: "model",
20105
- refreshMs: "streaming",
20106
- statusRefreshMs: "streaming",
20107
- loggerLevel: "streaming"
20108
- };
20109
20516
  /** One descriptor per editable key, in `EDITABLE_KEYS` order (Host-driven form). */
20110
20517
  const FIELD_DESCRIPTORS = EDITABLE_KEYS.map((key) => {
20111
20518
  const meta = FIELD_META[key];
20112
20519
  const options = SELECT_OPTIONS[key];
20520
+ const toggle = TOGGLES[key];
20113
20521
  const list = STRING_LIST_FIELDS[key];
20114
20522
  return {
20115
20523
  key,
20116
20524
  label: meta.label,
20117
20525
  kind: list !== void 0 ? "string-list" : BOOLEAN_KEYS.has(key) ? "boolean" : options !== void 0 ? "select" : meta.min !== void 0 ? "number" : "text",
20118
- group: GROUP_OF[key],
20119
- help: meta.help,
20526
+ group: meta.group,
20527
+ ...meta.summary === void 0 ? {} : { summary: meta.summary },
20528
+ ...meta.help === void 0 ? {} : { help: meta.help },
20120
20529
  ...options === void 0 ? {} : { options },
20530
+ ...toggle === void 0 ? {} : { toggle },
20121
20531
  ...meta.min === void 0 ? {} : { min: meta.min },
20122
20532
  ...meta.max === void 0 ? {} : { max: meta.max },
20123
20533
  ...meta.step === void 0 ? {} : { step: meta.step },
20534
+ ...meta.unit === void 0 ? {} : { unit: meta.unit },
20535
+ ...meta.scale === void 0 ? {} : { scale: meta.scale },
20536
+ ...meta.optional === void 0 ? {} : { optional: meta.optional },
20124
20537
  ...meta.placeholder === void 0 ? {} : { placeholder: meta.placeholder },
20125
20538
  ...list === void 0 ? {} : { maxItems: list.maxItems },
20126
20539
  ...list?.itemFormat === void 0 ? {} : { itemFormat: list.itemFormat },
@@ -20214,15 +20627,6 @@ function withDirectoryBrowse(fields, available) {
20214
20627
  function messageOf$1(error) {
20215
20628
  return error instanceof Error ? error.message : String(error);
20216
20629
  }
20217
- function formatDays(ms) {
20218
- return ms === 0 ? "关闭(不过期)" : `${Math.round(ms / 864e5)} 天`;
20219
- }
20220
- function formatSeconds(ms) {
20221
- return `${Math.round(ms / 1e3)} 秒`;
20222
- }
20223
- function formatOptional(value, suffix = "") {
20224
- return value === void 0 ? "未设置" : `${value}${suffix}`;
20225
- }
20226
20630
  /** Credential values are opaque strings; trim, cap length, reject control chars. */
20227
20631
  const MAX_CREDENTIAL_LENGTH = 512;
20228
20632
  function normalizeCredentialValue(raw) {
@@ -20308,39 +20712,6 @@ function normalizeAccessName(raw) {
20308
20712
  value
20309
20713
  };
20310
20714
  }
20311
- /** Build the read-only advanced card from the effective config. */
20312
- function readOnlyFields(config) {
20313
- return [
20314
- {
20315
- key: "topicIsolation",
20316
- label: "话题目录隔离",
20317
- value: config.topicIsolation === "auto" ? "自动(Git 仓库用 worktree,否则普通子目录)" : config.topicIsolation === "worktree" ? "Git worktree(失败回退子目录)" : "普通子目录",
20318
- help: "每个话题的独立工作目录形式;在 profile 配置里用 topicIsolation 设置(auto / subdir / worktree)。"
20319
- },
20320
- {
20321
- key: "routeTtlMs",
20322
- label: "话题路由过期",
20323
- value: formatDays(config.routeTtlMs),
20324
- help: "超期话题的后续消息会被明确拒绝,而不是新建 Context-free Session。"
20325
- },
20326
- {
20327
- key: "approvalTimeoutMs",
20328
- label: "审批卡片等待",
20329
- value: formatSeconds(config.approvalTimeoutMs)
20330
- },
20331
- {
20332
- key: "questionTimeoutMs",
20333
- label: "提问卡片等待",
20334
- value: formatSeconds(config.questionTimeoutMs)
20335
- },
20336
- {
20337
- key: "maxTokens",
20338
- label: "单次输出上限",
20339
- value: formatOptional(config.maxTokens, " tokens"),
20340
- help: "留空/未设置时使用模型适配器默认值。"
20341
- }
20342
- ];
20343
- }
20344
20715
  /**
20345
20716
  * Derive the panel phase from readiness + runtime facts. The order matters:
20346
20717
  * an incomplete configuration always wins (no connection is attempted), and a
@@ -20441,6 +20812,19 @@ var SettingsSurface = class {
20441
20812
  * the draft provider/model changes). A broken lookup degrades to the
20442
20813
  * free-text form instead of failing the panel.
20443
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
+ }
20444
20828
  async catalogFor(provider, model) {
20445
20829
  if (this.catalog === void 0) return emptyCatalog();
20446
20830
  try {
@@ -20520,7 +20904,6 @@ var SettingsSurface = class {
20520
20904
  overriddenKeys,
20521
20905
  restartPendingKeys,
20522
20906
  fields,
20523
- readOnly: readOnlyFields(effective),
20524
20907
  credentials
20525
20908
  },
20526
20909
  access
@@ -21234,6 +21617,6 @@ function apply(ctx, config) {
21234
21617
  }, "dsh-channels");
21235
21618
  }
21236
21619
  //#endregion
21237
- export { AUTH_USER_TARGET_ERRORS, AccessController, AccessRequests, AccessStore, ApprovalAnswerer, CARD_MAX_BYTES, CARD_TARGET_BYTES, CATALOG_ROUTE, CHANNEL_COMMAND_NAMES, CONFIG_ROUTE, CREDENTIALS_ROUTE, Config, DM_DENY_TEXT, DM_UNCONFIGURED_TEXT, DSH_CHANNELS_VERSION, EDITABLE_KEYS, EMPTY_TOTALS, FIELD_DESCRIPTORS, FeishuService, HEALTH_DEPENDENCY_NAMES, INTERRUPTION_NOTICE, LOGICAL_EXPORT_FORMAT, LOGICAL_EXPORT_FORMAT_VERSION, MAX_QUESTION_OPTIONS, MAX_TABLES_PER_CARD, OverlayStore, PANEL_CHEVRON_TOKEN, PANEL_COPY, PANEL_ROUTE, PANEL_TITLES, PICK_DIRECTORY_ROUTE, QuestionAnswerer, REASONING_MAX_BYTES, RECONNECT_ROUTE, RESTART_KEYS, RouteOperationQueue, SESSION_HISTORY_DISPLAY_LIMIT, STATUS_ROUTE, SUPPORTED_COMMAND_NAMES, SettingsSurface, TRUNCATION_SUFFIX, TurnStreamer, accessGroupRecordSchema, accessRequestRecordSchema, accessUserRecordSchema, addUsage, appendSessionHistory, apply, applyCatalog, applyProjectionViews, armModelCommand, averageDecodeTokensPerSecond, averageTtftMs, buildApprovalCard, buildApprovalSettledCard, buildCard, buildCommandResultCard, buildCompactComparisonCard, buildGroupInfoCard, buildHealthCard, buildInterruptionCard, buildQuestionCard, buildQuestionSettledCard, buildRenewCard, buildRequestSettledCard, buildSessionHistoryCard, buildSessionStatusCard, buildUserRequestCard, buildWorkDirCard, cacheHitPct, cardJsonBytes, chatRouteKey, commandResultCardKind, compactFailureContext, countFiberStates, createHostCatalog, createLogicalSessionExport, createSettingsDirectoryPicker, decideAccess, defaultOverlayPath, emptyCatalog, ensureWorkspace, expiredRouteRecordSchema, feishuDomain, foldAll, foldEvent, foldHealthSnapshot, foldSessionStats, inflightSnapshotSchema, inject, isAdminUserId, isSameOriginRequest, isSupportedCommand, maskOpenIdTail, measureSessionContext, mergeOverlay, mountSettingsRoutes, name, normalizeCredentialValue, normalizeOverlay, normalizeRouteRecord, normalizeSessionHistory, parseOverlay, parseSlashCommand, pickEditable, processedRecordSchema, querySessionHistory, readOnlyFields, readProjectionViews, registerChannelCommands, registerModelCommand, registerSettingsRoutes, resolveAuthUserTarget, resolveDshHome, resolveWorkDir, routeRecordSchema, segmentCardContent, serializeLogicalSession, sessionDirOf, sessionHistoryEntrySchema, splitBodyForCard, stagingPathFor, stagingRoot, stripTables, supportedCommandHelp, supportedCommandList, tableBlocks, topicDirFor, topicTokenFor, usageFromBuckets, withDirectoryBrowse, workspacePathFor, workspaceTitleFor, worktreeBranchFor };
21620
+ export { AUTH_USER_TARGET_ERRORS, AccessController, AccessRequests, AccessStore, ApprovalAnswerer, CARD_MAX_BYTES, CARD_TARGET_BYTES, CATALOG_ROUTE, CHANNEL_COMMAND_NAMES, CONFIG_ROUTE, CREDENTIALS_ROUTE, Config, DM_DENY_TEXT, DM_UNCONFIGURED_TEXT, DSH_CHANNELS_VERSION, EDITABLE_KEYS, EMPTY_TOTALS, FIELD_DESCRIPTORS, FeishuService, HEALTH_DEPENDENCY_NAMES, INTERRUPTION_NOTICE, LOGICAL_EXPORT_FORMAT, LOGICAL_EXPORT_FORMAT_VERSION, MAX_QUESTION_OPTIONS, MAX_TABLES_PER_CARD, OverlayStore, PANEL_CHEVRON_TOKEN, PANEL_COPY, PANEL_ROUTE, PANEL_TITLES, PICK_DIRECTORY_ROUTE, QuestionAnswerer, REASONING_MAX_BYTES, RECONNECT_ROUTE, RESTART_KEYS, RouteOperationQueue, SESSION_HISTORY_DISPLAY_LIMIT, STATUS_ROUTE, SUPPORTED_COMMAND_NAMES, SettingsSurface, TRUNCATION_SUFFIX, TurnStreamer, accessGroupRecordSchema, accessRequestRecordSchema, accessUserRecordSchema, addUsage, appendSessionHistory, apply, applyCatalog, applyProjectionViews, armModelCommand, averageDecodeTokensPerSecond, averageTtftMs, buildApprovalCard, buildApprovalSettledCard, buildCard, buildCommandResultCard, buildCompactComparisonCard, buildGroupInfoCard, buildHealthCard, buildInterruptionCard, buildQuestionCard, buildQuestionSettledCard, buildRenewCard, buildRequestSettledCard, buildSessionHistoryCard, buildSessionStatusCard, buildUserRequestCard, buildWorkDirCard, cacheHitPct, cardJsonBytes, chatRouteKey, commandResultCardKind, compactFailureContext, countFiberStates, createHostCatalog, createLogicalSessionExport, createSettingsDirectoryPicker, decideAccess, defaultOverlayPath, emptyCatalog, ensureWorkspace, expiredRouteRecordSchema, feishuDomain, foldAll, foldEvent, foldHealthSnapshot, foldSessionStats, inflightSnapshotSchema, inject, isAdminUserId, isSameOriginRequest, isSupportedCommand, maskOpenIdTail, measureSessionContext, mergeOverlay, mountSettingsRoutes, name, normalizeCredentialValue, normalizeOverlay, normalizeRouteRecord, normalizeSessionHistory, parseOverlay, parseSlashCommand, pickEditable, processedRecordSchema, querySessionHistory, readProjectionViews, registerChannelCommands, registerModelCommand, registerSettingsRoutes, resolveAuthUserTarget, resolveDshHome, resolveWorkDir, routeRecordSchema, segmentCardContent, serializeLogicalSession, sessionDirOf, sessionHistoryEntrySchema, splitBodyForCard, stagingPathFor, stagingRoot, stripTables, supportedCommandHelp, supportedCommandList, tableBlocks, topicDirFor, topicTokenFor, usageFromBuckets, withDirectoryBrowse, workspacePathFor, workspaceTitleFor, worktreeBranchFor };
21238
21621
 
21239
21622
  //# sourceMappingURL=index.js.map