@modesto_ma/dsh-channels 0.5.0 → 0.7.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,29 @@ 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 /open-api/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 sits outside DSH's `/api`
7481
+ * namespace so a gateway in front of DSH can expose it separately from the
7482
+ * login-protected DSH API; the request carries this API's bearer token, not a
7483
+ * DSH login.
7484
+ */
7485
+ const ANALYZE_ROUTE = "/open-api/logalyze";
7464
7486
  /** Largest accepted request body (the prompt dominates it). */
7465
7487
  const ANALYZE_MAX_BODY_BYTES = 1024 * 1024;
7466
7488
  /** Largest accepted prompt, in characters. */
@@ -7571,9 +7593,25 @@ var AnalyzeLauncher = class {
7571
7593
  accepting = /* @__PURE__ */ new Map();
7572
7594
  /** Background analyses still starting (drained on shutdown). */
7573
7595
  running = /* @__PURE__ */ new Set();
7596
+ /** Whether requests are admitted: from {@link open} after start until {@link close}. */
7597
+ admitting = false;
7574
7598
  constructor(deps) {
7575
7599
  this.deps = deps;
7576
7600
  }
7601
+ /**
7602
+ * Start admitting requests. Called at channel start after
7603
+ * {@link markInterrupted}, so a new request is never marked interrupted.
7604
+ */
7605
+ open() {
7606
+ this.admitting = true;
7607
+ }
7608
+ /** Stop admitting requests (503) ahead of shutdown. */
7609
+ close() {
7610
+ this.admitting = false;
7611
+ }
7612
+ enabled() {
7613
+ return this.deps.config.analyzeEnabled;
7614
+ }
7577
7615
  async token() {
7578
7616
  return await this.deps.resolveToken();
7579
7617
  }
@@ -7596,16 +7634,18 @@ var AnalyzeLauncher = class {
7596
7634
  }
7597
7635
  }
7598
7636
  /**
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.
7637
+ * Wait for every acceptance in flight, then every background analysis that
7638
+ * is still starting. Called after `started` turns false, so each one ends on
7639
+ * its failure card or, when its first prompt was already sent, as an
7640
+ * ordinary topic.
7602
7641
  */
7603
7642
  async drain() {
7643
+ await Promise.allSettled([...this.accepting.values()].map((pending) => pending.response));
7604
7644
  await Promise.allSettled([...this.running]);
7605
7645
  }
7606
7646
  /**
7607
7647
  * 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
7648
+ * its record. Called at channel start before admission opens, so each
7609
7649
  * such record belongs to a previous instance whose first turn never
7610
7650
  * settled. A card that cannot be updated (deleted, recalled) is logged and
7611
7651
  * still recorded as interrupted, so it is not retried on every start.
@@ -7625,7 +7665,7 @@ var AnalyzeLauncher = class {
7625
7665
  async accept(request, key, bodyHash) {
7626
7666
  const channel = this.deps.channel();
7627
7667
  const domain = this.deps.domain();
7628
- if (!this.deps.started() || channel === void 0 || domain === void 0) return refused(503, "飞书渠道尚未连接,请稍后重试");
7668
+ if (!this.admitting || !this.deps.started() || channel === void 0 || domain === void 0) return refused(503, "飞书渠道尚未连接,请稍后重试");
7629
7669
  const stored = domain.analyzeRequests.get(key);
7630
7670
  if (stored !== void 0) {
7631
7671
  if (stored.bodyHash !== bodyHash) return reusedKey();
@@ -7852,168 +7892,6 @@ function reusedKey() {
7852
7892
  return refused(409, "Idempotency-Key 已被内容不同的请求使用");
7853
7893
  }
7854
7894
  //#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
7895
  //#region src/session/repair.ts
8018
7896
  /**
8019
7897
  * In-flight turn snapshots and the startup repair pass.
@@ -8271,17 +8149,44 @@ function foldRequestPolicy(events) {
8271
8149
  model
8272
8150
  };
8273
8151
  }
8152
+ function sameModel(left, right) {
8153
+ return left?.provider === right?.provider && left?.model === right?.model;
8154
+ }
8155
+ /**
8156
+ * Record one effective policy as the Agent's next-step selection. A policy
8157
+ * without a model keeps the current route, except right after a model
8158
+ * override is cleared, when the Agent's live options (deployment default)
8159
+ * supply it again. Without a resolvable provider and model nothing is recorded.
8160
+ */
8161
+ function selectPolicy(model, agent, effort, target, modelCleared) {
8162
+ const current = model.current();
8163
+ const base = modelCleared && agent.options.provider !== void 0 && agent.options.model !== void 0 ? {
8164
+ provider: agent.options.provider,
8165
+ model: agent.options.model
8166
+ } : current;
8167
+ const provider = target?.provider ?? base?.provider;
8168
+ const modelId = target?.model ?? base?.model;
8169
+ if (provider === void 0 || provider === "" || modelId === void 0 || modelId === "") return;
8170
+ model.select({
8171
+ provider,
8172
+ model: modelId,
8173
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
8174
+ });
8175
+ }
8274
8176
  /** Install the mutable request policy outside every other request wrapper. */
