@modesto_ma/dsh-channels 0.5.0-beta.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -7,7 +7,6 @@ import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
8
  import { LoggerLevel, createLarkChannel } from "@larksuiteoapi/node-sdk";
9
9
  import { homedir } from "node:os";
10
- import { createServer } from "node:http";
11
10
  import { promisify } from "node:util";
12
11
  import { gzip } from "node:zlib";
13
12
  //#region src/access/policy.ts
@@ -7461,6 +7460,28 @@ var InboundRouter = class {
7461
7460
  live.agent.followup(message);
7462
7461
  }
7463
7462
  };
7463
+ //#endregion
7464
+ //#region src/analyze/contract.ts
7465
+ /**
7466
+ * Wire contract of the log-platform analyze API.
7467
+ *
7468
+ * `POST /api/dsh-channels.logalyze` on the DSH Web server, with
7469
+ * `Authorization: Bearer <token>`, an `Idempotency-Key` header, and a JSON
7470
+ * body naming the requester, the trigger time, the subject (application +
7471
+ * error code), the log-platform detail pages, the target group, and the
7472
+ * prompt. A valid request posts the analysis root card into the group and
7473
+ * answers `202`; the analysis then streams in a topic below that card. Log
7474
+ * packages are the prompt's business: the analysis skill fetches whatever the
7475
+ * prompt names, and the detail pages are only listed on the card.
7476
+ *
7477
+ * @module dsh-channels/analyze/contract
7478
+ */
7479
+ /**
7480
+ * Exact DSH Web route of the analyze API. It follows the host's plain-HTTP
7481
+ * `/api/<name>.<method>` naming; an exact route wins over the `/api` RPC prefix
7482
+ * route, so the request carries this API's bearer token, not a DSH login.
7483
+ */
7484
+ const ANALYZE_ROUTE = "/api/dsh-channels.logalyze";
7464
7485
  /** Largest accepted request body (the prompt dominates it). */
7465
7486
  const ANALYZE_MAX_BODY_BYTES = 1024 * 1024;
7466
7487
  /** Largest accepted prompt, in characters. */
@@ -7571,9 +7592,25 @@ var AnalyzeLauncher = class {
7571
7592
  accepting = /* @__PURE__ */ new Map();
7572
7593
  /** Background analyses still starting (drained on shutdown). */
7573
7594
  running = /* @__PURE__ */ new Set();
7595
+ /** Whether requests are admitted: from {@link open} after start until {@link close}. */
7596
+ admitting = false;
7574
7597
  constructor(deps) {
7575
7598
  this.deps = deps;
7576
7599
  }
7600
+ /**
7601
+ * Start admitting requests. Called at channel start after
7602
+ * {@link markInterrupted}, so a new request is never marked interrupted.
7603
+ */
7604
+ open() {
7605
+ this.admitting = true;
7606
+ }
7607
+ /** Stop admitting requests (503) ahead of shutdown. */
7608
+ close() {
7609
+ this.admitting = false;
7610
+ }
7611
+ enabled() {
7612
+ return this.deps.config.analyzeEnabled;
7613
+ }
7577
7614
  async token() {
7578
7615
  return await this.deps.resolveToken();
7579
7616
  }
@@ -7596,16 +7633,18 @@ var AnalyzeLauncher = class {
7596
7633
  }
7597
7634
  }
7598
7635
  /**
7599
- * Wait for every background analysis that is still starting. Called after
7600
- * `started` turns false, so each one ends on its failure card or, when its
7601
- * first prompt was already sent, as an ordinary topic.
7636
+ * Wait for every acceptance in flight, then every background analysis that
7637
+ * is still starting. Called after `started` turns false, so each one ends on
7638
+ * its failure card or, when its first prompt was already sent, as an
7639
+ * ordinary topic.
7602
7640
  */
7603
7641
  async drain() {
7642
+ await Promise.allSettled([...this.accepting.values()].map((pending) => pending.response));
7604
7643
  await Promise.allSettled([...this.running]);
7605
7644
  }
