@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/README.md +10 -10
- package/lib/client.js +53 -37
- package/lib/client.js.map +1 -1
- package/lib/index.js +446 -665
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/analyze/contract.ts +8 -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,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
|
|
7600
|
-
* `started` turns false, so each one ends on
|
|
7601
|
-
* first prompt was already sent, as an
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
16322
|
-
*
|
|
16323
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
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
|
|
16296
|
+
const RECONNECT_ROUTE = "/api/dsh-channels.reconnect";
|
|
16378
16297
|
/** 模型/推理档位/预设选择器的宿主目录(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
|
|
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
|
|
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
|
|
16398
|
-
/** 会话的飞书来源(GET
|
|
16399
|
-
const SESSION_ORIGIN_ROUTE = "/dsh-channels
|
|
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
|
|
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
|
-
* 编辑引用背后的值(`
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19057
|
-
if (spec.name === "
|
|
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
|
|
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)
|
|
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
|
-
|
|
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, "请求体为空");
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
}
|
|
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(
|
|
19537
|
+
return await handler(request);
|
|
19704
19538
|
} catch (error) {
|
|
19705
|
-
if (
|
|
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
|
|
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
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
|
20028
|
-
* `
|
|
20029
|
-
*
|
|
20030
|
-
*
|
|
20031
|
-
*
|
|
20032
|
-
* the panel
|
|
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
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
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"
|
|
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
|
-
|
|
20532
|
-
label: "
|
|
20312
|
+
analyzeEnabled: {
|
|
20313
|
+
label: "日志分析接口",
|
|
20533
20314
|
group: "analyze",
|
|
20534
|
-
summary: "
|
|
20535
|
-
help: "
|
|
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,
|
|
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
|