8275
8177
  function installRequestPolicy(agentCtx, config = {}) {
8276
8178
  let effortOverride;
8277
8179
  let modelOverride;
8278
8180
  let disposed = false;
8181
+ /** Last policy recorded through `agent.model`; undefined until the first selecting sync. */
8182
+ let applied;
8279
8183
  const configuredModel = config.provider !== void 0 && config.model !== void 0 ? {
8280
8184
  provider: config.provider,
8281
8185
  model: config.model
8282
8186
  } : void 0;
8283
- const disposeListener = agentCtx.on("agent/request", async (_payload, next) => {
8187
+ const disposeListener = agentCtx.on("agent/request", async (payload, next) => {
8284
8188
  const request = await next();
8189
+ if (payload.agent?.model !== void 0) return request;
8285
8190
  const effort = effortOverride ?? config.effort;
8286
8191
  const model = modelOverride ?? configuredModel;
8287
8192
  if (effort === void 0 && model === void 0) return request;
@@ -8307,10 +8212,24 @@ function installRequestPolicy(agentCtx, config = {}) {
8307
8212
  modelSource: modelOverride !== void 0 ? "session" : configuredModel !== void 0 ? "config" : "deployment-default"
8308
8213
  };
8309
8214
  },
8310
- sync(events) {
8215
+ sync(events, agent, reconcile) {
8311
8216
  const folded = foldRequestPolicy(events);
8312
8217
  effortOverride = folded.effort;
8313
8218
  modelOverride = folded.model;
8219
+ const model = agent?.model;
8220
+ if (agent === void 0 || model === void 0 || disposed) return;
8221
+ const next = {
8222
+ effort: effortOverride ?? config.effort,
8223
+ model: modelOverride ?? configuredModel
8224
+ };
8225
+ const previous = applied ?? {
8226
+ effort: void 0,
8227
+ model: void 0
8228
+ };
8229
+ applied = next;
8230
+ if (reconcile === void 0 && previous.effort === next.effort && sameModel(previous.model, next.model)) return;
8231
+ const modelCleared = next.model === void 0 && (reconcile === "model" || previous.model !== void 0);
8232
+ selectPolicy(model, agent, next.effort, next.model, modelCleared);
8314
8233
  },
8315
8234
  dispose() {
8316
8235
  if (disposed) return;
@@ -8456,7 +8375,7 @@ var SessionManager = class {
8456
8375
  }
8457
8376
  /** Borrow one foreign live agent and mount this channel's scoped policy. */
8458
8377
  async borrow(sessionId, agent) {
8459
- const borrowed = borrowLiveAgent(agent, this.mountRequestPolicy(agent.ctx, agent.session));
8378
+ const borrowed = borrowLiveAgent(agent, this.mountRequestPolicy(agent.ctx, agent));
8460
8379
  this.live.set(sessionId, borrowed);
8461
8380
  await this.mountToolsFor(agent.ctx, sessionId);
8462
8381
  return borrowed;
@@ -8490,13 +8409,13 @@ var SessionManager = class {
8490
8409
  }
8491
8410
  }
8492
8411
  /** Mount one mutable Session-aware request policy outside every other wrapper. */
8493
- mountRequestPolicy(agentCtx, session) {
8412
+ mountRequestPolicy(agentCtx, agent) {
8494
8413
  const controller = installRequestPolicy(agentCtx, {
8495
8414
  effort: this.deps.config.reasoningEffort,
8496
8415
  provider: this.deps.config.provider,
8497
8416
  model: this.deps.config.model
8498
8417
  });
8499
- if (session !== void 0) controller.sync(session.snapshotEvents());
8418
+ if (agent !== void 0) controller.sync(agent.session.snapshotEvents(), agent);
8500
8419
  this.policies.set(agentCtx, controller);
8501
8420
  return () => {
8502
8421
  controller.dispose();
@@ -8507,7 +8426,7 @@ var SessionManager = class {
8507
8426
  requestPolicy(agent) {
8508
8427
  const controller = this.policies.get(agent.ctx);
8509
8428
  if (controller === void 0) throw new Error("dsh-channels: request policy is not mounted for this Agent");
8510
- controller.sync(agent.session.snapshotEvents());
8429
+ controller.sync(agent.session.snapshotEvents(), agent);
8511
8430
  return controller;
8512
8431
  }
8513
8432
  /**
@@ -12638,7 +12557,7 @@ async function read$1(kit, sessionId, rawArgs, signal) {
12638
12557
  };
12639
12558
  });
12640
12559
  }
12641
- function json(value) {
12560
+ function json$1(value) {
12642
12561
  return preview(JSON.stringify(value) ?? "null", 120);
12643
12562
  }
12644
12563
  function cardLines$1(operations, tableName) {
@@ -12646,7 +12565,7 @@ function cardLines$1(operations, tableName) {
12646
12565
  const n = `${index + 1}.`;
12647
12566
  switch (operation.command) {
12648
12567
  case "create": {
12649
- const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${json(record)}`);
12568
+ const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${json$1(record)}`);
12650
12569
  const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : [];
12651
12570
  return [
12652
12571
  `${n} 新增 ${operation.records.length} 条记录`,
@@ -12655,7 +12574,7 @@ function cardLines$1(operations, tableName) {
12655
12574
  ].join("\n");
12656
12575
  }
12657
12576
  case "update": {
12658
- const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${preview(record.record_id, 40)}:${json(record.fields)}`);
12577
+ const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${preview(record.record_id, 40)}:${json$1(record.fields)}`);
12659
12578
  const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : [];
12660
12579
  return [
12661
12580
  `${n} 修改 ${operation.records.length} 条记录`,
@@ -14241,7 +14160,7 @@ function foldHealthSnapshot(input) {
14241
14160
  const connectionState = input.connection?.state;
14242
14161
  let level;
14243
14162
  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";
14163
+ else if (connectionState !== "connected" || fibers.pending > 0 || fibers.loading > 0 || fibers.unloading > 0) level = "degraded";
14245
14164
  else level = "healthy";
14246
14165
  const reasons = [];
14247
14166
  if (!input.started) reasons.push("飞书通道尚未启动");
@@ -14256,7 +14175,6 @@ function foldHealthSnapshot(input) {
14256
14175
  const stalledLabels = inactive.filter((issue) => issue.state === "pending" || issue.state === "loading" || issue.state === "unloading").map((issue) => issue.label);
14257
14176
  reasons.push(`宿主其它插件仍有 ${stalled} 个入口在等待依赖或加载/卸载(与飞书通道无关)` + (stalledLabels.length === 0 ? "" : `:${labelList(stalledLabels, stalled)}`));
14258
14177
  }
14259
- if (input.analyzeListenError !== void 0) reasons.push(`日志分析接口监听失败:${input.analyzeListenError}`);
14260
14178
  return {
14261
14179
  level,
14262
14180
  fibers,
@@ -14267,7 +14185,6 @@ function foldHealthSnapshot(input) {
14267
14185
  routeCount: input.routeCount,
14268
14186
  liveAgentCount: input.liveAgentCount,
14269
14187
  inflightTurnCount: input.inflightTurnCount,
14270
- ...input.analyzeListenError === void 0 ? {} : { analyzeListenError: input.analyzeListenError },
14271
14188
  inactiveFibers: inactive,
14272
14189
  inactiveTotal,
14273
14190
  reasons
@@ -14319,11 +14236,7 @@ function buildHealthCard(snapshot) {
14319
14236
  {
14320
14237
  tag: "markdown",
14321
14238
  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
- }]
14239
+ }
14327
14240
  ]);
14328
14241
  }
14329
14242
  //#endregion
@@ -15238,7 +15151,7 @@ var TurnStreamer = class {
15238
15151
  const fold = this.folds.get(session.id);
15239
15152
  if (fold !== void 0) foldEvent(fold, event);
15240
15153
  this.sessions.set(session.id, session);
15241
- const parentId = session.header.parentSession;
15154
+ const parentId = session.header.origin === "subagent" ? session.header.parentSession : void 0;
15242
15155
  if (parentId !== void 0) {
15243
15156
  const parentState = this.front(parentId);
15244
15157
  if (parentState !== void 0) {
@@ -15985,8 +15898,6 @@ const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 2e4;
15985
15898
  const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3e3;
15986
15899
  /** Conventional credential reference holding the log-platform analyze API bearer token. */
15987
15900
  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
15901
  /** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
15991
15902
  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
15903
  const configSchema = z.object({
@@ -16318,13 +16229,11 @@ const configSchema = z.object({
16318
16229
  */
16319
16230
  questionTimeoutMs: z.number().int().min(1e4).max(36e5).default(DEFAULT_QUESTION_TIMEOUT_MS),
16320
16231
  /**
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.
16232
+ * Switch for the log-platform analyze API (`POST /open-api/logalyze`
16233
+ * on the DSH Web server). Off (default) answers 404; read per request, so
16234
+ * toggling it needs no reconnect. Needs a host with a Web server.
16326
16235
  */
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(),
16236
+ analyzeEnabled: z.boolean().default(false),
16328
16237
  /**
16329
16238
  * Credential reference holding the analyze API bearer token (defaults to
16330
16239
  * `DSH_CHANNELS_ANALYZE_TOKEN`). Resolved per request, so rotating the value
@@ -16364,29 +16273,45 @@ function resolveConfig(raw) {
16364
16273
  *
16365
16274
  * @module dsh-channels/settings/contract
16366
16275
  */
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";
16276
+ /**
16277
+ * Settings panel routes: exact DSH `/api` Fetch routes named
16278
+ * `/api/dsh-channels.<method>`. Reads are GET; every write is POST, because
16279
+ * DSH exact `/api` routes accept only GET, HEAD, and POST.
16280
+ */
16281
+ const STATUS_ROUTE = "/api/dsh-channels.status";
16282
+ const PANEL_ROUTE = "/api/dsh-channels.panel";
16283
+ /** Persist a partial config edit (POST, body `{ values }`). */
16284
+ const SAVE_CONFIG_ROUTE = "/api/dsh-channels.saveConfig";
16285
+ /** Drop every Web override (POST, no body). */
16286
+ const CLEAR_CONFIG_ROUTE = "/api/dsh-channels.clearConfig";
16287
+ /** Store credential values (POST, body `SaveCredentialsRequest`). */
16288
+ const SAVE_CREDENTIALS_ROUTE = "/api/dsh-channels.saveCredentials";
16289
+ /** Remove one stored credential (POST, body `{ field }`). */
16290
+ const CLEAR_CREDENTIAL_ROUTE = "/api/dsh-channels.clearCredential";
16372
16291
  /**
16373
16292
  * Connection lifecycle action (POST). While the channel runs it restarts the
16374
16293
  * long connection; while it is stopped it opens one — so the panel has exactly
16375
16294
  * one "connect / reconnect" button. Credentials are resolved again either way.
16376
16295
  */
16377
- const RECONNECT_ROUTE = "/dsh-channels/reconnect";
16296
+ const RECONNECT_ROUTE = "/api/dsh-channels.reconnect";
16378
16297
  /** 模型/推理档位/预设选择器的宿主目录(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";
16298
+ const CATALOG_ROUTE = "/api/dsh-channels.catalog";
16299
+ /** 访问与授权只读快照(GET):准入模式、管理员、群/用户/知识库白名单、待处理申请。 */
16300
+ const ACCESS_ROUTE = "/api/dsh-channels.access";
16301
+ /** 授权一个群(POST,body `{ chatId, name? }`)。 */
16302
+ const ADD_ACCESS_GROUP_ROUTE = "/api/dsh-channels.addAccessGroup";
16303
+ /** 撤销一个群(POST,body `{ chatId }`)。 */
16304
+ const REMOVE_ACCESS_GROUP_ROUTE = "/api/dsh-channels.removeAccessGroup";
16305
+ /** 用户白名单添加(POST,body `{ userId, name? }`)。 */
16306
+ const ADD_ACCESS_USER_ROUTE = "/api/dsh-channels.addAccessUser";
16307
+ /** 用户白名单移除(POST,body `{ userId }`)。 */
16308
+ const REMOVE_ACCESS_USER_ROUTE = "/api/dsh-channels.removeAccessUser";
16309
+ /** 知识库白名单添加(POST,body `{ userId, name? }`)。 */
16310
+ const ADD_WIKI_USER_ROUTE = "/api/dsh-channels.addWikiUser";
16311
+ /** 知识库白名单移除(POST,body `{ userId }`)。 */
16312
+ const REMOVE_WIKI_USER_ROUTE = "/api/dsh-channels.removeWikiUser";
16388
16313
  /** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
16389
- const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
16314
+ const RESOLVE_ACCESS_REQUEST_ROUTE = "/api/dsh-channels.resolveAccessRequest";
16390
16315
  /**
16391
16316
  * 打开宿主机的系统目录选择框(POST,无请求体),返回绝对路径。
16392
16317
  *
@@ -16394,15 +16319,15 @@ const ACCESS_REQUESTS_ROUTE = "/dsh-channels/access/requests";
16394
16319
  * 会注册实际选择逻辑;远程/无头会话的 browse 后端不可用,面板不下发
16395
16320
  * [`EditableFieldWire.browse`],保留手写输入。
16396
16321
  */
16397
- const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
16398
- /** 会话的飞书来源(GET/HEAD,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
16399
- const SESSION_ORIGIN_ROUTE = "/dsh-channels/session-origin";
16322
+ const PICK_DIRECTORY_ROUTE = "/api/dsh-channels.pickDirectory";
16323
+ /** 会话的飞书来源(GET,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
16324
+ const SESSION_ORIGIN_ROUTE = "/api/dsh-channels.sessionOrigin";
16400
16325
  /**
16401
16326
  * `GET PROFILES_ROUTE?users=<user_id,...>&chats=<chat_id,...>`: live Feishu
16402
16327
  * names and avatars for the users and chats the panel shows. At most
16403
16328
  * {@link PROFILES_MAX_IDS} ids per list.
16404
16329
  */
16405
- const PROFILES_ROUTE = "/dsh-channels/profiles";
16330
+ const PROFILES_ROUTE = "/api/dsh-channels.profiles";
16406
16331
  /**
16407
16332
  * 可由 Web 面板覆盖的配置项。顶层键对应 `src/config.ts` 的字段;`feishuTools.*`
16408
16333
  * 是嵌套字段(覆盖层里仍写成 `{ "feishuTools": { ... } }` 对象)。覆盖值写进
@@ -16410,7 +16335,7 @@ const PROFILES_ROUTE = "/dsh-channels/profiles";
16410
16335
  * `cordis.patch.yml`(见 README「Web 设置面板」)。
16411
16336
  *
16412
16337
  * 刻意不包含 `appId` / `appSecret`:它们是**凭据引用名**,面板改为直接
16413
- * 编辑引用背后的值(`PUT /dsh-channels/credentials`,经 DSH 凭据服务写入),
16338
+ * 编辑引用背后的值(`POST /api/dsh-channels.saveCredentials`,经 DSH 凭据服务写入),
16414
16339
  * 因此不会落进覆盖层文件。
16415
16340
  *
16416
16341
  * `baseDir`(工作区根目录 = 模型/工具产生文件的落点)可覆盖,但面板只接受
@@ -16460,7 +16385,7 @@ const EDITABLE_KEYS = [
16460
16385
  "feishuTools.wikiRead",
16461
16386
  "feishuTools.wikiWrite",
16462
16387
  "wikiRoots",
16463
- "analyzeListen",
16388
+ "analyzeEnabled",
16464
16389
  "loggerLevel"
16465
16390
  ];
16466
16391
  /**
@@ -16471,7 +16396,6 @@ const RESTART_KEYS = [
16471
16396
  "dmMode",
16472
16397
  "requireMention",
16473
16398
  "slashCommandPanel",
16474
- "analyzeListen",
16475
16399
  "refreshMs",
16476
16400
  "statusRefreshMs",
16477
16401
  "loggerLevel"
@@ -17076,10 +17000,6 @@ var FeishuService = class {
17076
17000
  inbound;
17077
17001
  /** Log-platform analyze API business side (requests → shared analysis topics). */
17078
17002
  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
17003
  /** Agent/session lifecycle: live registry, resume, presets, renew. */
17084
17004
  sessions;
17085
17005
  /**
@@ -17628,6 +17548,10 @@ var FeishuService = class {
17628
17548
  this.restarting = false;
17629
17549
  }
17630
17550
  }
17551
+ /** The analyze API's business side, for the Web route. */
17552
+ analyzeHandler() {
17553
+ return this.analyzeLauncher;
17554
+ }
17631
17555
  isRunning() {
17632
17556
  return this.started;
17633
17557
  }
@@ -18316,7 +18240,7 @@ var FeishuService = class {
18316
18240
  this.runScopeCheck(channel, appId, ownerLookup);
18317
18241
  if (this.config.slashCommandPanel) this.runSlashCommandSync(channel);
18318
18242
  await this.analyzeLauncher.markInterrupted();
18319
- await this.openAnalyzeListener();
18243
+ this.analyzeLauncher.open();
18320
18244
  this.repair.repairAll().catch((error) => {
18321
18245
  this.ctx.logger?.warn?.("dsh-channels: inflight repair failed: %s", String(error));
18322
18246
  });
@@ -18348,29 +18272,10 @@ var FeishuService = class {
18348
18272
  const questionsMounted = this.ctx.get?.("userQuestions") !== void 0;
18349
18273
  console.info(questionsMounted ? `[dsh-channels] ask_user_question 应答已就绪(提问卡片超时 ${this.config.questionTimeoutMs} ms)` : "[dsh-channels] 主机未挂载 userQuestions 服务,结构化提问应答关闭");
18350
18274
  }
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
18275
  /** Tear down: close the connection and dispose all live agents. */
18369
18276
  async shutdown() {
18370
18277
  this.started = false;
18371
- const analyzeServer = this.analyzeServer;
18372
- this.analyzeServer = void 0;
18373
- await analyzeServer?.close();
18278
+ this.analyzeLauncher.close();
18374
18279
  await this.analyzeLauncher.drain();
18375
18280
  for (const controller of this.commandControllers) controller.abort();
18376
18281
  await Promise.allSettled([...this.commandOperations]);
@@ -19053,8 +18958,9 @@ var FeishuService = class {
19053
18958
  /** Render one settled agent-plane command result. */
19054
18959
  async renderCommandResult(input) {
19055
18960
  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());
18961
+ const reconcile = !input.bare && result.kind === "success" ? spec.name : void 0;
18962
+ if (spec.name === "effort") effortAdmission?.controller.sync(live.agent.session.snapshotEvents(), live.agent, reconcile === "effort" ? "effort" : void 0);
18963
+ if (spec.name === "model") modelAdmission?.controller.sync(live.agent.session.snapshotEvents(), live.agent, reconcile === "model" ? "model" : void 0);
19058
18964
  if (input.bare && result.kind === "success") {
19059
18965
  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
18966
  if (picker !== void 0) {
@@ -19251,8 +19157,7 @@ var FeishuService = class {
19251
19157
  started: this.started,
19252
19158
  routeCount: this.routes.size(),
19253
19159
  liveAgentCount: this.sessions.liveCount(),
19254
- inflightTurnCount,
19255
- ...this.analyzeListenError === void 0 ? {} : { analyzeListenError: this.analyzeListenError }
19160
+ inflightTurnCount
19256
19161
  });
19257
19162
  }
19258
19163
  /**
@@ -19565,10 +19470,11 @@ var FeishuService = class {
19565
19470
  /** The model route the next request of a Session uses, as far as it is known. */
19566
19471
  function currentModelRoute(live, controller) {
19567
19472
  const state = controller.state;
19473
+ const selected = live.agent.model?.current();
19568
19474
  const header = live.agent.session.requestHeader?.()?.config;
19569
19475
  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)
19476
+ provider: state.effectiveModel?.provider ?? selected?.provider ?? (typeof header?.provider === "string" && header.provider !== "" ? header.provider : void 0),
19477
+ model: state.effectiveModel?.model ?? selected?.model ?? (typeof header?.model === "string" && header.model !== "" ? header.model : void 0)
19572
19478
  };
19573
19479
  }
19574
19480
  /** `**当前模型**:…` line shared by the `/model` text answer and its picker. */
@@ -19584,463 +19490,338 @@ const MAX_BODY_BYTES = 64 * 1024;
19584
19490
  function messageOf$2(error) {
19585
19491
  return error instanceof Error ? error.message : String(error);
19586
19492
  }
19587
- var HttpError = class extends Error {
19493
+ var HttpError$1 = class extends Error {
19588
19494
  status;
19589
19495
  constructor(status, message) {
19590
19496
  super(message);
19591
19497
  this.status = status;
19592
19498
  }
19593
19499
  };
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)
19500
+ function json(status, payload) {
19501
+ return new Response(JSON.stringify(payload), {
19502
+ status,
19503
+ headers: {
19504
+ "content-type": "application/json; charset=utf-8",
19505
+ "cache-control": "no-store"
19506
+ }
19619
19507
  });
19620
- res.end(body);
19621
- return false;
19622
19508
  }
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, "请求体为空");
19509
+ /** Parse a JSON-object body; malformed bodies throw {@link HttpError} (415/413/400). */
19510
+ async function readJsonObject(request) {
19511
+ if ((request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") throw new HttpError$1(415, "请求体必须是 application/json");
19512
+ const text = await request.text();
19513
+ if (Buffer.byteLength(text) > MAX_BODY_BYTES) throw new HttpError$1(413, "请求体过大");
19514
+ if (text === "") throw new HttpError$1(400, "请求体为空");
19515
+ let body;
19650
19516
  try {
19651
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
19517
+ body = JSON.parse(text);
19652
19518
  } catch {
19653
- throw new HttpError(400, "请求体不是合法 JSON");
19519
+ throw new HttpError$1(400, "请求体不是合法 JSON");
19654
19520
  }
19521
+ if (typeof body !== "object" || body === null || Array.isArray(body)) throw new HttpError$1(400, "请求体必须是 JSON 对象");
19522
+ return body;
19655
19523
  }
19656
19524
  /** Distinct non-empty entries of one comma-separated id query parameter. */
19657
19525
  function idList(raw) {
19658
19526
  if (raw === null) return [];
19659
19527
  return [...new Set(raw.split(",").map((id) => id.trim()).filter((id) => id !== ""))];
19660
19528
  }
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;
19529
+ /** A write's answer: 200 on success, 400 for a refusal the body explains. */
19530
+ function result(outcome) {
19531
+ return json(outcome.ok ? 200 : 400, outcome);
19687
19532
  }
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
- }
19533
+ /** Turn thrown errors into JSON answers instead of a dropped request. */
19534
+ function answered(handler) {
19535
+ return async (request) => {
19702
19536
  try {
19703
- await handler(req, res);
19537
+ return await handler(request);
19704
19538
  } catch (error) {
19705
- if (res.headersSent) {
19706
- res.end();
19707
- return;
19708
- }
19709
- sendJson(res, error instanceof HttpError ? error.status : 500, {
19539
+ if (error instanceof HttpError$1) return json(error.status, {
19710
19540
  ok: false,
19711
- error: error instanceof HttpError ? error.message : `内部错误:${messageOf$2(error)}`
19541
+ error: error.message
19542
+ });
19543
+ return json(500, {
19544
+ ok: false,
19545
+ error: `内部错误:${messageOf$2(error)}`
19712
19546
  });
19713
19547
  }
19714
19548
  };
19715
19549
  }
19716
- function forbiddenWrite(res) {
19717
- sendJson(res, 403, {
19718
- ok: false,
19719
- error: "跨站写入被拒绝(Origin 与 Host 不一致)"
19550
+ /**
19551
+ * Register the panel routes.
19552
+ * @param connection - the host `connection` service.
19553
+ * @param surface - the panel's business side.
19554
+ * @returns disposer removing every route.
19555
+ */
19556
+ function registerSettingsRoutes(connection, surface) {
19557
+ const get = (path, handler) => ({
19558
+ path,
19559
+ methods: ["GET"],
19560
+ requestBody: "buffered",
19561
+ fetch: answered((request) => handler(new URL(request.url)))
19562
+ });
19563
+ const post = (path, handler) => ({
19564
+ path,
19565
+ methods: ["POST"],
19566
+ requestBody: "buffered",
19567
+ fetch: answered(handler)
19720
19568
  });
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
19569
  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
- })
19570
+ get(STATUS_ROUTE, async () => json(200, await surface.status())),
19571
+ get(PANEL_ROUTE, async () => json(200, await surface.panel())),
19572
+ get(CATALOG_ROUTE, async (url) => json(200, await surface.catalogFor(url.searchParams.get("provider") ?? void 0, url.searchParams.get("model") ?? void 0))),
19573
+ get(SESSION_ORIGIN_ROUTE, async (url) => {
19574
+ const sessionId = url.searchParams.get("sessionId")?.trim();
19575
+ if (sessionId === void 0 || sessionId === "") return json(400, {
19576
+ ok: false,
19577
+ error: "缺少 sessionId 参数"
19578
+ });
19579
+ return json(200, await surface.sessionOrigin(sessionId));
19765
19580
  }),
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
- })
19581
+ get(PROFILES_ROUTE, async (url) => {
19582
+ const userIds = idList(url.searchParams.get("users"));
19583
+ const chatIds = idList(url.searchParams.get("chats"));
19584
+ if (userIds.length > 200 || chatIds.length > 200) return json(400, {
19585
+ ok: false,
19586
+ error: `每次最多查询 200 个用户和 200 个群`
19587
+ });
19588
+ return json(200, await surface.feishuProfiles(userIds, chatIds));
19786
19589
  }),
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
- })
19590
+ get(ACCESS_ROUTE, async () => json(200, await surface.access())),
19591
+ post(SAVE_CONFIG_ROUTE, async (request) => {
19592
+ const values = (await readJsonObject(request)).values;
19593
+ if (typeof values !== "object" || values === null || Array.isArray(values)) return json(400, {
19594
+ ok: false,
19595
+ error: "请求体缺少 values 对象"
19596
+ });
19597
+ return result(await surface.save(values));
19809
19598
  }),
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
- })
19599
+ post(CLEAR_CONFIG_ROUTE, async () => result(await surface.clear())),
19600
+ post(SAVE_CREDENTIALS_ROUTE, async (request) => {
19601
+ const payload = await readJsonObject(request);
19602
+ const values = {};
19603
+ if (payload.appId !== void 0) values.appId = payload.appId;
19604
+ if (payload.appSecret !== void 0) values.appSecret = payload.appSecret;
19605
+ if (payload.analyzeToken !== void 0) values.analyzeToken = payload.analyzeToken;
19606
+ return result(await surface.saveCredentials(values));
19848
19607
  }),
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
- })
19608
+ post(CLEAR_CREDENTIAL_ROUTE, async (request) => {
19609
+ const field = (await readJsonObject(request)).field;
19610
+ if (field !== "appId" && field !== "appSecret" && field !== "analyzeToken") return json(400, {
19611
+ ok: false,
19612
+ error: "field 必须是 appId、appSecret 或 analyzeToken"
19613
+ });
19614
+ return result(await surface.clearCredential(field));
19899
19615
  }),
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
- })
19616
+ post(RECONNECT_ROUTE, async () => {
19617
+ const outcome = await surface.reconnect();
19618
+ return json(outcome.ok ? 200 : outcome.unconfigured === true ? 400 : 500, outcome);
19912
19619
  }),
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
- })
19620
+ post(PICK_DIRECTORY_ROUTE, async (request) => {
19621
+ const path = await surface.pickDirectory(request.signal);
19622
+ return json(200, path === null ? {
19623
+ ok: true,
19624
+ canceled: true
19625
+ } : {
19626
+ ok: true,
19627
+ path
19628
+ });
19947
19629
  }),
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
- })
19630
+ post(ADD_ACCESS_GROUP_ROUTE, async (request) => {
19631
+ const payload = await readJsonObject(request);
19632
+ return result(await surface.saveAccessGroup(payload.chatId, payload.name));
19960
19633
  }),
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
- })
19634
+ post(REMOVE_ACCESS_GROUP_ROUTE, async (request) => result(await surface.removeAccessGroup((await readJsonObject(request)).chatId))),
19635
+ post(ADD_ACCESS_USER_ROUTE, async (request) => {
19636
+ const payload = await readJsonObject(request);
19637
+ return result(await surface.saveAccessUser(payload.userId, payload.name));
19990
19638
  }),
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
- })
19639
+ post(REMOVE_ACCESS_USER_ROUTE, async (request) => result(await surface.removeAccessUser((await readJsonObject(request)).userId))),
19640
+ post(ADD_WIKI_USER_ROUTE, async (request) => {
19641
+ const payload = await readJsonObject(request);
19642
+ return result(await surface.saveWikiUser(payload.userId, payload.name));
20005
19643
  }),
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
- })
19644
+ post(REMOVE_WIKI_USER_ROUTE, async (request) => result(await surface.removeWikiUser((await readJsonObject(request)).userId))),
19645
+ post(RESOLVE_ACCESS_REQUEST_ROUTE, async (request) => {
19646
+ const payload = await readJsonObject(request);
19647
+ return result(await surface.resolveAccessRequest(payload.token, payload.decision));
20020
19648
  })