7606
7645
  /**
7607
7646
  * Mark every analysis still `running` as interrupted, on its card and in
7608
- * its record. Called at channel start before the listener opens, so each
7647
+ * its record. Called at channel start before admission opens, so each
7609
7648
  * such record belongs to a previous instance whose first turn never
7610
7649
  * settled. A card that cannot be updated (deleted, recalled) is logged and
7611
7650
  * still recorded as interrupted, so it is not retried on every start.
@@ -7625,7 +7664,7 @@ var AnalyzeLauncher = class {
7625
7664
  async accept(request, key, bodyHash) {
7626
7665
  const channel = this.deps.channel();
7627
7666
  const domain = this.deps.domain();
7628
- if (!this.deps.started() || channel === void 0 || domain === void 0) return refused(503, "飞书渠道尚未连接,请稍后重试");
7667
+ if (!this.admitting || !this.deps.started() || channel === void 0 || domain === void 0) return refused(503, "飞书渠道尚未连接,请稍后重试");
7629
7668
  const stored = domain.analyzeRequests.get(key);
7630
7669
  if (stored !== void 0) {
7631
7670
  if (stored.bodyHash !== bodyHash) return reusedKey();
@@ -7852,168 +7891,6 @@ function reusedKey() {
7852
7891
  return refused(409, "Idempotency-Key 已被内容不同的请求使用");
7853
7892
  }
7854
7893
  //#endregion
7855
- //#region src/analyze/server.ts
7856
- /**
7857
- * The analyze API's dedicated HTTP listener.
7858
- *
7859
- * Serves exactly `POST /dsh-channels/analyze` (see `./contract.ts`) on the
7860
- * `analyzeListen` address. This module owns the transport-level checks, in
7861
- * order: path, method, bearer token, `Idempotency-Key`, media type, body size,
7862
- * and JSON validation; everything after validation belongs to the
7863
- * {@link AnalyzeHandler}. Bodies, tokens, and prompts are never logged.
7864
- *
7865
- * @module dsh-channels/analyze/server
7866
- */
7867
- var HttpError$1 = class extends Error {
7868
- status;
7869
- constructor(status, message) {
7870
- super(message);
7871
- this.status = status;
7872
- }
7873
- };
7874
- function send(res, response) {
7875
- const body = JSON.stringify(response.body);
7876
- res.writeHead(response.status, {
7877
- "content-type": "application/json; charset=utf-8",
7878
- "cache-control": "no-store",
7879
- "content-length": Buffer.byteLength(body)
7880
- });
7881
- res.end(body);
7882
- }
7883
- function refuse(res, status, error, headers = {}) {
7884
- for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
7885
- send(res, {
7886
- status,
7887
- body: {
7888
- ok: false,
7889
- error
7890
- }
7891
- });
7892
- }
7893
- /** Constant-time token comparison over fixed-length digests. */
7894
- function tokenMatches(presented, expected) {
7895
- return timingSafeEqual(createHash("sha256").update(presented).digest(), createHash("sha256").update(expected).digest());
7896
- }
7897
- /** The bearer credential of one request, or undefined. */
7898
- function bearerOf(req) {
7899
- const header = req.headers.authorization;
7900
- if (header === void 0) return void 0;
7901
- return /^Bearer[ \t]+(\S+)[ \t]*$/iu.exec(header)?.[1];
7902
- }
7903
- /** Read the body up to {@link ANALYZE_MAX_BODY_BYTES}; larger bodies fail with 413. */
7904
- async function readBody(req) {
7905
- const declared = Number(req.headers["content-length"]);
7906
- if (Number.isFinite(declared) && declared > 1048576) throw new HttpError$1(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
7907
- const chunks = [];
7908
- let size = 0;
7909
- for await (const chunk of req) {
7910
- const buffer = chunk;
7911
- size += buffer.length;
7912
- if (size > 1048576) throw new HttpError$1(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
7913
- chunks.push(buffer);
7914
- }
7915
- return Buffer.concat(chunks).toString("utf8");
7916
- }
7917
- /**
7918
- * Handle one HTTP request against the analyze contract.
7919
- * @param req - the incoming request.
7920
- * @param res - its response.
7921
- * @param handler - the business side.
7922
- */
7923
- async function handleAnalyzeHttp(req, res, handler) {
7924
- if (new URL(req.url ?? "/", "http://analyze.invalid").pathname !== "/dsh-channels/analyze") {
7925
- refuse(res, 404, "未知路径");
7926
- return;
7927
- }
7928
- if ((req.method ?? "GET").toUpperCase() !== "POST") {
7929
- refuse(res, 405, "只接受 POST", { allow: "POST" });
7930
- return;
7931
- }
7932
- const expected = await handler.token();
7933
- if (expected === void 0 || expected === "") {
7934
- refuse(res, 503, "分析接口未配置访问令牌,请联系部署者配置 analyzeToken 引用的凭据");
7935
- return;
7936
- }
7937
- const presented = bearerOf(req);
7938
- if (presented === void 0 || !tokenMatches(presented, expected)) {
7939
- refuse(res, 401, "访问令牌无效", { "www-authenticate": "Bearer" });
7940
- return;
7941
- }
7942
- const idempotencyKey = req.headers["idempotency-key"];
7943
- if (typeof idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)) {
7944
- refuse(res, 400, "缺少或不合法的 Idempotency-Key 请求头(1-200 个可见 ASCII 字符)");
7945
- return;
7946
- }
7947
- if ((req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") {
7948
- refuse(res, 415, "Content-Type 必须是 application/json");
7949
- return;
7950
- }
7951
- let raw;
7952
- try {
7953
- raw = JSON.parse(await readBody(req));
7954
- } catch (error) {
7955
- if (error instanceof HttpError$1) {
7956
- refuse(res, error.status, error.message);
7957
- return;
7958
- }
7959
- refuse(res, 400, "请求体不是合法的 JSON");
7960
- return;
7961
- }
7962
- const parsed = parseAnalyzeRequest(raw);
7963
- if (!parsed.ok) {
7964
- refuse(res, 400, parsed.error);
7965
- return;
7966
- }
7967
- send(res, await handler.analyze(parsed.value, idempotencyKey));
7968
- }
7969
- /** Split `host:port` (`[v6]:port` allowed) into listen arguments. */
7970
- function parseListenAddress(listen) {
7971
- const separator = listen.lastIndexOf(":");
7972
- return {
7973
- host: listen.slice(0, separator).replace(/^\[(.*)\]$/u, "$1"),
7974
- port: Number(listen.slice(separator + 1))
7975
- };
7976
- }
7977
- /**
7978
- * Bind the analyze listener.
7979
- * @param listen - `host:port` address (`analyzeListen`).
7980
- * @param handler - the business side.
7981
- * @param warn - diagnostics sink for unexpected handler failures.
7982
- * @returns the bound server; rejects when the address cannot be bound.
7983
- */
7984
- async function startAnalyzeServer(listen, handler, warn) {
7985
- const server = createServer((req, res) => {
7986
- handleAnalyzeHttp(req, res, handler).catch((error) => {
7987
- warn("dsh-channels: analyze request failed: %s", String(error));
7988
- if (!res.headersSent) refuse(res, 500, `分析请求处理失败:${String(error)}`);
7989
- else res.destroy();
7990
- });
7991
- });
7992
- const { host, port } = parseListenAddress(listen);
7993
- await new Promise((resolve, reject) => {
7994
- const onError = (error) => {
7995
- reject(error);
7996
- };
7997
- server.once("error", onError);
7998
- server.listen(port, host, () => {
7999
- server.off("error", onError);
8000
- resolve();
8001
- });
8002
- });
8003
- server.on("error", (error) => {
8004
- warn("dsh-channels: analyze listener error: %s", String(error));
8005
- });
8006
- return {
8007
- address: server.address(),
8008
- close: () => new Promise((resolve) => {
8009
- server.close(() => {
8010
- resolve();
8011
- });
8012
- server.closeIdleConnections();
8013
- })
8014
- };
8015
- }
8016
- //#endregion
8017
7894
  //#region src/session/repair.ts
8018
7895
  /**
8019
7896
  * In-flight turn snapshots and the startup repair pass.
@@ -8271,17 +8148,44 @@ function foldRequestPolicy(events) {
8271
8148
  model
8272
8149
  };
8273
8150
  }
8151
+ function sameModel(left, right) {
8152
+ return left?.provider === right?.provider && left?.model === right?.model;
8153
+ }
8154
+ /**
8155
+ * Record one effective policy as the Agent's next-step selection. A policy
8156
+ * without a model keeps the current route, except right after a model
8157
+ * override is cleared, when the Agent's live options (deployment default)
8158
+ * supply it again. Without a resolvable provider and model nothing is recorded.
8159
+ */
8160
+ function selectPolicy(model, agent, effort, target, modelCleared) {
8161
+ const current = model.current();
8162
+ const base = modelCleared && agent.options.provider !== void 0 && agent.options.model !== void 0 ? {
8163
+ provider: agent.options.provider,
8164
+ model: agent.options.model
8165
+ } : current;
8166
+ const provider = target?.provider ?? base?.provider;
8167
+ const modelId = target?.model ?? base?.model;
8168
+ if (provider === void 0 || provider === "" || modelId === void 0 || modelId === "") return;
8169
+ model.select({
8170
+ provider,
8171
+ model: modelId,
8172
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
8173
+ });
8174
+ }
8274
8175
  /** Install the mutable request policy outside every other request wrapper. */
8275
8176
  function installRequestPolicy(agentCtx, config = {}) {
8276
8177
  let effortOverride;
8277
8178
  let modelOverride;
8278
8179
  let disposed = false;
8180
+ /** Last policy recorded through `agent.model`; undefined until the first selecting sync. */
8181
+ let applied;
8279
8182
  const configuredModel = config.provider !== void 0 && config.model !== void 0 ? {
8280
8183
  provider: config.provider,
8281
8184
  model: config.model
8282
8185
  } : void 0;
8283
- const disposeListener = agentCtx.on("agent/request", async (_payload, next) => {
8186
+ const disposeListener = agentCtx.on("agent/request", async (payload, next) => {
8284
8187
  const request = await next();
8188
+ if (payload.agent?.model !== void 0) return request;
8285
8189
  const effort = effortOverride ?? config.effort;
8286
8190
  const model = modelOverride ?? configuredModel;
8287
8191
  if (effort === void 0 && model === void 0) return request;
@@ -8307,10 +8211,24 @@ function installRequestPolicy(agentCtx, config = {}) {
8307
8211
  modelSource: modelOverride !== void 0 ? "session" : configuredModel !== void 0 ? "config" : "deployment-default"
8308
8212
  };
8309
8213
  },
8310
- sync(events) {
8214
+ sync(events, agent, reconcile) {
8311
8215
  const folded = foldRequestPolicy(events);
8312
8216
  effortOverride = folded.effort;
8313
8217
  modelOverride = folded.model;
8218
+ const model = agent?.model;
8219
+ if (agent === void 0 || model === void 0 || disposed) return;
8220
+ const next = {
8221
+ effort: effortOverride ?? config.effort,
8222
+ model: modelOverride ?? configuredModel
8223
+ };
8224
+ const previous = applied ?? {
8225
+ effort: void 0,
8226
+ model: void 0
8227
+ };
8228
+ applied = next;
8229
+ if (reconcile === void 0 && previous.effort === next.effort && sameModel(previous.model, next.model)) return;
8230
+ const modelCleared = next.model === void 0 && (reconcile === "model" || previous.model !== void 0);
8231
+ selectPolicy(model, agent, next.effort, next.model, modelCleared);
8314
8232
  },
8315
8233
  dispose() {
8316
8234
  if (disposed) return;
@@ -8456,7 +8374,7 @@ var SessionManager = class {
8456
8374
  }
8457
8375
  /** Borrow one foreign live agent and mount this channel's scoped policy. */
8458
8376
  async borrow(sessionId, agent) {
8459
- const borrowed = borrowLiveAgent(agent, this.mountRequestPolicy(agent.ctx, agent.session));
8377
+ const borrowed = borrowLiveAgent(agent, this.mountRequestPolicy(agent.ctx, agent));
8460
8378
  this.live.set(sessionId, borrowed);
8461
8379
  await this.mountToolsFor(agent.ctx, sessionId);
8462
8380
  return borrowed;
@@ -8490,13 +8408,13 @@ var SessionManager = class {
8490
8408
  }
8491
8409
  }
8492
8410
  /** Mount one mutable Session-aware request policy outside every other wrapper. */
8493
- mountRequestPolicy(agentCtx, session) {
8411
+ mountRequestPolicy(agentCtx, agent) {
8494
8412
  const controller = installRequestPolicy(agentCtx, {
8495
8413
  effort: this.deps.config.reasoningEffort,
8496
8414
  provider: this.deps.config.provider,
8497
8415
  model: this.deps.config.model
8498
8416
  });
8499
- if (session !== void 0) controller.sync(session.snapshotEvents());
8417
+ if (agent !== void 0) controller.sync(agent.session.snapshotEvents(), agent);
8500
8418
  this.policies.set(agentCtx, controller);
8501
8419
  return () => {
8502
8420
  controller.dispose();
@@ -8507,7 +8425,7 @@ var SessionManager = class {
8507
8425
  requestPolicy(agent) {
8508
8426
  const controller = this.policies.get(agent.ctx);
8509
8427
  if (controller === void 0) throw new Error("dsh-channels: request policy is not mounted for this Agent");
8510
- controller.sync(agent.session.snapshotEvents());
8428
+ controller.sync(agent.session.snapshotEvents(), agent);
8511
8429
  return controller;
8512
8430
  }
8513
8431
  /**
@@ -12638,7 +12556,7 @@ async function read$1(kit, sessionId, rawArgs, signal) {
12638
12556
  };
12639
12557
  });
12640
12558
  }
12641
- function json(value) {
12559
+ function json$1(value) {
12642
12560
  return preview(JSON.stringify(value) ?? "null", 120);
12643
12561
  }
12644
12562
  function cardLines$1(operations, tableName) {
@@ -12646,7 +12564,7 @@ function cardLines$1(operations, tableName) {
12646
12564
  const n = `${index + 1}.`;
12647
12565
  switch (operation.command) {
12648
12566
  case "create": {
12649
- const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${json(record)}`);
12567
+ const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${json$1(record)}`);
12650
12568
  const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : [];
12651
12569
  return [
12652
12570
  `${n} 新增 ${operation.records.length} 条记录`,
@@ -12655,7 +12573,7 @@ function cardLines$1(operations, tableName) {
12655
12573
  ].join("\n");
12656
12574
  }
12657
12575
  case "update": {
12658
- const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${preview(record.record_id, 40)}:${json(record.fields)}`);
12576
+ const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${preview(record.record_id, 40)}:${json$1(record.fields)}`);
12659
12577
  const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : [];
12660
12578
  return [
12661
12579
  `${n} 修改 ${operation.records.length} 条记录`,
@@ -14241,7 +14159,7 @@ function foldHealthSnapshot(input) {
14241
14159
  const connectionState = input.connection?.state;
14242
14160
  let level;
14243
14161
  if (!input.started || dependencyMissing || connectionState === "failed" || fibers.failed > 0) level = "unhealthy";
14244
- else if (connectionState !== "connected" || fibers.pending > 0 || fibers.loading > 0 || fibers.unloading > 0 || input.analyzeListenError !== void 0) level = "degraded";
14162
+ else if (connectionState !== "connected" || fibers.pending > 0 || fibers.loading > 0 || fibers.unloading > 0) level = "degraded";
14245
14163
  else level = "healthy";
14246
14164
  const reasons = [];
14247
14165
  if (!input.started) reasons.push("飞书通道尚未启动");
@@ -14256,7 +14174,6 @@ function foldHealthSnapshot(input) {
14256
14174
  const stalledLabels = inactive.filter((issue) => issue.state === "pending" || issue.state === "loading" || issue.state === "unloading").map((issue) => issue.label);
14257
14175
  reasons.push(`宿主其它插件仍有 ${stalled} 个入口在等待依赖或加载/卸载(与飞书通道无关)` + (stalledLabels.length === 0 ? "" : `:${labelList(stalledLabels, stalled)}`));
14258
14176
  }
14259
- if (input.analyzeListenError !== void 0) reasons.push(`日志分析接口监听失败:${input.analyzeListenError}`);
14260
14177
  return {
14261
14178
  level,
14262
14179
  fibers,
@@ -14267,7 +14184,6 @@ function foldHealthSnapshot(input) {
14267
14184
  routeCount: input.routeCount,
14268
14185
  liveAgentCount: input.liveAgentCount,
14269
14186
  inflightTurnCount: input.inflightTurnCount,
14270
- ...input.analyzeListenError === void 0 ? {} : { analyzeListenError: input.analyzeListenError },
14271
14187
  inactiveFibers: inactive,
14272
14188
  inactiveTotal,
14273
14189
  reasons
@@ -14319,11 +14235,7 @@ function buildHealthCard(snapshot) {
14319
14235
  {
14320
14236
  tag: "markdown",
14321
14237
  content: `**dsh-channels**\n${snapshot.started ? "started" : "stopped"} · 持久化路由 ${snapshot.routeCount} · live Agent ${snapshot.liveAgentCount} · in-flight turn ${snapshot.inflightTurnCount}`
14322
- },
14323
- ...snapshot.analyzeListenError === void 0 ? [] : [{
14324
- tag: "markdown",
14325
- content: `**日志分析接口**\n❌ 监听失败:${snapshot.analyzeListenError}`
14326
- }]
14238
+ }
14327
14239
  ]);
14328
14240
  }
14329
14241
  //#endregion
@@ -15238,7 +15150,7 @@ var TurnStreamer = class {
15238
15150
  const fold = this.folds.get(session.id);
15239
15151
  if (fold !== void 0) foldEvent(fold, event);
15240
15152
  this.sessions.set(session.id, session);
15241
- const parentId = session.header.parentSession;
15153
+ const parentId = session.header.origin === "subagent" ? session.header.parentSession : void 0;
15242
15154
  if (parentId !== void 0) {
15243
15155
  const parentState = this.front(parentId);
15244
15156
  if (parentState !== void 0) {
@@ -15985,8 +15897,6 @@ const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 2e4;
15985
15897
  const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3e3;
15986
15898
  /** Conventional credential reference holding the log-platform analyze API bearer token. */
15987
15899
  const DEFAULT_ANALYZE_TOKEN_REF = "DSH_CHANNELS_ANALYZE_TOKEN";
15988
- /** `host:port` listen address of the analyze API (`[v6]:port` for IPv6 literals). */
15989
- const ANALYZE_LISTEN_PATTERN = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+):(?:[1-9][0-9]{0,4})$/u;
15990
15900
  /** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
15991
15901
  const credentialRefSchema = z.string().regex(CREDENTIAL_REF_PATTERN, "must be a credential reference name such as FEISHU_APP_ID (resolved through the dsh credentials seam, e.g. $DSH_HOME/.credentials.yaml) — plaintext secrets are not accepted");
15992
15902
  const configSchema = z.object({
@@ -16318,13 +16228,11 @@ const configSchema = z.object({
16318
16228
  */
16319
16229
  questionTimeoutMs: z.number().int().min(1e4).max(36e5).default(DEFAULT_QUESTION_TIMEOUT_MS),
16320
16230
  /**
16321
- * Listen address (`host:port`, e.g. `0.0.0.0:3090`) of the log-platform
16322
- * analyze API: a dedicated HTTP listener serving only
16323
- * `POST /dsh-channels/analyze`, separate from the DSH Web port. Unset
16324
- * (default) keeps the API off. The listener runs while the Feishu connection
16325
- * is up and is rebound on reconnect.
16231
+ * Switch for the log-platform analyze API (`POST /api/dsh-channels.logalyze`
16232
+ * on the DSH Web server). Off (default) answers 404; read per request, so
16233
+ * toggling it needs no reconnect. Needs a host with a Web server.
16326
16234
  */
16327
- analyzeListen: z.string().trim().regex(ANALYZE_LISTEN_PATTERN, "must be host:port, e.g. 0.0.0.0:3090 or 127.0.0.1:3090").refine((value) => Number(value.slice(value.lastIndexOf(":") + 1)) <= 65535, "port must be 1-65535").optional(),
16235
+ analyzeEnabled: z.boolean().default(false),
16328
16236
  /**
16329
16237
  * Credential reference holding the analyze API bearer token (defaults to
16330
16238
  * `DSH_CHANNELS_ANALYZE_TOKEN`). Resolved per request, so rotating the value
@@ -16364,29 +16272,45 @@ function resolveConfig(raw) {
16364
16272
  *
16365
16273
  * @module dsh-channels/settings/contract
16366
16274
  */
16367
- /** Same-origin routes the settings panel talks to. */
16368
- const STATUS_ROUTE = "/dsh-channels/status";
16369
- const PANEL_ROUTE = "/dsh-channels/panel";
16370
- const CONFIG_ROUTE = "/dsh-channels/config";
16371
- const CREDENTIALS_ROUTE = "/dsh-channels/credentials";
16275
+ /**
16276
+ * Settings panel routes: exact DSH `/api` Fetch routes named
16277
+ * `/api/dsh-channels.<method>`. Reads are GET; every write is POST, because
16278
+ * DSH exact `/api` routes accept only GET, HEAD, and POST.
16279
+ */
16280
+ const STATUS_ROUTE = "/api/dsh-channels.status";
16281
+ const PANEL_ROUTE = "/api/dsh-channels.panel";
16282
+ /** Persist a partial config edit (POST, body `{ values }`). */
16283
+ const SAVE_CONFIG_ROUTE = "/api/dsh-channels.saveConfig";
16284
+ /** Drop every Web override (POST, no body). */
16285
+ const CLEAR_CONFIG_ROUTE = "/api/dsh-channels.clearConfig";
16286
+ /** Store credential values (POST, body `SaveCredentialsRequest`). */
16287
+ const SAVE_CREDENTIALS_ROUTE = "/api/dsh-channels.saveCredentials";
16288
+ /** Remove one stored credential (POST, body `{ field }`). */
16289
+ const CLEAR_CREDENTIAL_ROUTE = "/api/dsh-channels.clearCredential";
16372
16290
  /**
16373
16291
  * Connection lifecycle action (POST). While the channel runs it restarts the
16374
16292
  * long connection; while it is stopped it opens one — so the panel has exactly
16375
16293
  * one "connect / reconnect" button. Credentials are resolved again either way.
16376
16294
  */
16377
- const RECONNECT_ROUTE = "/dsh-channels/reconnect";
16295
+ const RECONNECT_ROUTE = "/api/dsh-channels.reconnect";
16378
16296
  /** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
16379
- const CATALOG_ROUTE = "/dsh-channels/catalog";
16380
- /** 访问与授权只读快照(GET/HEAD):准入模式、管理员、群/用户/知识库白名单、待处理申请。 */
16381
- const ACCESS_ROUTE = "/dsh-channels/access";
16382
- /** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
16383
- const ACCESS_GROUPS_ROUTE = "/dsh-channels/access/groups";
16384
- /** 用户白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
16385
- const ACCESS_USERS_ROUTE = "/dsh-channels/access/users";
16386
- /** 知识库白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
16387
- const ACCESS_WIKI_USERS_ROUTE = "/dsh-channels/access/wiki-users";
16297
+ const CATALOG_ROUTE = "/api/dsh-channels.catalog";
16298
+ /** 访问与授权只读快照(GET):准入模式、管理员、群/用户/知识库白名单、待处理申请。 */
16299
+ const ACCESS_ROUTE = "/api/dsh-channels.access";
16300
+ /** 授权一个群(POST,body `{ chatId, name? }`)。 */
16301
+ const ADD_ACCESS_GROUP_ROUTE = "/api/dsh-channels.addAccessGroup";
16302
+ /** 撤销一个群(POST,body `{ chatId }`)。 */
16303
+ const REMOVE_ACCESS_GROUP_ROUTE = "/api/dsh-channels.removeAccessGroup";
16304
+ /** 用户白名单添加(POST,body `{ userId, name? }`)。 */
16305
+ const ADD_ACCESS_USER_ROUTE = "/api/dsh-channels.addAccessUser";
16306
+ /** 用户白名单移除(POST,body `{ userId }`)。 */
16307
+ const REMOVE_ACCESS_USER_ROUTE = "/api/dsh-channels.removeAccessUser";
16308
+ /** 知识库白名单添加(POST,body `{ userId, name? }`)。 */
16309
+ const ADD_WIKI_USER_ROUTE = "/api/dsh-channels.addWikiUser";
16310
+ /** 知识库白名单移除(POST,body `{ userId }`)。 */
16311
+ const REMOVE_WIKI_USER_ROUTE = "/api/dsh-channels.removeWikiUser";
16388
16312
  /** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
16389
- const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
16313
+ const RESOLVE_ACCESS_REQUEST_ROUTE = "/api/dsh-channels.resolveAccessRequest";
16390
16314
  /**
16391
16315
  * 打开宿主机的系统目录选择框(POST,无请求体),返回绝对路径。
16392
16316
  *
@@ -16394,15 +16318,15 @@ const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
16394
16318
  * 会注册实际选择逻辑;远程/无头会话的 browse 后端不可用,面板不下发
16395
16319
  * [`EditableFieldWire.browse`],保留手写输入。
16396
16320
  */
16397
- const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
16398
- /** 会话的飞书来源(GET/HEAD,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
16399
- const SESSION_ORIGIN_ROUTE = "/dsh-channels/session-origin";
16321
+ const PICK_DIRECTORY_ROUTE = "/api/dsh-channels.pickDirectory";
16322
+ /** 会话的飞书来源(GET,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
16323
+ const SESSION_ORIGIN_ROUTE = "/api/dsh-channels.sessionOrigin";
16400
16324
  /**
16401
16325
  * `GET PROFILES_ROUTE?users=<user_id,...>&chats=<chat_id,...>`: live Feishu
16402
16326
  * names and avatars for the users and chats the panel shows. At most
16403
16327
  * {@link PROFILES_MAX_IDS} ids per list.
16404
16328
  */
16405
- const PROFILES_ROUTE = "/dsh-channels/profiles";
16329
+ const PROFILES_ROUTE = "/api/dsh-channels.profiles";
16406
16330
  /**
16407
16331
  * 可由 Web 面板覆盖的配置项。顶层键对应 `src/config.ts` 的字段;`feishuTools.*`
16408
16332
  * 是嵌套字段(覆盖层里仍写成 `{ "feishuTools": { ... } }` 对象)。覆盖值写进
@@ -16410,7 +16334,7 @@ const PROFILES_ROUTE = "/dsh-channels/profiles";
16410
16334
  * `cordis.patch.yml`(见 README「Web 设置面板」)。
16411
16335
  *
16412
16336
  * 刻意不包含 `appId` / `appSecret`:它们是**凭据引用名**,面板改为直接
16413
- * 编辑引用背后的值(`PUT /dsh-channels/credentials`,经 DSH 凭据服务写入),
16337
+ * 编辑引用背后的值(`POST /api/dsh-channels.saveCredentials`,经 DSH 凭据服务写入),
16414
16338
  * 因此不会落进覆盖层文件。
16415
16339
  *
16416
16340
  * `baseDir`(工作区根目录 = 模型/工具产生文件的落点)可覆盖,但面板只接受
@@ -16460,7 +16384,7 @@ const EDITABLE_KEYS = [
16460
16384
  "feishuTools.wikiRead",
16461
16385
  "feishuTools.wikiWrite",
16462
16386
  "wikiRoots",
16463
- "analyzeListen",
16387
+ "analyzeEnabled",
16464
16388
  "loggerLevel"
16465
16389
  ];
16466
16390
  /**
@@ -16471,7 +16395,6 @@ const RESTART_KEYS = [
16471
16395
  "dmMode",
16472
16396
  "requireMention",
16473
16397
  "slashCommandPanel",
16474
- "analyzeListen",
16475
16398
  "refreshMs",
16476
16399
  "statusRefreshMs",
16477
16400
  "loggerLevel"
@@ -17076,10 +16999,6 @@ var FeishuService = class {
17076
16999
  inbound;
17077
17000
  /** Log-platform analyze API business side (requests → shared analysis topics). */
17078
17001
  analyzeLauncher;
17079
- /** Bound analyze listener while connected with `analyzeListen` set. */
17080
- analyzeServer;
17081
- /** Why the analyze listener could not bind at the last connect. */
17082
- analyzeListenError;
17083
17002
  /** Agent/session lifecycle: live registry, resume, presets, renew. */
17084
17003
  sessions;
17085
17004
  /**
@@ -17628,6 +17547,10 @@ var FeishuService = class {
17628
17547
  this.restarting = false;
17629
17548
  }
17630
17549
  }
17550
+ /** The analyze API's business side, for the Web route. */
17551
+ analyzeHandler() {
17552
+ return this.analyzeLauncher;
17553
+ }
17631
17554
  isRunning() {
17632
17555
  return this.started;
17633
17556
  }
@@ -18316,7 +18239,7 @@ var FeishuService = class {
18316
18239
  this.runScopeCheck(channel, appId, ownerLookup);
18317
18240
  if (this.config.slashCommandPanel) this.runSlashCommandSync(channel);
18318
18241
  await this.analyzeLauncher.markInterrupted();
18319
- await this.openAnalyzeListener();
18242
+ this.analyzeLauncher.open();
18320
18243
  this.repair.repairAll().catch((error) => {
18321
18244
  this.ctx.logger?.warn?.("dsh-channels: inflight repair failed: %s", String(error));
18322
18245
  });
@@ -18348,29 +18271,10 @@ var FeishuService = class {
18348
18271
  const questionsMounted = this.ctx.get?.("userQuestions") !== void 0;
18349
18272
  console.info(questionsMounted ? `[dsh-channels] ask_user_question 应答已就绪(提问卡片超时 ${this.config.questionTimeoutMs} ms)` : "[dsh-channels] 主机未挂载 userQuestions 服务,结构化提问应答关闭");
18350
18273
  }
18351
- /**
18352
- * Bind the analyze listener when `analyzeListen` is set. A bind failure is
18353
- * logged and folded into the health snapshot (panel reasons, `/health`); the Feishu connection stays up.
18354
- */
18355
- async openAnalyzeListener() {
18356
- this.analyzeListenError = void 0;
18357
- const listen = this.config.analyzeListen;
18358
- if (listen === void 0) return;
18359
- try {
18360
- this.analyzeServer = await startAnalyzeServer(listen, this.analyzeLauncher, (message, ...args) => this.ctx.logger?.warn?.(message, ...args));
18361
- console.info("[dsh-channels] 日志分析接口已监听", listen);
18362
- } catch (error) {
18363
- this.analyzeListenError = messageOf$3(error);
18364
- this.ctx.logger?.error?.("dsh-channels: analyze listener on %s failed: %s", listen, this.analyzeListenError);
18365
- console.error("[dsh-channels] 日志分析接口监听失败:", listen, error);
18366
- }
18367
- }
18368
18274
  /** Tear down: close the connection and dispose all live agents. */
18369
18275
  async shutdown() {
18370
18276
  this.started = false;
18371
- const analyzeServer = this.analyzeServer;
18372
- this.analyzeServer = void 0;
18373
- await analyzeServer?.close();
18277
+ this.analyzeLauncher.close();
18374
18278
  await this.analyzeLauncher.drain();
18375
18279
  for (const controller of this.commandControllers) controller.abort();
18376
18280
  await Promise.allSettled([...this.commandOperations]);
@@ -19053,8 +18957,9 @@ var FeishuService = class {
19053
18957
  /** Render one settled agent-plane command result. */
19054
18958
  async renderCommandResult(input) {
19055
18959
  const { spec, msg, current, live, result, compactBefore, renewAdmission, effortAdmission, modelAdmission } = input;
19056
- if (spec.name === "effort") effortAdmission?.controller.sync(live.agent.session.snapshotEvents());
19057
- if (spec.name === "model") modelAdmission?.controller.sync(live.agent.session.snapshotEvents());
18960
+ const reconcile = !input.bare && result.kind === "success" ? spec.name : void 0;
18961
+ if (spec.name === "effort") effortAdmission?.controller.sync(live.agent.session.snapshotEvents(), live.agent, reconcile === "effort" ? "effort" : void 0);
18962
+ if (spec.name === "model") modelAdmission?.controller.sync(live.agent.session.snapshotEvents(), live.agent, reconcile === "model" ? "model" : void 0);
19058
18963
  if (input.bare && result.kind === "success") {
19059
18964
  const picker = spec.name === "model" && modelAdmission !== void 0 ? await this.modelPicker(current, live, modelAdmission) : spec.name === "effort" && effortAdmission !== void 0 ? await this.effortPicker(current, live, effortAdmission, result.text ?? "") : void 0;
19060
18965
  if (picker !== void 0) {
@@ -19251,8 +19156,7 @@ var FeishuService = class {
19251
19156
  started: this.started,
19252
19157
  routeCount: this.routes.size(),
19253
19158
  liveAgentCount: this.sessions.liveCount(),
19254
- inflightTurnCount,
19255
- ...this.analyzeListenError === void 0 ? {} : { analyzeListenError: this.analyzeListenError }
19159
+ inflightTurnCount
19256
19160
  });
19257
19161
  }
19258
19162
  /**
@@ -19565,10 +19469,11 @@ var FeishuService = class {
19565
19469
  /** The model route the next request of a Session uses, as far as it is known. */
19566
19470
  function currentModelRoute(live, controller) {
19567
19471
  const state = controller.state;
19472
+ const selected = live.agent.model?.current();
19568
19473
  const header = live.agent.session.requestHeader?.()?.config;
19569
19474
  return {
19570
- provider: state.effectiveModel?.provider ?? (typeof header?.provider === "string" && header.provider !== "" ? header.provider : void 0),
19571
- model: state.effectiveModel?.model ?? (typeof header?.model === "string" && header.model !== "" ? header.model : void 0)
19475
+ provider: state.effectiveModel?.provider ?? selected?.provider ?? (typeof header?.provider === "string" && header.provider !== "" ? header.provider : void 0),
19476
+ model: state.effectiveModel?.model ?? selected?.model ?? (typeof header?.model === "string" && header.model !== "" ? header.model : void 0)
19572
19477
  };
19573
19478
  }
19574
19479
  /** `**当前模型**:…` line shared by the `/model` text answer and its picker. */
@@ -19584,463 +19489,338 @@ const MAX_BODY_BYTES = 64 * 1024;
19584
19489
  function messageOf$2(error) {
19585
19490
  return error instanceof Error ? error.message : String(error);
19586
19491
  }
19587
- var HttpError = class extends Error {
19492
+ var HttpError$1 = class extends Error {
19588
19493
  status;
19589
19494
  constructor(status, message) {
19590
19495
  super(message);
19591
19496
  this.status = status;
19592
19497
  }
19593
19498
  };
19594
- function methodOf(req) {
19595
- return (req.method ?? "GET").toUpperCase();
19596
- }
19597
- function sendJson(res, status, payload) {
19598
- const body = JSON.stringify(payload);
19599
- res.writeHead(status, {
19600
- "content-type": "application/json; charset=utf-8",
19601
- "cache-control": "no-store",
19602
- "content-length": Buffer.byteLength(body)
19603
- });
19604
- res.end(body);
19605
- }
19606
- /** Reject non-allowed methods with 405 + Allow, returning false. */
19607
- function allowOnly(req, res, allowed) {
19608
- const method = methodOf(req);
19609
- if (allowed.includes(method)) return true;
19610
- const body = JSON.stringify({
19611
- ok: false,
19612
- error: `方法 ${method} 不受支持`
19613
- });
19614
- res.writeHead(405, {
19615
- allow: allowed.join(", "),
19616
- "content-type": "application/json; charset=utf-8",
19617
- "cache-control": "no-store",
19618
- "content-length": Buffer.byteLength(body)
19499
+ function json(status, payload) {
19500
+ return new Response(JSON.stringify(payload), {
19501
+ status,
19502
+ headers: {
19503
+ "content-type": "application/json; charset=utf-8",
19504
+ "cache-control": "no-store"
19505
+ }
19619
19506
  });
19620
- res.end(body);
19621
- return false;
19622
19507
  }
19623
- /**
19624
- * Mutating requests must come from this same origin. Missing Origin is allowed
19625
- * (non-browser local clients, e.g. curl); a present-but-mismatched Origin is
19626
- * a cross-site write and is refused.
19627
- */
19628
- function isSameOriginRequest(req) {
19629
- const origin = req.headers.origin;
19630
- if (origin === void 0 || origin === "") return true;
19631
- const host = req.headers.host;
19632
- if (host === void 0) return false;
19633
- try {
19634
- return new URL(origin).host === host;
19635
- } catch {
19636
- return false;
19637
- }
19638
- }
19639
- async function readJsonBody(req) {
19640
- if ((req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") throw new HttpError(415, "请求体必须是 application/json");
19641
- const chunks = [];
19642
- let size = 0;
19643
- for await (const chunk of req) {
19644
- const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
19645
- size += buffer.length;
19646
- if (size > MAX_BODY_BYTES) throw new HttpError(413, "请求体过大");
19647
- chunks.push(buffer);
19648
- }
19649
- if (size === 0) throw new HttpError(400, "请求体为空");
19508
+ /** Parse a JSON-object body; malformed bodies throw {@link HttpError} (415/413/400). */
19509
+ async function readJsonObject(request) {
19510
+ if ((request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") throw new HttpError$1(415, "请求体必须是 application/json");
19511
+ const text = await request.text();
19512
+ if (Buffer.byteLength(text) > MAX_BODY_BYTES) throw new HttpError$1(413, "请求体过大");
19513
+ if (text === "") throw new HttpError$1(400, "请求体为空");
19514
+ let body;
19650
19515
  try {
19651
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
19516
+ body = JSON.parse(text);
19652
19517
  } catch {
19653
- throw new HttpError(400, "请求体不是合法 JSON");
19518
+ throw new HttpError$1(400, "请求体不是合法 JSON");
19654
19519
  }
19520
+ if (typeof body !== "object" || body === null || Array.isArray(body)) throw new HttpError$1(400, "请求体必须是 JSON 对象");
19521
+ return body;
19655
19522
  }
19656
19523
  /** Distinct non-empty entries of one comma-separated id query parameter. */
19657
19524
  function idList(raw) {
19658
19525
  if (raw === null) return [];
19659
19526
  return [...new Set(raw.split(",").map((id) => id.trim()).filter((id) => id !== ""))];
19660
19527
  }
19661
- /**
19662
- * Parse one JSON-object body for an access write. HttpErrors are answered
19663
- * here (415/413/400); `undefined` means "already responded, stop".
19664
- */
19665
- async function readAccessBody(req, res) {
19666
- let body;
19667
- try {
19668
- body = await readJsonBody(req);
19669
- } catch (error) {
19670
- if (error instanceof HttpError) {
19671
- sendJson(res, error.status, {
19672
- ok: false,
19673
- error: error.message
19674
- });
19675
- return;
19676
- }
19677
- throw error;
19678
- }
19679
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
19680
- sendJson(res, 400, {
19681
- ok: false,
19682
- error: "请求体必须是 JSON 对象"
19683
- });
19684
- return;
19685
- }
19686
- return body;
19528
+ /** A write's answer: 200 on success, 400 for a refusal the body explains. */
19529
+ function result(outcome) {
19530
+ return json(outcome.ok ? 200 : 400, outcome);
19687
19531
  }
19688
- /**
19689
- * Wrap a handler so the host gate runs before anything else and thrown
19690
- * errors become JSON instead of a dropped socket.
19691
- */
19692
- function guardedBy(gate, handler) {
19693
- return async (req, res) => {
19694
- const rejection = gate.requestRejection(req);
19695
- if (rejection !== void 0) {
19696
- sendJson(res, rejection, {
19697
- ok: false,
19698
- error: rejection === 401 ? "未登录 DSH,请从 DSH 启动链接打开页面" : "请求来源不受信任"
19699
- });
19700
- return;
19701
- }
19532
+ /** Turn thrown errors into JSON answers instead of a dropped request. */
19533
+ function answered(handler) {
19534
+ return async (request) => {
19702
19535
  try {
19703
- await handler(req, res);
19536
+ return await handler(request);
19704
19537
  } catch (error) {
19705
- if (res.headersSent) {
19706
- res.end();
19707
- return;
19708
- }
19709
- sendJson(res, error instanceof HttpError ? error.status : 500, {
19538
+ if (error instanceof HttpError$1) return json(error.status, {
19710
19539
  ok: false,
19711
- error: error instanceof HttpError ? error.message : `内部错误:${messageOf$2(error)}`
19540
+ error: error.message
19541
+ });
19542
+ return json(500, {
19543
+ ok: false,
19544
+ error: `内部错误:${messageOf$2(error)}`
19712
19545
  });
19713
19546
  }
19714
19547
  };
19715
19548
  }
19716
- function forbiddenWrite(res) {
19717
- sendJson(res, 403, {
19718
- ok: false,
19719
- error: "跨站写入被拒绝(Origin 与 Host 不一致)"
19549
+ /**
19550
+ * Register the panel routes.
19551
+ * @param connection - the host `connection` service.
19552
+ * @param surface - the panel's business side.
19553
+ * @returns disposer removing every route.
19554
+ */
19555
+ function registerSettingsRoutes(connection, surface) {
19556
+ const get = (path, handler) => ({
19557
+ path,
19558
+ methods: ["GET"],
19559
+ requestBody: "buffered",
19560
+ fetch: answered((request) => handler(new URL(request.url)))
19561
+ });
19562
+ const post = (path, handler) => ({
19563
+ path,
19564
+ methods: ["POST"],
19565
+ requestBody: "buffered",
19566
+ fetch: answered(handler)
19720
19567
  });
19721
- }
19722
- /** Register the panel routes; the returned disposer removes all of them. */
19723
- function registerSettingsRoutes(server, gate, surface) {
19724
- const guarded = (handler) => guardedBy(gate, handler);
19725
19568
  const disposers = [
19726
- server.register({
19727
- kind: "exact",
19728
- path: STATUS_ROUTE,
19729
- handler: guarded(async (req, res) => {
19730
- if (!allowOnly(req, res, ["GET", "HEAD"])) return;
19731
- if (methodOf(req) === "HEAD") {
19732
- res.writeHead(200, { "cache-control": "no-store" });
19733
- res.end();
19734
- return;
19735
- }
19736
- sendJson(res, 200, await surface.status());
19737
- })
19738
- }),
19739
- server.register({
19740
- kind: "exact",
19741
- path: PANEL_ROUTE,
19742
- handler: guarded(async (req, res) => {
19743
- if (!allowOnly(req, res, ["GET", "HEAD"])) return;
19744
- if (methodOf(req) === "HEAD") {
19745
- res.writeHead(200, { "cache-control": "no-store" });
19746
- res.end();
19747
- return;
19748
- }
19749
- sendJson(res, 200, await surface.panel());
19750
- })
19751
- }),
19752
- server.register({
19753
- kind: "exact",
19754
- path: CATALOG_ROUTE,
19755
- handler: guarded(async (req, res) => {
19756
- if (!allowOnly(req, res, ["GET", "HEAD"])) return;
19757
- if (methodOf(req) === "HEAD") {
19758
- res.writeHead(200, { "cache-control": "no-store" });
19759
- res.end();
19760
- return;
19761
- }
19762
- const url = new URL(req.url ?? "/", "http://dsh.local");
19763
- sendJson(res, 200, await surface.catalogFor(url.searchParams.get("provider") ?? void 0, url.searchParams.get("model") ?? void 0));
19764
- })
19569
+ get(STATUS_ROUTE, async () => json(200, await surface.status())),
19570
+ get(PANEL_ROUTE, async () => json(200, await surface.panel())),
19571
+ get(CATALOG_ROUTE, async (url) => json(200, await surface.catalogFor(url.searchParams.get("provider") ?? void 0, url.searchParams.get("model") ?? void 0))),
19572
+ get(SESSION_ORIGIN_ROUTE, async (url) => {
19573
+ const sessionId = url.searchParams.get("sessionId")?.trim();
19574
+ if (sessionId === void 0 || sessionId === "") return json(400, {
19575
+ ok: false,
19576
+ error: "缺少 sessionId 参数"
19577
+ });
19578
+ return json(200, await surface.sessionOrigin(sessionId));
19765
19579
  }),
19766
- server.register({
19767
- kind: "exact",
19768
- path: SESSION_ORIGIN_ROUTE,
19769
- handler: guarded(async (req, res) => {
19770
- if (!allowOnly(req, res, ["GET", "HEAD"])) return;
19771
- if (methodOf(req) === "HEAD") {
19772
- res.writeHead(200, { "cache-control": "no-store" });
19773
- res.end();
19774
- return;
19775
- }
19776
- const sessionId = new URL(req.url ?? "/", "http://dsh.local").searchParams.get("sessionId")?.trim();
19777
- if (sessionId === void 0 || sessionId === "") {
19778
- sendJson(res, 400, {
19779
- ok: false,
19780
- error: "缺少 sessionId 参数"
19781
- });
19782
- return;
19783
- }
19784
- sendJson(res, 200, await surface.sessionOrigin(sessionId));
19785
- })
19580
+ get(PROFILES_ROUTE, async (url) => {
19581
+ const userIds = idList(url.searchParams.get("users"));
19582
+ const chatIds = idList(url.searchParams.get("chats"));
19583
+ if (userIds.length > 200 || chatIds.length > 200) return json(400, {
19584
+ ok: false,
19585
+ error: `每次最多查询 200 个用户和 200 个群`
19586
+ });
19587
+ return json(200, await surface.feishuProfiles(userIds, chatIds));
19786
19588
  }),
19787
- server.register({
19788
- kind: "exact",
19789
- path: PROFILES_ROUTE,
19790
- handler: guarded(async (req, res) => {
19791
- if (!allowOnly(req, res, ["GET", "HEAD"])) return;
19792
- if (methodOf(req) === "HEAD") {
19793
- res.writeHead(200, { "cache-control": "no-store" });
19794
- res.end();
19795
- return;
19796
- }
19797
- const params = new URL(req.url ?? "/", "http://dsh.local").searchParams;
19798
- const userIds = idList(params.get("users"));
19799
- const chatIds = idList(params.get("chats"));
19800
- if (userIds.length > 200 || chatIds.length > 200) {
19801
- sendJson(res, 400, {
19802
- ok: false,
19803
- error: `每次最多查询 200 个用户和 200 个群`
19804
- });
19805
- return;
19806
- }
19807
- sendJson(res, 200, await surface.feishuProfiles(userIds, chatIds));
19808
- })
19589
+ get(ACCESS_ROUTE, async () => json(200, await surface.access())),
19590
+ post(SAVE_CONFIG_ROUTE, async (request) => {
19591
+ const values = (await readJsonObject(request)).values;
19592
+ if (typeof values !== "object" || values === null || Array.isArray(values)) return json(400, {
19593
+ ok: false,
19594
+ error: "请求体缺少 values 对象"
19595
+ });
19596
+ return result(await surface.save(values));
19809
19597
  }),
19810
- server.register({
19811
- kind: "exact",
19812
- path: CONFIG_ROUTE,
19813
- handler: guarded(async (req, res) => {
19814
- if (!allowOnly(req, res, ["PUT", "DELETE"])) return;
19815
- if (!isSameOriginRequest(req)) {
19816
- forbiddenWrite(res);
19817
- return;
19818
- }
19819
- if (methodOf(req) === "DELETE") {
19820
- const result = await surface.clear();
19821
- sendJson(res, result.ok ? 200 : 400, result);
19822
- return;
19823
- }
19824
- let body;
19825
- try {
19826
- body = await readJsonBody(req);
19827
- } catch (error) {
19828
- if (error instanceof HttpError) {
19829
- sendJson(res, error.status, {
19830
- ok: false,
19831
- error: error.message
19832
- });
19833
- return;
19834
- }
19835
- throw error;
19836
- }
19837
- const values = body?.values;
19838
- if (typeof values !== "object" || values === null || Array.isArray(values)) {
19839
- sendJson(res, 400, {
19840
- ok: false,
19841
- error: "请求体缺少 values 对象"
19842
- });
19843
- return;
19844
- }
19845
- const result = await surface.save(values);
19846
- sendJson(res, result.ok ? 200 : 400, result);
19847
- })
19598
+ post(CLEAR_CONFIG_ROUTE, async () => result(await surface.clear())),
19599
+ post(SAVE_CREDENTIALS_ROUTE, async (request) => {
19600
+ const payload = await readJsonObject(request);
19601
+ const values = {};
19602
+ if (payload.appId !== void 0) values.appId = payload.appId;
19603
+ if (payload.appSecret !== void 0) values.appSecret = payload.appSecret;
19604
+ if (payload.analyzeToken !== void 0) values.analyzeToken = payload.analyzeToken;
19605
+ return result(await surface.saveCredentials(values));
19848
19606
  }),
19849
- server.register({
19850
- kind: "exact",
19851
- path: CREDENTIALS_ROUTE,
19852
- handler: guarded(async (req, res) => {
19853
- if (!allowOnly(req, res, ["PUT", "DELETE"])) return;
19854
- if (!isSameOriginRequest(req)) {
19855
- forbiddenWrite(res);
19856
- return;
19857
- }
19858
- let body;
19859
- try {
19860
- body = await readJsonBody(req);
19861
- } catch (error) {
19862
- if (error instanceof HttpError) {
19863
- sendJson(res, error.status, {
19864
- ok: false,
19865
- error: error.message
19866
- });
19867
- return;
19868
- }
19869
- throw error;
19870
- }
19871
- const payload = body;
19872
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
19873
- sendJson(res, 400, {
19874
- ok: false,
19875
- error: "请求体必须是 JSON 对象"
19876
- });
19877
- return;
19878
- }
19879
- if (methodOf(req) === "DELETE") {
19880
- const field = payload.field;
19881
- if (field !== "appId" && field !== "appSecret" && field !== "analyzeToken") {
19882
- sendJson(res, 400, {
19883
- ok: false,
19884
- error: "field 必须是 appId、appSecret 或 analyzeToken"
19885
- });
19886
- return;
19887
- }
19888
- const result = await surface.clearCredential(field);
19889
- sendJson(res, result.ok ? 200 : 400, result);
19890
- return;
19891
- }
19892
- const values = {};
19893
- if (payload.appId !== void 0) values.appId = payload.appId;
19894
- if (payload.appSecret !== void 0) values.appSecret = payload.appSecret;
19895
- if (payload.analyzeToken !== void 0) values.analyzeToken = payload.analyzeToken;
19896
- const result = await surface.saveCredentials(values);
19897
- sendJson(res, result.ok ? 200 : 400, result);
19898
- })
19607
+ post(CLEAR_CREDENTIAL_ROUTE, async (request) => {
19608
+ const field = (await readJsonObject(request)).field;
19609
+ if (field !== "appId" && field !== "appSecret" && field !== "analyzeToken") return json(400, {
19610
+ ok: false,
19611
+ error: "field 必须是 appId、appSecret 或 analyzeToken"
19612
+ });
19613
+ return result(await surface.clearCredential(field));
19899
19614
  }),
19900
- server.register({
19901
- kind: "exact",
19902
- path: RECONNECT_ROUTE,
19903
- handler: guarded(async (req, res) => {
19904
- if (!allowOnly(req, res, ["POST"])) return;
19905
- if (!isSameOriginRequest(req)) {
19906
- forbiddenWrite(res);
19907
- return;
19908
- }
19909
- const result = await surface.reconnect();
19910
- sendJson(res, result.ok ? 200 : result.unconfigured === true ? 400 : 500, result);
19911
- })
19615
+ post(RECONNECT_ROUTE, async () => {
19616
+ const outcome = await surface.reconnect();
19617
+ return json(outcome.ok ? 200 : outcome.unconfigured === true ? 400 : 500, outcome);
19912
19618
  }),
19913
- server.register({
19914
- kind: "exact",
19915
- path: PICK_DIRECTORY_ROUTE,
19916
- handler: guarded(async (req, res) => {
19917
- if (!allowOnly(req, res, ["POST"])) return;
19918
- if (!isSameOriginRequest(req)) {
19919
- forbiddenWrite(res);
19920
- return;
19921
- }
19922
- const controller = new AbortController();
19923
- let answered = false;
19924
- if (typeof res.on === "function") res.on("close", () => {
19925
- if (!answered) controller.abort();
19926
- });
19927
- let path;
19928
- try {
19929
- path = await surface.pickDirectory(controller.signal);
19930
- } catch (error) {
19931
- answered = true;
19932
- if (controller.signal.aborted) {
19933
- res.end();
19934
- return;
19935
- }
19936
- throw error;
19937
- }
19938
- answered = true;
19939
- sendJson(res, 200, path === null ? {
19940
- ok: true,
19941
- canceled: true
19942
- } : {
19943
- ok: true,
19944
- path
19945
- });
19946
- })
19619
+ post(PICK_DIRECTORY_ROUTE, async (request) => {
19620
+ const path = await surface.pickDirectory(request.signal);
19621
+ return json(200, path === null ? {
19622
+ ok: true,
19623
+ canceled: true
19624
+ } : {
19625
+ ok: true,
19626
+ path
19627
+ });
19947
19628
  }),
19948
- server.register({
19949
- kind: "exact",
19950
- path: ACCESS_ROUTE,
19951
- handler: guarded(async (req, res) => {
19952
- if (!allowOnly(req, res, ["GET", "HEAD"])) return;
19953
- if (methodOf(req) === "HEAD") {
19954
- res.writeHead(200, { "cache-control": "no-store" });
19955
- res.end();
19956
- return;
19957
- }
19958
- sendJson(res, 200, await surface.access());
19959
- })
19629
+ post(ADD_ACCESS_GROUP_ROUTE, async (request) => {
19630
+ const payload = await readJsonObject(request);
19631
+ return result(await surface.saveAccessGroup(payload.chatId, payload.name));
19960
19632
  }),
19961
- server.register({
19962
- kind: "exact",
19963
- path: ACCESS_GROUPS_ROUTE,
19964
- handler: guarded(async (req, res) => {
19965
- if (!allowOnly(req, res, ["POST", "DELETE"])) return;
19966
- if (!isSameOriginRequest(req)) {
19967
- forbiddenWrite(res);
19968
- return;
19969
- }
19970
- const payload = await readAccessBody(req, res);
19971
- if (payload === void 0) return;
19972
- const result = methodOf(req) === "DELETE" ? await surface.removeAccessGroup(payload.chatId) : await surface.saveAccessGroup(payload.chatId, payload.name);
19973
- sendJson(res, result.ok ? 200 : 400, result);
19974
- })
19975
- }),
19976
- server.register({
19977
- kind: "exact",
19978
- path: ACCESS_USERS_ROUTE,
19979
- handler: guarded(async (req, res) => {
19980
- if (!allowOnly(req, res, ["POST", "DELETE"])) return;
19981
- if (!isSameOriginRequest(req)) {
19982
- forbiddenWrite(res);
19983
- return;
19984
- }
19985
- const payload = await readAccessBody(req, res);
19986
- if (payload === void 0) return;
19987
- const result = methodOf(req) === "DELETE" ? await surface.removeAccessUser(payload.userId) : await surface.saveAccessUser(payload.userId, payload.name);
19988
- sendJson(res, result.ok ? 200 : 400, result);
19989
- })
19633
+ post(REMOVE_ACCESS_GROUP_ROUTE, async (request) => result(await surface.removeAccessGroup((await readJsonObject(request)).chatId))),
19634
+ post(ADD_ACCESS_USER_ROUTE, async (request) => {
19635
+ const payload = await readJsonObject(request);
19636
+ return result(await surface.saveAccessUser(payload.userId, payload.name));
19990
19637
  }),
19991
- server.register({
19992
- kind: "exact",
19993
- path: ACCESS_WIKI_USERS_ROUTE,
19994
- handler: guarded(async (req, res) => {
19995
- if (!allowOnly(req, res, ["POST", "DELETE"])) return;
19996
- if (!isSameOriginRequest(req)) {
19997
- forbiddenWrite(res);
19998
- return;
19999
- }
20000
- const payload = await readAccessBody(req, res);
20001
- if (payload === void 0) return;
20002
- const result = methodOf(req) === "DELETE" ? await surface.removeWikiUser(payload.userId) : await surface.saveWikiUser(payload.userId, payload.name);
20003
- sendJson(res, result.ok ? 200 : 400, result);
20004
- })
19638
+ post(REMOVE_ACCESS_USER_ROUTE, async (request) => result(await surface.removeAccessUser((await readJsonObject(request)).userId))),
19639
+ post(ADD_WIKI_USER_ROUTE, async (request) => {
19640
+ const payload = await readJsonObject(request);
19641
+ return result(await surface.saveWikiUser(payload.userId, payload.name));
20005
19642
  }),
20006
- server.register({
20007
- kind: "exact",
20008
- path: ACCESS_REQUESTS_ROUTE,
20009
- handler: guarded(async (req, res) => {
20010
- if (!allowOnly(req, res, ["POST"])) return;
20011
- if (!isSameOriginRequest(req)) {
20012
- forbiddenWrite(res);
20013
- return;
20014
- }
20015
- const payload = await readAccessBody(req, res);
20016
- if (payload === void 0) return;
20017
- const result = await surface.resolveAccessRequest(payload.token, payload.decision);
20018
- sendJson(res, result.ok ? 200 : 400, result);
20019
- })
19643
+ post(REMOVE_WIKI_USER_ROUTE, async (request) => result(await surface.removeWikiUser((await readJsonObject(request)).userId))),
19644
+ post(RESOLVE_ACCESS_REQUEST_ROUTE, async (request) => {
19645
+ const payload = await readJsonObject(request);
19646
+ return result(await surface.resolveAccessRequest(payload.token, payload.decision));
20020
19647
  })
20021
- ];
19648
+ ].map((route) => connection.fetch.register(route));
20022
19649
  return () => {
20023
- for (const dispose of disposers) dispose();
19650
+ for (const dispose of disposers) dispose().catch(() => {});
20024
19651
  };
20025
19652
  }
20026
19653
  /**
20027
- * Mount the routes once the host carries both a Web server and the
20028
- * `connection` service that authenticates its browser requests. The optional
20029
- * `ctx.inject` keeps headless profiles from waiting on services they will
20030
- * never have; embedded/test hosts that resolve services synchronously fall
20031
- * back to the direct attach path. Without `connection` nothing is mounted, so
20032
- * the panel never serves unauthenticated requests.
19654
+ * Mount the routes once the host carries the `connection` service. The
19655
+ * optional `ctx.inject` keeps headless profiles from waiting on a service
19656
+ * they will never have; embedded/test hosts that resolve services
19657
+ * synchronously fall back to the direct attach path.
19658
+ * @param ctx - the plugin context.
19659
+ * @param surface - the panel's business side.
20033
19660
  */
20034
19661
  function mountSettingsRoutes(ctx, surface) {
19662
+ const attach = (connectionCtx) => {
19663
+ const connection = connectionCtx.get?.("connection");
19664
+ if (connection?.fetch === void 0) return;
19665
+ connectionCtx.effect(() => registerSettingsRoutes(connection, surface), "dsh-channels: settings panel routes");
19666
+ };
19667
+ const inject = ctx.inject;
19668
+ if (typeof inject === "function") {
19669
+ inject.call(ctx, ["connection"], attach);
19670
+ return;
19671
+ }
19672
+ attach(ctx);
19673
+ }
19674
+ //#endregion
19675
+ //#region src/analyze/server.ts
19676
+ /**
19677
+ * The analyze API's HTTP route on the DSH Web server.
19678
+ *
19679
+ * Registers the exact route `POST /api/dsh-channels.logalyze` (see
19680
+ * `./contract.ts`) on the host `webServer`; hosts without one (headless) have
19681
+ * no analyze API. This module owns the transport-level checks, in order:
19682
+ * path, the `analyzeEnabled` switch, method, bearer token, `Idempotency-Key`,
19683
+ * media type, body size, and JSON validation; everything after validation
19684
+ * belongs to the {@link AnalyzeHandler}. Bodies, tokens, and prompts are never
19685
+ * logged.
19686
+ *
19687
+ * @module dsh-channels/analyze/server
19688
+ */
19689
+ var HttpError = class extends Error {
19690
+ status;
19691
+ constructor(status, message) {
19692
+ super(message);
19693
+ this.status = status;
19694
+ }
19695
+ };
19696
+ function send(res, response) {
19697
+ const body = JSON.stringify(response.body);
19698
+ res.writeHead(response.status, {
19699
+ "content-type": "application/json; charset=utf-8",
19700
+ "cache-control": "no-store",
19701
+ "content-length": Buffer.byteLength(body)
19702
+ });
19703
+ res.end(body);
19704
+ }
19705
+ function refuse(res, status, error, headers = {}) {
19706
+ for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
19707
+ send(res, {
19708
+ status,
19709
+ body: {
19710
+ ok: false,
19711
+ error
19712
+ }
19713
+ });
19714
+ }
19715
+ /** Constant-time token comparison over fixed-length digests. */
19716
+ function tokenMatches(presented, expected) {
19717
+ return timingSafeEqual(createHash("sha256").update(presented).digest(), createHash("sha256").update(expected).digest());
19718
+ }
19719
+ /** The bearer credential of one request, or undefined. */
19720
+ function bearerOf(req) {
19721
+ const header = req.headers.authorization;
19722
+ if (header === void 0) return void 0;
19723
+ return /^Bearer[ \t]+(\S+)[ \t]*$/iu.exec(header)?.[1];
19724
+ }
19725
+ /** Read the body up to {@link ANALYZE_MAX_BODY_BYTES}; larger bodies fail with 413. */
19726
+ async function readBody(req) {
19727
+ const declared = Number(req.headers["content-length"]);
19728
+ if (Number.isFinite(declared) && declared > 1048576) throw new HttpError(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
19729
+ const chunks = [];
19730
+ let size = 0;
19731
+ for await (const chunk of req) {
19732
+ const buffer = chunk;
19733
+ size += buffer.length;
19734
+ if (size > 1048576) throw new HttpError(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
19735
+ chunks.push(buffer);
19736
+ }
19737
+ return Buffer.concat(chunks).toString("utf8");
19738
+ }
19739
+ /**
19740
+ * Handle one HTTP request against the analyze contract.
19741
+ * @param req - the incoming request.
19742
+ * @param res - its response.
19743
+ * @param handler - the business side.
19744
+ */
19745
+ async function handleAnalyzeHttp(req, res, handler) {
19746
+ if (new URL(req.url ?? "/", "http://analyze.invalid").pathname !== "/api/dsh-channels.logalyze") {
19747
+ refuse(res, 404, "未知路径");
19748
+ return;
19749
+ }
19750
+ if (!handler.enabled()) {
19751
+ refuse(res, 404, "日志分析接口未开启,请在设置面板「高级 → 日志分析接口」打开开关");
19752
+ return;
19753
+ }
19754
+ if ((req.method ?? "GET").toUpperCase() !== "POST") {
19755
+ refuse(res, 405, "只接受 POST", { allow: "POST" });
19756
+ return;
19757
+ }
19758
+ const expected = await handler.token();
19759
+ if (expected === void 0 || expected === "") {
19760
+ refuse(res, 503, "分析接口未配置访问令牌,请联系部署者配置 analyzeToken 引用的凭据");
19761
+ return;
19762
+ }
19763
+ const presented = bearerOf(req);
19764
+ if (presented === void 0 || !tokenMatches(presented, expected)) {
19765
+ refuse(res, 401, "访问令牌无效", { "www-authenticate": "Bearer" });
19766
+ return;
19767
+ }
19768
+ const idempotencyKey = req.headers["idempotency-key"];
19769
+ if (typeof idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)) {
19770
+ refuse(res, 400, "缺少或不合法的 Idempotency-Key 请求头(1-200 个可见 ASCII 字符)");
19771
+ return;
19772
+ }
19773
+ if ((req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") {
19774
+ refuse(res, 415, "Content-Type 必须是 application/json");
19775
+ return;
19776
+ }
19777
+ let raw;
19778
+ try {
19779
+ raw = JSON.parse(await readBody(req));
19780
+ } catch (error) {
19781
+ if (error instanceof HttpError) {
19782
+ refuse(res, error.status, error.message);
19783
+ return;
19784
+ }
19785
+ refuse(res, 400, "请求体不是合法的 JSON");
19786
+ return;
19787
+ }
19788
+ const parsed = parseAnalyzeRequest(raw);
19789
+ if (!parsed.ok) {
19790
+ refuse(res, 400, parsed.error);
19791
+ return;
19792
+ }
19793
+ send(res, await handler.analyze(parsed.value, idempotencyKey));
19794
+ }
19795
+ /**
19796
+ * Register the analyze route on the host Web server once it is available.
19797
+ * Requests skip the DSH login gate on purpose: the bearer token is this API's
19798
+ * only credential.
19799
+ * @param ctx - the plugin context.
19800
+ * @param handler - the business side.
19801
+ * @param warn - diagnostics sink for unexpected handler failures.
19802
+ */
19803
+ function mountAnalyzeRoute(ctx, handler, warn) {
20035
19804
  const attach = (webCtx) => {
20036
19805
  const server = webCtx.get?.("webServer");
20037
- const gate = webCtx.get?.("connection");
20038
- if (server === void 0 || gate === void 0) return;
20039
- webCtx.effect(() => registerSettingsRoutes(server, gate, surface), "dsh-channels: settings panel routes");
19806
+ if (server === void 0) return;
19807
+ webCtx.effect(() => server.register({
19808
+ kind: "exact",
19809
+ path: ANALYZE_ROUTE,
19810
+ handler: async (req, res) => {
19811
+ try {
19812
+ await handleAnalyzeHttp(req, res, handler);
19813
+ } catch (error) {
19814
+ warn("dsh-channels: analyze request failed: %s", String(error));
19815
+ if (!res.headersSent) refuse(res, 500, `分析请求处理失败:${String(error)}`);
19816
+ else res.destroy();
19817
+ }
19818
+ }
19819
+ }), "dsh-channels: analyze route");
20040
19820
  };
20041
19821
  const inject = ctx.inject;
20042
19822
  if (typeof inject === "function") {
20043
- inject.call(ctx, ["webServer", "connection"], attach);
19823
+ inject.call(ctx, ["webServer"], attach);
20044
19824
  return;
20045
19825
  }
20046
19826
  attach(ctx);
@@ -20072,8 +19852,7 @@ const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
20072
19852
  "reasoningEffort",
20073
19853
  "agentPreset",
20074
19854
  "maxTokens",
20075
- "adminUserId",
20076
- "analyzeListen"
19855
+ "adminUserId"
20077
19856
  ]);
20078
19857
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
20079
19858
  "requireMention",
@@ -20087,7 +19866,8 @@ const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
20087
19866
  "feishuTools.topicMessages",
20088
19867
  "feishuTools.sendFile",
20089
19868
  "feishuTools.wikiRead",
20090
- "feishuTools.wikiWrite"
19869
+ "feishuTools.wikiWrite",
19870
+ "analyzeEnabled"
20091
19871
  ]);
20092
19872
  /** Editable keys rendered as `string-list` (one input per entry). */
20093
19873
  const STRING_LIST_FIELDS = { wikiRoots: {
@@ -20528,12 +20308,11 @@ const FIELD_META = {
20528
20308
  placeholder: "https://<租户>.feishu.cn/wiki/space/<space_id>",
20529
20309
  wide: true
20530
20310
  },
20531
- analyzeListen: {
20532
- label: "监听地址",
20311
+ analyzeEnabled: {
20312
+ label: "日志分析接口",
20533
20313
  group: "analyze",
20534
- summary: "日志平台调用分析接口的 host:port,留空关闭",
20535
- help: "接口路径 POST /dsh-channels/analyze,请求需带 Authorization: Bearer <访问令牌>。",
20536
- placeholder: "例如 0.0.0.0:3090"
20314
+ summary: "关闭时接口返回 404",
20315
+ help: "接口挂在 DSH Web 端口上:POST /api/dsh-channels.logalyze,请求需带 Authorization: Bearer <访问令牌>。只有带 Web 的 profile 才有这个接口。改后立即生效,不需要重连。"
20537
20316
  },
20538
20317
  loggerLevel: {
20539
20318
  label: "SDK 日志级别",
@@ -21635,6 +21414,7 @@ function apply(ctx, config) {
21635
21414
  ctx.logger?.warn?.(message, ...args);
21636
21415
  };
21637
21416
  mountSettingsRoutes(ctx, new SettingsSurface(service, overlay, { warn }, createHostCatalog(ctx, warn), createSettingsDirectoryPicker(ctx)));
21417
+ mountAnalyzeRoute(ctx, service.analyzeHandler(), warn);
21638
21418
  ctx.effect(() => {
21639
21419
  service.boot().catch((error) => {
21640
21420
  ctx.logger?.error?.("dsh-channels failed to boot: %s", String(error));
@@ -21644,6 +21424,6 @@ function apply(ctx, config) {
21644
21424
  }, "dsh-channels");
21645
21425
  }
21646
21426
  //#endregion
21647
- 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_DENY_WHITELIST_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 };
21427
+ export { AUTH_USER_TARGET_ERRORS, AccessController, AccessRequests, AccessStore, ApprovalAnswerer, CARD_MAX_BYTES, CARD_TARGET_BYTES, CATALOG_ROUTE, CHANNEL_COMMAND_NAMES, CLEAR_CONFIG_ROUTE, CLEAR_CREDENTIAL_ROUTE, Config, DM_DENY_TEXT, DM_DENY_WHITELIST_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, SAVE_CONFIG_ROUTE, SAVE_CREDENTIALS_ROUTE, 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, 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 };
21648
21428
 
21649
21429
  //# sourceMappingURL=index.js.map