@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/README.md +10 -10
- package/lib/client.js +53 -37
- package/lib/client.js.map +1 -1
- package/lib/index.js +445 -665
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/analyze/contract.ts +7 -3
- package/src/analyze/launcher.ts +28 -5
- package/src/analyze/server.ts +60 -54
- package/src/client/api.ts +28 -30
- package/src/client/catalog.ts +1 -1
- package/src/client/panel/advanced-tab.tsx +1 -1
- package/src/config.ts +4 -13
- package/src/health.ts +0 -12
- package/src/host/agent.ts +24 -1
- package/src/index.ts +6 -4
- package/src/service.ts +18 -38
- package/src/session/manager.ts +4 -4
- package/src/session/request-policy.ts +64 -5
- package/src/settings/contract.ts +49 -34
- package/src/settings/routes.ts +159 -443
- package/src/settings/surface.ts +5 -6
- package/src/streamer.ts +3 -2
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
|
|
7600
|
-
* `started` turns false, so each one ends on
|
|
7601
|
-
* first prompt was already sent, as an
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
16322
|
-
*
|
|
16323
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
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
|
|
16295
|
+
const RECONNECT_ROUTE = "/api/dsh-channels.reconnect";
|
|
16378
16296
|
/** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
|
|
16379
|
-
const CATALOG_ROUTE = "/dsh-channels
|
|
16380
|
-
/** 访问与授权只读快照(GET
|
|
16381
|
-
const ACCESS_ROUTE = "/dsh-channels
|
|
16382
|
-
/**
|
|
16383
|
-
const
|
|
16384
|
-
/**
|
|
16385
|
-
const
|
|
16386
|
-
/**
|
|
16387
|
-
const
|
|
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
|
|
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
|
|
16398
|
-
/** 会话的飞书来源(GET
|
|
16399
|
-
const SESSION_ORIGIN_ROUTE = "/dsh-channels
|
|
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
|
|
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
|
-
* 编辑引用背后的值(`
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19057
|
-
if (spec.name === "
|
|
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
|
|
19595
|
-
return (
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
|
|
19600
|
-
|
|
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
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19663
|
-
|
|
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
|
-
|
|
19690
|
-
|
|
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(
|
|
19536
|
+
return await handler(request);
|
|
19704
19537
|
} catch (error) {
|
|
19705
|
-
if (
|
|
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
|
|
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
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
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
|
-
|
|
19727
|
-
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
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
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
19773
|
-
|
|
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
|
-
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
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
|
-
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19817
|
-
|
|
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
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
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
|
-
|
|
19901
|
-
|
|
19902
|
-
|
|
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
|
-
|
|
19914
|
-
|
|
19915
|
-
path
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
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
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
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
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
|
|
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
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
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
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
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
|
|
20028
|
-
* `
|
|
20029
|
-
*
|
|
20030
|
-
*
|
|
20031
|
-
*
|
|
20032
|
-
* the panel
|
|
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
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
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"
|
|
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
|
-
|
|
20532
|
-
label: "
|
|
20311
|
+
analyzeEnabled: {
|
|
20312
|
+
label: "日志分析接口",
|
|
20533
20313
|
group: "analyze",
|
|
20534
|
-
summary: "
|
|
20535
|
-
help: "
|
|
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,
|
|
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
|