20021
- ];
19649
+ ].map((route) => connection.fetch.register(route));
20022
19650
  return () => {
20023
- for (const dispose of disposers) dispose();
19651
+ for (const dispose of disposers) dispose().catch(() => {});
20024
19652
  };
20025
19653
  }
20026
19654
  /**
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.
19655
+ * Mount the routes once the host carries the `connection` service. The
19656
+ * optional `ctx.inject` keeps headless profiles from waiting on a service
19657
+ * they will never have; embedded/test hosts that resolve services
19658
+ * synchronously fall back to the direct attach path.
19659
+ * @param ctx - the plugin context.
19660
+ * @param surface - the panel's business side.
20033
19661
  */
20034
19662
  function mountSettingsRoutes(ctx, surface) {
19663
+ const attach = (connectionCtx) => {
19664
+ const connection = connectionCtx.get?.("connection");
19665
+ if (connection?.fetch === void 0) return;
19666
+ connectionCtx.effect(() => registerSettingsRoutes(connection, surface), "dsh-channels: settings panel routes");
19667
+ };
19668
+ const inject = ctx.inject;
19669
+ if (typeof inject === "function") {
19670
+ inject.call(ctx, ["connection"], attach);
19671
+ return;
19672
+ }
19673
+ attach(ctx);
19674
+ }
19675
+ //#endregion
19676
+ //#region src/analyze/server.ts
19677
+ /**
19678
+ * The analyze API's HTTP route on the DSH Web server.
19679
+ *
19680
+ * Registers the exact route `POST /open-api/logalyze` (see
19681
+ * `./contract.ts`) on the host `webServer`; hosts without one (headless) have
19682
+ * no analyze API. This module owns the transport-level checks, in order:
19683
+ * path, the `analyzeEnabled` switch, method, bearer token, `Idempotency-Key`,
19684
+ * media type, body size, and JSON validation; everything after validation
19685
+ * belongs to the {@link AnalyzeHandler}. Bodies, tokens, and prompts are never
19686
+ * logged.
19687
+ *
19688
+ * @module dsh-channels/analyze/server
19689
+ */
19690
+ var HttpError = class extends Error {
19691
+ status;
19692
+ constructor(status, message) {
19693
+ super(message);
19694
+ this.status = status;
19695
+ }
19696
+ };
19697
+ function send(res, response) {
19698
+ const body = JSON.stringify(response.body);
19699
+ res.writeHead(response.status, {
19700
+ "content-type": "application/json; charset=utf-8",
19701
+ "cache-control": "no-store",
19702
+ "content-length": Buffer.byteLength(body)
19703
+ });
19704
+ res.end(body);
19705
+ }
19706
+ function refuse(res, status, error, headers = {}) {
19707
+ for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
19708
+ send(res, {
19709
+ status,
19710
+ body: {
19711
+ ok: false,
19712
+ error
19713
+ }
19714
+ });
19715
+ }
19716
+ /** Constant-time token comparison over fixed-length digests. */
19717
+ function tokenMatches(presented, expected) {
19718
+ return timingSafeEqual(createHash("sha256").update(presented).digest(), createHash("sha256").update(expected).digest());
19719
+ }
19720
+ /** The bearer credential of one request, or undefined. */
19721
+ function bearerOf(req) {
19722
+ const header = req.headers.authorization;
19723
+ if (header === void 0) return void 0;
19724
+ return /^Bearer[ \t]+(\S+)[ \t]*$/iu.exec(header)?.[1];
19725
+ }
19726
+ /** Read the body up to {@link ANALYZE_MAX_BODY_BYTES}; larger bodies fail with 413. */
19727
+ async function readBody(req) {
19728
+ const declared = Number(req.headers["content-length"]);
19729
+ if (Number.isFinite(declared) && declared > 1048576) throw new HttpError(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
19730
+ const chunks = [];
19731
+ let size = 0;
19732
+ for await (const chunk of req) {
19733
+ const buffer = chunk;
19734
+ size += buffer.length;
19735
+ if (size > 1048576) throw new HttpError(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
19736
+ chunks.push(buffer);
19737
+ }
19738
+ return Buffer.concat(chunks).toString("utf8");
19739
+ }
19740
+ /**
19741
+ * Handle one HTTP request against the analyze contract.
19742
+ * @param req - the incoming request.
19743
+ * @param res - its response.
19744
+ * @param handler - the business side.
19745
+ */
19746
+ async function handleAnalyzeHttp(req, res, handler) {
19747
+ if (new URL(req.url ?? "/", "http://analyze.invalid").pathname !== "/open-api/logalyze") {
19748
+ refuse(res, 404, "未知路径");
19749
+ return;
19750
+ }
19751
+ if (!handler.enabled()) {
19752
+ refuse(res, 404, "日志分析接口未开启,请在设置面板「高级 → 日志分析接口」打开开关");
19753
+ return;
19754
+ }
19755
+ if ((req.method ?? "GET").toUpperCase() !== "POST") {
19756
+ refuse(res, 405, "只接受 POST", { allow: "POST" });
19757
+ return;
19758
+ }
19759
+ const expected = await handler.token();
19760
+ if (expected === void 0 || expected === "") {
19761
+ refuse(res, 503, "分析接口未配置访问令牌,请联系部署者配置 analyzeToken 引用的凭据");
19762
+ return;
19763
+ }
19764
+ const presented = bearerOf(req);
19765
+ if (presented === void 0 || !tokenMatches(presented, expected)) {
19766
+ refuse(res, 401, "访问令牌无效", { "www-authenticate": "Bearer" });
19767
+ return;
19768
+ }
19769
+ const idempotencyKey = req.headers["idempotency-key"];
19770
+ if (typeof idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)) {
19771
+ refuse(res, 400, "缺少或不合法的 Idempotency-Key 请求头(1-200 个可见 ASCII 字符)");
19772
+ return;
19773
+ }
19774
+ if ((req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") {
19775
+ refuse(res, 415, "Content-Type 必须是 application/json");
19776
+ return;
19777
+ }
19778
+ let raw;
19779
+ try {
19780
+ raw = JSON.parse(await readBody(req));
19781
+ } catch (error) {
19782
+ if (error instanceof HttpError) {
19783
+ refuse(res, error.status, error.message);
19784
+ return;
19785
+ }
19786
+ refuse(res, 400, "请求体不是合法的 JSON");
19787
+ return;
19788
+ }
19789
+ const parsed = parseAnalyzeRequest(raw);
19790
+ if (!parsed.ok) {
19791
+ refuse(res, 400, parsed.error);
19792
+ return;
19793
+ }
19794
+ send(res, await handler.analyze(parsed.value, idempotencyKey));
19795
+ }
19796
+ /**
19797
+ * Register the analyze route on the host Web server once it is available.
19798
+ * Requests skip the DSH login gate on purpose: the bearer token is this API's
19799
+ * only credential.
19800
+ * @param ctx - the plugin context.
19801
+ * @param handler - the business side.
19802
+ * @param warn - diagnostics sink for unexpected handler failures.
19803
+ */
19804
+ function mountAnalyzeRoute(ctx, handler, warn) {
20035
19805
  const attach = (webCtx) => {
20036
19806
  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");
19807
+ if (server === void 0) return;
19808
+ webCtx.effect(() => server.register({
19809
+ kind: "exact",
19810
+ path: ANALYZE_ROUTE,
19811
+ handler: async (req, res) => {
19812
+ try {
19813
+ await handleAnalyzeHttp(req, res, handler);
19814
+ } catch (error) {
19815
+ warn("dsh-channels: analyze request failed: %s", String(error));
19816
+ if (!res.headersSent) refuse(res, 500, `分析请求处理失败:${String(error)}`);
19817
+ else res.destroy();
19818
+ }
19819
+ }
19820
+ }), "dsh-channels: analyze route");
20040
19821
  };
20041
19822
  const inject = ctx.inject;
20042
19823
  if (typeof inject === "function") {
20043
- inject.call(ctx, ["webServer", "connection"], attach);
19824
+ inject.call(ctx, ["webServer"], attach);
20044
19825
  return;
20045
19826
  }
20046
19827
  attach(ctx);
@@ -20072,8 +19853,7 @@ const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
20072
19853
  "reasoningEffort",
20073
19854
  "agentPreset",
20074
19855
  "maxTokens",
20075
- "adminUserId",
20076
- "analyzeListen"
19856
+ "adminUserId"
20077
19857
  ]);
20078
19858
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
20079
19859
  "requireMention",
@@ -20087,7 +19867,8 @@ const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
20087
19867
  "feishuTools.topicMessages",
20088
19868
  "feishuTools.sendFile",
20089
19869
  "feishuTools.wikiRead",
20090
- "feishuTools.wikiWrite"
19870
+ "feishuTools.wikiWrite",
19871
+ "analyzeEnabled"
20091
19872
  ]);
20092
19873
  /** Editable keys rendered as `string-list` (one input per entry). */
20093
19874
  const STRING_LIST_FIELDS = { wikiRoots: {
@@ -20528,12 +20309,11 @@ const FIELD_META = {
20528
20309
  placeholder: "https://<租户>.feishu.cn/wiki/space/<space_id>",
20529
20310
  wide: true
20530
20311
  },
20531
- analyzeListen: {
20532
- label: "监听地址",
20312
+ analyzeEnabled: {
20313
+ label: "日志分析接口",
20533
20314
  group: "analyze",
20534
- summary: "日志平台调用分析接口的 host:port,留空关闭",
20535
- help: "接口路径 POST /dsh-channels/analyze,请求需带 Authorization: Bearer <访问令牌>。",
20536
- placeholder: "例如 0.0.0.0:3090"
20315
+ summary: "关闭时接口返回 404",
20316
+ help: "接口挂在 DSH Web 端口上:POST /open-api/logalyze,请求需带 Authorization: Bearer <访问令牌>。只有带 Web 的 profile 才有这个接口。改后立即生效,不需要重连。"
20537
20317
  },
20538
20318
  loggerLevel: {
20539
20319
  label: "SDK 日志级别",
@@ -21635,6 +21415,7 @@ function apply(ctx, config) {
21635
21415
  ctx.logger?.warn?.(message, ...args);
21636
21416
  };
21637
21417
  mountSettingsRoutes(ctx, new SettingsSurface(service, overlay, { warn }, createHostCatalog(ctx, warn), createSettingsDirectoryPicker(ctx)));
21418
+ mountAnalyzeRoute(ctx, service.analyzeHandler(), warn);
21638
21419
  ctx.effect(() => {
21639
21420
  service.boot().catch((error) => {
21640
21421
  ctx.logger?.error?.("dsh-channels failed to boot: %s", String(error));
@@ -21644,6 +21425,6 @@ function apply(ctx, config) {
21644
21425
  }, "dsh-channels");
21645
21426
  }
21646
21427
  //#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 };
21428
+ 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
21429
 
21649
21430
  //# sourceMappingURL=index.js.map