@modesto_ma/dsh-channels 0.7.0 → 0.8.0-beta.1
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 +31 -47
- package/lib/client.js +44 -122
- package/lib/client.js.map +1 -1
- package/lib/index.js +436 -558
- package/lib/index.js.map +1 -1
- package/package.json +4 -1
- package/src/access/controller.ts +3 -3
- package/src/cards/index.ts +1 -1
- package/src/cards/topic.ts +104 -0
- package/src/client/origin.tsx +1 -1
- package/src/client/panel/advanced-tab.tsx +5 -29
- package/src/client/panel/index.tsx +2 -39
- package/src/client/panel/shared.tsx +0 -12
- package/src/config.ts +26 -16
- package/src/domain.ts +89 -32
- package/src/host/core.ts +32 -1
- package/src/host/services.ts +5 -0
- package/src/index.ts +11 -4
- package/src/routing/inbound.ts +1 -1
- package/src/service.ts +20 -25
- package/src/settings/contract.ts +6 -21
- package/src/settings/overlay.ts +3 -1
- package/src/settings/routes.ts +2 -3
- package/src/settings/surface.ts +1 -14
- package/src/streamer.ts +91 -5
- package/src/topics/contract.ts +149 -0
- package/src/topics/launcher.ts +428 -0
- package/src/workspaces.ts +25 -18
- package/src/analyze/contract.ts +0 -126
- package/src/analyze/launcher.ts +0 -376
- package/src/analyze/server.ts +0 -207
- package/src/cards/analyze.ts +0 -116
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { constants, createWriteStream, promises, readFileSync } from "node:fs";
|
|
4
4
|
import { access, mkdir, open, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -180,12 +180,12 @@ var AccessController = class {
|
|
|
180
180
|
}, this.config(), this.deps.store.snapshot()).kind === "allow";
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
|
-
* Whether
|
|
184
|
-
* any group in `open` access mode.
|
|
183
|
+
* Whether a `feishuTopics` launch may post into `chatId`: an authorized
|
|
184
|
+
* group, or any group in `open` access mode.
|
|
185
185
|
* @param chatId - target group chat id.
|
|
186
186
|
* @returns true when the chat is a permitted target.
|
|
187
187
|
*/
|
|
188
|
-
|
|
188
|
+
mayLaunchIn(chatId) {
|
|
189
189
|
if (this.config().accessMode === "open") return true;
|
|
190
190
|
return this.deps.store.snapshot().groups.has(chatId);
|
|
191
191
|
}
|
|
@@ -1114,7 +1114,7 @@ const routeRecordSchema = z.object({
|
|
|
1114
1114
|
/** Sessions this topic has pointed at, oldest first. Optional for v1 compatibility. */
|
|
1115
1115
|
sessionHistory: z.array(sessionHistoryEntrySchema).optional(),
|
|
1116
1116
|
/**
|
|
1117
|
-
* Group-shared route (
|
|
1117
|
+
* Group-shared route (topics started through `feishuTopics`): every member admitted
|
|
1118
1118
|
* in `chatId` may continue the Session and decide its cards, not only
|
|
1119
1119
|
* `senderUserId` (who then names the requester). Absent = owner-only.
|
|
1120
1120
|
*/
|
|
@@ -1305,36 +1305,67 @@ const processedRecordSchema = z.object({
|
|
|
1305
1305
|
/** Epoch ms when the message was accepted for processing. */
|
|
1306
1306
|
processedAt: z.number().int().nonnegative()
|
|
1307
1307
|
});
|
|
1308
|
+
/** {@link TopicRootCard} as stored in a launch record. */
|
|
1309
|
+
const topicRootCardSchema = z.object({
|
|
1310
|
+
title: z.string(),
|
|
1311
|
+
taskName: z.string(),
|
|
1312
|
+
lead: z.array(z.object({
|
|
1313
|
+
text: z.string(),
|
|
1314
|
+
bold: z.boolean().optional()
|
|
1315
|
+
})),
|
|
1316
|
+
links: z.object({
|
|
1317
|
+
nameHeader: z.string(),
|
|
1318
|
+
linkHeader: z.string(),
|
|
1319
|
+
rows: z.array(z.object({
|
|
1320
|
+
name: z.string(),
|
|
1321
|
+
url: z.string()
|
|
1322
|
+
}))
|
|
1323
|
+
}).optional()
|
|
1324
|
+
});
|
|
1308
1325
|
/**
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1326
|
+
* Root card content written by plugin versions before the `feishuTopics`
|
|
1327
|
+
* service (the built-in log-platform analyze API). Read only; converted by
|
|
1328
|
+
* {@link rootCardOf}.
|
|
1312
1329
|
*/
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1330
|
+
const legacyAnalyzeSubjectSchema = z.object({
|
|
1331
|
+
requesterUserId: z.string(),
|
|
1332
|
+
triggeredAt: z.string(),
|
|
1333
|
+
app: z.string(),
|
|
1334
|
+
errorCode: z.string(),
|
|
1335
|
+
logDetails: z.array(z.object({
|
|
1336
|
+
name: z.string(),
|
|
1337
|
+
url: z.string()
|
|
1338
|
+
}))
|
|
1339
|
+
});
|
|
1340
|
+
/**
|
|
1341
|
+
* One accepted `feishuTopics` launch, keyed by its idempotency key. Kept
|
|
1342
|
+
* forever: a retried key answers the stored root card instead of posting a
|
|
1343
|
+
* second topic, and a reused key with another fingerprint is refused. The
|
|
1344
|
+
* records live in the `analyze_requests` table, named after the analyze API
|
|
1345
|
+
* that wrote the first of them; those older records carry `subject` instead
|
|
1346
|
+
* of `requesterUserId` + `card`, and their keys are the analyze API's
|
|
1347
|
+
* `Idempotency-Key`s.
|
|
1348
|
+
*/
|
|
1349
|
+
const topicLaunchRecordSchema = z.object({
|
|
1350
|
+
/** The launch's idempotency key (also the table key). */
|
|
1315
1351
|
key: z.string().min(1),
|
|
1316
|
-
/** SHA-256
|
|
1352
|
+
/** The launch's fingerprint (older records: SHA-256 of the analyze request). */
|
|
1317
1353
|
bodyHash: z.string().min(1),
|
|
1318
1354
|
/** Target group chat. */
|
|
1319
1355
|
chatId: z.string().min(1),
|
|
1320
|
-
/** Root card message id (the
|
|
1356
|
+
/** Root card message id (the topic's root). */
|
|
1321
1357
|
messageId: z.string().min(1),
|
|
1322
|
-
/** Epoch ms when the
|
|
1358
|
+
/** Epoch ms when the launch was accepted. */
|
|
1323
1359
|
acceptedAt: z.number().int().nonnegative(),
|
|
1324
|
-
/**
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
logDetails: z.array(z.object({
|
|
1331
|
-
name: z.string(),
|
|
1332
|
-
url: z.string()
|
|
1333
|
-
}))
|
|
1334
|
-
}),
|
|
1360
|
+
/** Requester @-mentioned on the root card; absent on older records. */
|
|
1361
|
+
requesterUserId: z.string().optional(),
|
|
1362
|
+
/** What the root card shows, kept so the card can be redrawn after a restart; absent on older records. */
|
|
1363
|
+
card: topicRootCardSchema.optional(),
|
|
1364
|
+
/** Root card content of older records. */
|
|
1365
|
+
subject: legacyAnalyzeSubjectSchema.optional(),
|
|
1335
1366
|
/**
|
|
1336
1367
|
* Root card status. `running` until the first turn settles; a record still
|
|
1337
|
-
* `running` when the channel starts belongs to
|
|
1368
|
+
* `running` when the channel starts belongs to a topic the previous
|
|
1338
1369
|
* instance never finished, and becomes `interrupted`.
|
|
1339
1370
|
*/
|
|
1340
1371
|
status: z.enum([
|
|
@@ -1347,9 +1378,48 @@ const analyzeRequestRecordSchema = z.object({
|
|
|
1347
1378
|
])
|
|
1348
1379
|
});
|
|
1349
1380
|
/**
|
|
1381
|
+
* The requester and root card content of one launch record, converting the
|
|
1382
|
+
* analyze API's older records to the card that API drew.
|
|
1383
|
+
* @param record - stored launch record.
|
|
1384
|
+
* @returns what to redraw, or undefined for a record carrying neither form.
|
|
1385
|
+
*/
|
|
1386
|
+
function rootCardOf(record) {
|
|
1387
|
+
if (record.card !== void 0 && record.requesterUserId !== void 0) return {
|
|
1388
|
+
requesterUserId: record.requesterUserId,
|
|
1389
|
+
card: record.card
|
|
1390
|
+
};
|
|
1391
|
+
const subject = record.subject;
|
|
1392
|
+
if (subject === void 0) return void 0;
|
|
1393
|
+
return {
|
|
1394
|
+
requesterUserId: subject.requesterUserId,
|
|
1395
|
+
card: {
|
|
1396
|
+
title: "日志分析",
|
|
1397
|
+
taskName: "分析",
|
|
1398
|
+
lead: [
|
|
1399
|
+
{ text: `于 ${subject.triggeredAt} 发起了对 ` },
|
|
1400
|
+
{
|
|
1401
|
+
text: subject.app,
|
|
1402
|
+
bold: true
|
|
1403
|
+
},
|
|
1404
|
+
{ text: " 应用 " },
|
|
1405
|
+
{
|
|
1406
|
+
text: subject.errorCode,
|
|
1407
|
+
bold: true
|
|
1408
|
+
},
|
|
1409
|
+
{ text: " 错误码不良日志的分析。" }
|
|
1410
|
+
],
|
|
1411
|
+
links: {
|
|
1412
|
+
nameHeader: "日志",
|
|
1413
|
+
linkHeader: "详情页",
|
|
1414
|
+
rows: subject.logDetails
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1350
1420
|
* Where one Feishu-routed Session came from, keyed by its DSH session id.
|
|
1351
1421
|
* Written the first time a route record names the Session (new topic,
|
|
1352
|
-
* `/renew`,
|
|
1422
|
+
* `/renew`, `feishuTopics` launch) and kept after the route retires, so the Web
|
|
1353
1423
|
* client can still show the origin of an old Session. Names are resolved
|
|
1354
1424
|
* live when read, never stored.
|
|
1355
1425
|
*/
|
|
@@ -1364,11 +1434,11 @@ const sessionOriginRecordSchema = z.object({
|
|
|
1364
1434
|
chatId: z.string().min(1),
|
|
1365
1435
|
/** `p2p` or `group`. */
|
|
1366
1436
|
chatType: z.enum(["p2p", "group"]),
|
|
1367
|
-
/** `user_id` of the user who started the Session (the requester for
|
|
1437
|
+
/** `user_id` of the user who started the Session (the requester for launched topics). */
|
|
1368
1438
|
senderUserId: z.string().min(1),
|
|
1369
1439
|
/** Epoch ms when the Session was bound to the route. */
|
|
1370
1440
|
createdAt: z.number().int().nonnegative(),
|
|
1371
|
-
/** Group-shared route (
|
|
1441
|
+
/** Group-shared route (topic started through `feishuTopics`). */
|
|
1372
1442
|
shared: z.literal(true).optional()
|
|
1373
1443
|
});
|
|
1374
1444
|
/**
|
|
@@ -1399,7 +1469,7 @@ const feishuDomain = {
|
|
|
1399
1469
|
access_users: { valueSchema: accessUserRecordSchema },
|
|
1400
1470
|
access_requests: { valueSchema: accessRequestRecordSchema },
|
|
1401
1471
|
access_wiki_users: { valueSchema: accessUserRecordSchema },
|
|
1402
|
-
analyze_requests: { valueSchema:
|
|
1472
|
+
analyze_requests: { valueSchema: topicLaunchRecordSchema },
|
|
1403
1473
|
session_origins: { valueSchema: sessionOriginRecordSchema }
|
|
1404
1474
|
}
|
|
1405
1475
|
};
|
|
@@ -2833,18 +2903,10 @@ function parsePickAction(value) {
|
|
|
2833
2903
|
};
|
|
2834
2904
|
}
|
|
2835
2905
|
//#endregion
|
|
2836
|
-
//#region src/cards/
|
|
2837
|
-
/**
|
|
2838
|
-
* Root card of a log-platform analysis: who started which analysis when, the
|
|
2839
|
-
* log-platform detail pages behind it, and a status tag (分析中 → 已完成 /
|
|
2840
|
-
* 已停止 / 失败 / 已中断). The analysis itself streams in the topic below the
|
|
2841
|
-
* card through the ordinary streaming card.
|
|
2842
|
-
*
|
|
2843
|
-
* @module dsh-channels/cards/analyze
|
|
2844
|
-
*/
|
|
2906
|
+
//#region src/cards/topic.ts
|
|
2845
2907
|
/** Rows per table page (Feishu caps `page_size` at 10). */
|
|
2846
2908
|
const TABLE_PAGE_SIZE = 10;
|
|
2847
|
-
/** Escape markdown emphasis characters in
|
|
2909
|
+
/** Escape markdown emphasis characters in caller-supplied text. */
|
|
2848
2910
|
function plain$1(text) {
|
|
2849
2911
|
return text.replace(/([*_~`[\]\\])/gu, "\\$1");
|
|
2850
2912
|
}
|
|
@@ -2853,10 +2915,10 @@ function linkTarget(url) {
|
|
|
2853
2915
|
return url.replace(/[()\s]/gu, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
2854
2916
|
}
|
|
2855
2917
|
/** Header tag text, tag color, and header template of each status. */
|
|
2856
|
-
function statusLook(status) {
|
|
2918
|
+
function statusLook(status, task) {
|
|
2857
2919
|
switch (status.kind) {
|
|
2858
2920
|
case "running": return {
|
|
2859
|
-
tag:
|
|
2921
|
+
tag: `${task}中`,
|
|
2860
2922
|
color: "blue",
|
|
2861
2923
|
template: "blue"
|
|
2862
2924
|
};
|
|
@@ -2884,31 +2946,60 @@ function statusLook(status) {
|
|
|
2884
2946
|
}
|
|
2885
2947
|
}
|
|
2886
2948
|
/** Footer line of each status. */
|
|
2887
|
-
function statusNote(status) {
|
|
2949
|
+
function statusNote(status, task) {
|
|
2888
2950
|
switch (status.kind) {
|
|
2889
|
-
case "running": return
|
|
2890
|
-
case "done": return
|
|
2891
|
-
case "stopped": return
|
|
2892
|
-
case "error": return
|
|
2893
|
-
case "failed": return
|
|
2894
|
-
case "interrupted": return
|
|
2951
|
+
case "running": return `${task}过程在本卡片下的话题内实时输出;群内已授权的成员可以在话题内直接回复追问。`;
|
|
2952
|
+
case "done": return `${task}已完成,结果见话题;在话题内回复即可继续追问。`;
|
|
2953
|
+
case "stopped": return `${task}已停止;在话题内回复即可继续。`;
|
|
2954
|
+
case "error": return `${task}回合失败,原因见话题;在话题内回复即可重试或追问。`;
|
|
2955
|
+
case "failed": return `${task}未能开始:${plain$1(status.reason)}`;
|
|
2956
|
+
case "interrupted": return `DSH 重启导致${task}中断;在话题内回复即可继续。`;
|
|
2895
2957
|
}
|
|
2896
2958
|
}
|
|
2897
2959
|
/**
|
|
2898
|
-
* The
|
|
2899
|
-
* @param
|
|
2960
|
+
* The topic root card.
|
|
2961
|
+
* @param requesterUserId - `user_id` @-mentioned before the lead sentence.
|
|
2962
|
+
* @param card - what the caller asked the card to show.
|
|
2900
2963
|
* @param status - the status the card shows.
|
|
2901
2964
|
* @returns card JSON.
|
|
2902
2965
|
*/
|
|
2903
|
-
function
|
|
2904
|
-
const
|
|
2966
|
+
function buildTopicRootCard(requesterUserId, card, status) {
|
|
2967
|
+
const task = plain$1(card.taskName);
|
|
2968
|
+
const look = statusLook(status, card.taskName);
|
|
2969
|
+
const elements = [markdown(`<at id=${requesterUserId}></at> ${card.lead.map((run) => run.bold === true ? `**${plain$1(run.text)}**` : plain$1(run.text)).join("")}`)];
|
|
2970
|
+
if (card.links !== void 0 && card.links.rows.length > 0) elements.push({
|
|
2971
|
+
tag: "table",
|
|
2972
|
+
page_size: TABLE_PAGE_SIZE,
|
|
2973
|
+
row_height: "low",
|
|
2974
|
+
header_style: {
|
|
2975
|
+
background_style: "grey",
|
|
2976
|
+
bold: true,
|
|
2977
|
+
lines: 1
|
|
2978
|
+
},
|
|
2979
|
+
columns: [{
|
|
2980
|
+
name: "name",
|
|
2981
|
+
display_name: card.links.nameHeader,
|
|
2982
|
+
data_type: "text",
|
|
2983
|
+
width: "auto"
|
|
2984
|
+
}, {
|
|
2985
|
+
name: "link",
|
|
2986
|
+
display_name: card.links.linkHeader,
|
|
2987
|
+
data_type: "markdown",
|
|
2988
|
+
width: "auto"
|
|
2989
|
+
}],
|
|
2990
|
+
rows: card.links.rows.map((row) => ({
|
|
2991
|
+
name: row.name,
|
|
2992
|
+
link: `[查看](${linkTarget(row.url)})`
|
|
2993
|
+
}))
|
|
2994
|
+
});
|
|
2995
|
+
elements.push(note(statusNote(status, task)));
|
|
2905
2996
|
return {
|
|
2906
2997
|
schema: "2.0",
|
|
2907
2998
|
config: CARD_CONFIG,
|
|
2908
2999
|
header: {
|
|
2909
3000
|
title: {
|
|
2910
3001
|
tag: "plain_text",
|
|
2911
|
-
content:
|
|
3002
|
+
content: card.title
|
|
2912
3003
|
},
|
|
2913
3004
|
template: look.template,
|
|
2914
3005
|
text_tag_list: [{
|
|
@@ -2920,35 +3011,7 @@ function buildAnalyzeRootCard(subject, status) {
|
|
|
2920
3011
|
color: look.color
|
|
2921
3012
|
}]
|
|
2922
3013
|
},
|
|
2923
|
-
body: { elements
|
|
2924
|
-
markdown(`<at id=${subject.requesterUserId}></at> 于 ${plain$1(subject.triggeredAt)} 发起了对 **${plain$1(subject.app)}** 应用 **${plain$1(subject.errorCode)}** 错误码不良日志的分析。`),
|
|
2925
|
-
{
|
|
2926
|
-
tag: "table",
|
|
2927
|
-
page_size: TABLE_PAGE_SIZE,
|
|
2928
|
-
row_height: "low",
|
|
2929
|
-
header_style: {
|
|
2930
|
-
background_style: "grey",
|
|
2931
|
-
bold: true,
|
|
2932
|
-
lines: 1
|
|
2933
|
-
},
|
|
2934
|
-
columns: [{
|
|
2935
|
-
name: "name",
|
|
2936
|
-
display_name: "日志",
|
|
2937
|
-
data_type: "text",
|
|
2938
|
-
width: "auto"
|
|
2939
|
-
}, {
|
|
2940
|
-
name: "link",
|
|
2941
|
-
display_name: "详情页",
|
|
2942
|
-
data_type: "markdown",
|
|
2943
|
-
width: "auto"
|
|
2944
|
-
}],
|
|
2945
|
-
rows: subject.logDetails.map((detail) => ({
|
|
2946
|
-
name: detail.name,
|
|
2947
|
-
link: `[查看](${linkTarget(detail.url)})`
|
|
2948
|
-
}))
|
|
2949
|
-
},
|
|
2950
|
-
note(statusNote(status))
|
|
2951
|
-
] }
|
|
3014
|
+
body: { elements }
|
|
2952
3015
|
};
|
|
2953
3016
|
}
|
|
2954
3017
|
//#endregion
|
|
@@ -6261,7 +6324,8 @@ async function storeFileResource(native, channel, msg, resource) {
|
|
|
6261
6324
|
* - p2p: workspace `<baseDir>/p2p/<user_id>/`
|
|
6262
6325
|
* - group: workspace `<baseDir>/groups/<chat_id>/<user_id>/`
|
|
6263
6326
|
* - topic: `<workspace>/topics/<topic-token>/` (the Session's effective cwd)
|
|
6264
|
-
* -
|
|
6327
|
+
* - `feishuTopics` launch: workspace `<baseDir>/<caller segments…>/` (观象's
|
|
6328
|
+
* log analyses use `<baseDir>/analyze/<app>/<errorCode>/`)
|
|
6265
6329
|
*
|
|
6266
6330
|
* The workspace root is the Session header cwd: it keeps the Session grouped
|
|
6267
6331
|
* under the user's project in the GUI and stays the sandbox write boundary.
|
|
@@ -6442,26 +6506,35 @@ async function ensureWorkspace(ctx, baseDir, chatType, chatId, senderUserId) {
|
|
|
6442
6506
|
return ctx.workspaceRegistry.create(dir, workspaceTitleFor(chatType, chatId, senderUserId));
|
|
6443
6507
|
}
|
|
6444
6508
|
/**
|
|
6445
|
-
*
|
|
6446
|
-
*
|
|
6447
|
-
* application and error code is grouped there, each in its own topic
|
|
6448
|
-
* directory.
|
|
6509
|
+
* First path segments this module lays out itself; a `feishuTopics` launch
|
|
6510
|
+
* workspace may not start with one of them.
|
|
6449
6511
|
*/
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6512
|
+
const RESERVED_WORKSPACE_ROOTS = [
|
|
6513
|
+
"p2p",
|
|
6514
|
+
"groups",
|
|
6515
|
+
"topics",
|
|
6516
|
+
"_staging"
|
|
6517
|
+
];
|
|
6518
|
+
/**
|
|
6519
|
+
* Workspace directory of one `feishuTopics` launch:
|
|
6520
|
+
* `<baseDir>/<segments…>/`. Launches with the same segments share it, each in
|
|
6521
|
+
* its own topic directory.
|
|
6522
|
+
* @throws when a segment is unsafe or the first one is reserved.
|
|
6523
|
+
*/
|
|
6524
|
+
function launchWorkspacePathFor(baseDir, segments) {
|
|
6525
|
+
if (segments.length === 0) throw new Error("dsh-channels: a launch workspace needs at least one path segment");
|
|
6526
|
+
const safe = segments.map((segment) => safeSegment(segment, "launch workspace segment"));
|
|
6527
|
+
if (RESERVED_WORKSPACE_ROOTS.includes(safe[0])) throw new Error(`dsh-channels: launch workspace may not start with reserved segment ${JSON.stringify(safe[0])}`);
|
|
6528
|
+
return path.join(baseDir, ...safe);
|
|
6456
6529
|
}
|
|
6457
6530
|
/**
|
|
6458
|
-
* Ensure one
|
|
6459
|
-
*
|
|
6531
|
+
* Ensure one launch workspace exists and is registered (idempotent like
|
|
6532
|
+
* {@link ensureWorkspace}).
|
|
6460
6533
|
*/
|
|
6461
|
-
async function
|
|
6462
|
-
const dir =
|
|
6534
|
+
async function ensureLaunchWorkspace(ctx, baseDir, segments, title) {
|
|
6535
|
+
const dir = launchWorkspacePathFor(baseDir, segments);
|
|
6463
6536
|
await mkdir(dir, { recursive: true });
|
|
6464
|
-
return ctx.workspaceRegistry.create(dir,
|
|
6537
|
+
return ctx.workspaceRegistry.create(dir, title);
|
|
6465
6538
|
}
|
|
6466
6539
|
//#endregion
|
|
6467
6540
|
//#region src/session/isolation.ts
|
|
@@ -6730,7 +6803,7 @@ var InboundRouter = class {
|
|
|
6730
6803
|
*/
|
|
6731
6804
|
firstDeliveries = /* @__PURE__ */ new Map();
|
|
6732
6805
|
/**
|
|
6733
|
-
* topic id → reservation by an external starter (the
|
|
6806
|
+
* topic id → reservation by an external starter (the `feishuTopics` launcher) that
|
|
6734
6807
|
* opened the topic but has not published its route yet. A reply in the topic
|
|
6735
6808
|
* waits for the release and then routes against the published record, or
|
|
6736
6809
|
* falls through as an unbound topic when the start failed.
|
|
@@ -7461,117 +7534,22 @@ var InboundRouter = class {
|
|
|
7461
7534
|
}
|
|
7462
7535
|
};
|
|
7463
7536
|
//#endregion
|
|
7464
|
-
//#region src/
|
|
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";
|
|
7486
|
-
/** Largest accepted request body (the prompt dominates it). */
|
|
7487
|
-
const ANALYZE_MAX_BODY_BYTES = 1024 * 1024;
|
|
7488
|
-
/** Largest accepted prompt, in characters. */
|
|
7489
|
-
const ANALYZE_MAX_PROMPT_CHARS = 1e5;
|
|
7490
|
-
/**
|
|
7491
|
-
* `Idempotency-Key` format: 1–200 visible ASCII characters without spaces, so
|
|
7492
|
-
* a platform can use a UUID or its own `type:id` token.
|
|
7493
|
-
*/
|
|
7494
|
-
const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,200}$/u;
|
|
7495
|
-
/**
|
|
7496
|
-
* Application / error-code segment: it names a workspace directory
|
|
7497
|
-
* (`<baseDir>/analyze/<app>/<errorCode>`), so only path-safe characters pass.
|
|
7498
|
-
*/
|
|
7499
|
-
const SUBJECT_SEGMENT = /^(?!\.{1,2}$)[A-Za-z0-9._-]{1,128}$/u;
|
|
7500
|
-
/** JSON body of one analyze request. */
|
|
7501
|
-
const analyzeRequestSchema = z.object({
|
|
7502
|
-
/** Feishu user_id of the person who started the analysis on the platform. */
|
|
7503
|
-
requesterUserId: z.string().regex(FEISHU_USER_ID_PATTERN, "must be a Feishu user_id").max(128),
|
|
7504
|
-
/** When the analysis was started on the platform, shown verbatim on the root card. */
|
|
7505
|
-
triggeredAt: z.string().min(1).max(64).refine((value) => value.trim() !== "", "must not be blank"),
|
|
7506
|
-
/** Application the logs belong to (workspace directory segment). */
|
|
7507
|
-
app: z.string().regex(SUBJECT_SEGMENT, "must be 1-128 of A-Z a-z 0-9 . _ -"),
|
|
7508
|
-
/** Error code of the logs (workspace directory segment). */
|
|
7509
|
-
errorCode: z.string().regex(SUBJECT_SEGMENT, "must be 1-128 of A-Z a-z 0-9 . _ -"),
|
|
7510
|
-
/** Log-platform detail pages of the logs that triggered the analysis (listed on the root card). */
|
|
7511
|
-
logDetails: z.array(z.object({
|
|
7512
|
-
/** Row label. */
|
|
7513
|
-
name: z.string().min(1).max(200).refine((value) => value.trim() !== "", "must not be blank"),
|
|
7514
|
-
/** Detail page address. */
|
|
7515
|
-
url: z.url({
|
|
7516
|
-
protocol: /^https?$/u,
|
|
7517
|
-
error: "must be an http(s) URL"
|
|
7518
|
-
}).max(2048)
|
|
7519
|
-
}).strict()).min(1).max(50),
|
|
7520
|
-
/** Target group chat (`oc_…`); must be an authorized group. */
|
|
7521
|
-
chatId: z.string().regex(/^oc_[A-Za-z0-9_-]{1,128}$/u, "must be a Feishu group chat_id (oc_...)"),
|
|
7522
|
-
/** Prompt forwarded verbatim as the analysis Session's first user message. */
|
|
7523
|
-
prompt: z.string().min(1).max(ANALYZE_MAX_PROMPT_CHARS).refine((value) => value.trim() !== "", "must not be blank")
|
|
7524
|
-
}).strict();
|
|
7525
|
-
/**
|
|
7526
|
-
* Fingerprint of one validated request, used to tell a retry of the same
|
|
7527
|
-
* request from a reused `Idempotency-Key`. Field order is fixed.
|
|
7528
|
-
* @param request - the validated request.
|
|
7529
|
-
* @returns SHA-256 hex digest.
|
|
7530
|
-
*/
|
|
7531
|
-
function analyzeRequestHash(request) {
|
|
7532
|
-
const canonical = JSON.stringify([
|
|
7533
|
-
request.requesterUserId,
|
|
7534
|
-
request.triggeredAt,
|
|
7535
|
-
request.app,
|
|
7536
|
-
request.errorCode,
|
|
7537
|
-
request.logDetails.map((detail) => [detail.name, detail.url]),
|
|
7538
|
-
request.chatId,
|
|
7539
|
-
request.prompt
|
|
7540
|
-
]);
|
|
7541
|
-
return createHash("sha256").update(canonical).digest("hex");
|
|
7542
|
-
}
|
|
7543
|
-
/**
|
|
7544
|
-
* Validate one decoded JSON body.
|
|
7545
|
-
* @param raw - the decoded body.
|
|
7546
|
-
* @returns the request, or a readable list of field errors.
|
|
7547
|
-
*/
|
|
7548
|
-
function parseAnalyzeRequest(raw) {
|
|
7549
|
-
const parsed = analyzeRequestSchema.safeParse(raw);
|
|
7550
|
-
if (parsed.success) return {
|
|
7551
|
-
ok: true,
|
|
7552
|
-
value: parsed.data
|
|
7553
|
-
};
|
|
7554
|
-
return {
|
|
7555
|
-
ok: false,
|
|
7556
|
-
error: `请求参数不合法:${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(body)"}: ${issue.message}`).join("; ")}`
|
|
7557
|
-
};
|
|
7558
|
-
}
|
|
7559
|
-
//#endregion
|
|
7560
|
-
//#region src/analyze/launcher.ts
|
|
7537
|
+
//#region src/topics/launcher.ts
|
|
7561
7538
|
/**
|
|
7562
|
-
*
|
|
7539
|
+
* The `feishuTopics` service: other plugins start group-shared topics (see
|
|
7540
|
+
* `./contract.ts`).
|
|
7563
7541
|
*
|
|
7564
|
-
* Synchronously (before
|
|
7565
|
-
*
|
|
7566
|
-
*
|
|
7567
|
-
* card
|
|
7568
|
-
* directory, the Session, the group-shared route, and the
|
|
7569
|
-
*
|
|
7570
|
-
* the first turn settles, the root card's status follows it
|
|
7571
|
-
* 失败), and the
|
|
7572
|
-
* records still `running` belong to
|
|
7573
|
-
* finished; {@link
|
|
7574
|
-
*
|
|
7542
|
+
* Synchronously (before {@link TopicLauncher.launch} settles): spec
|
|
7543
|
+
* validation, service state, target-group permission, idempotency replay, and
|
|
7544
|
+
* the root card (进行中). In the background: the topic (opened by the first
|
|
7545
|
+
* streaming card, replied in thread below the root card), the launch workspace
|
|
7546
|
+
* and its per-topic directory, the Session, the group-shared route, and the
|
|
7547
|
+
* first turn: the prompt verbatim, streamed into that card like any topic
|
|
7548
|
+
* turn. When the first turn settles, the root card's status follows it
|
|
7549
|
+
* (已完成 / 已停止 / 失败), and the launch record keeps the same status. At
|
|
7550
|
+
* channel start, records still `running` belong to topics a previous instance
|
|
7551
|
+
* never finished; {@link TopicLauncher.markInterrupted} marks their cards
|
|
7552
|
+
* 已中断.
|
|
7575
7553
|
*
|
|
7576
7554
|
* The topic is reserved in the inbound router from its creation until the
|
|
7577
7555
|
* route and the first prompt are published, so replies posted meanwhile queue
|
|
@@ -7581,152 +7559,179 @@ function parseAnalyzeRequest(raw) {
|
|
|
7581
7559
|
* root card failed with the reason, closes the streaming card, and leaves no
|
|
7582
7560
|
* route.
|
|
7583
7561
|
*
|
|
7584
|
-
* @module dsh-channels/
|
|
7585
|
-
*/
|
|
7562
|
+
* @module dsh-channels/topics/launcher
|
|
7563
|
+
*/
|
|
7564
|
+
/** Name the launcher is provided under. */
|
|
7565
|
+
const FEISHU_TOPICS_SERVICE = "feishuTopics";
|
|
7566
|
+
/** Contract version the launcher implements. */
|
|
7567
|
+
const FEISHU_TOPICS_VERSION = 1;
|
|
7568
|
+
const text = (max) => z.string().min(1).max(max).refine((value) => value.trim() !== "", "must not be blank");
|
|
7569
|
+
/** Launch spec validation: the rules {@link TopicLaunchSpec} documents. */
|
|
7570
|
+
const topicLaunchSpecSchema = z.object({
|
|
7571
|
+
key: z.string().regex(/^[\x21-\x7E]{1,200}$/u, "must be 1-200 visible ASCII characters"),
|
|
7572
|
+
fingerprint: z.string().min(1).max(512),
|
|
7573
|
+
chatId: z.string().regex(/^oc_[A-Za-z0-9_-]{1,128}$/u, "must be a Feishu group chat_id (oc_...)"),
|
|
7574
|
+
requesterUserId: z.string().regex(FEISHU_USER_ID_PATTERN, "must be a Feishu user_id").max(128),
|
|
7575
|
+
card: z.object({
|
|
7576
|
+
title: text(100),
|
|
7577
|
+
taskName: text(20),
|
|
7578
|
+
lead: z.array(z.object({
|
|
7579
|
+
text: z.string().max(1e3),
|
|
7580
|
+
bold: z.boolean().optional()
|
|
7581
|
+
}).strict()).min(1).max(20),
|
|
7582
|
+
links: z.object({
|
|
7583
|
+
nameHeader: text(20),
|
|
7584
|
+
linkHeader: text(20),
|
|
7585
|
+
rows: z.array(z.object({
|
|
7586
|
+
name: text(200),
|
|
7587
|
+
url: z.url({
|
|
7588
|
+
protocol: /^https?$/u,
|
|
7589
|
+
error: "must be an http(s) URL"
|
|
7590
|
+
}).max(2048)
|
|
7591
|
+
}).strict()).max(50)
|
|
7592
|
+
}).strict().optional()
|
|
7593
|
+
}).strict(),
|
|
7594
|
+
workspace: z.object({
|
|
7595
|
+
segments: z.array(z.string().regex(/^(?!\.{1,2}$)[A-Za-z0-9._-]{1,128}$/u, "must be 1-128 of A-Z a-z 0-9 . _ -")).min(1).max(8).refine((segments) => !RESERVED_WORKSPACE_ROOTS.includes(segments[0] ?? ""), `must not start with ${RESERVED_WORKSPACE_ROOTS.join(", ")}`),
|
|
7596
|
+
title: text(200)
|
|
7597
|
+
}).strict(),
|
|
7598
|
+
prompt: text(1e5)
|
|
7599
|
+
}).strict();
|
|
7586
7600
|
/** Readable reason of an error value. */
|
|
7587
7601
|
function messageOf$4(error) {
|
|
7588
7602
|
return error instanceof Error ? error.message : String(error);
|
|
7589
7603
|
}
|
|
7590
|
-
var
|
|
7604
|
+
var TopicLauncher = class {
|
|
7591
7605
|
deps;
|
|
7592
|
-
|
|
7606
|
+
version = 1;
|
|
7607
|
+
/** Idempotency key → launch still being accepted (concurrent retries share it). */
|
|
7593
7608
|
accepting = /* @__PURE__ */ new Map();
|
|
7594
|
-
/** Background
|
|
7609
|
+
/** Background topics still starting (drained on shutdown). */
|
|
7595
7610
|
running = /* @__PURE__ */ new Set();
|
|
7596
|
-
/** Whether
|
|
7611
|
+
/** Whether launches are admitted: from {@link open} after start until {@link close}. */
|
|
7597
7612
|
admitting = false;
|
|
7598
7613
|
constructor(deps) {
|
|
7599
7614
|
this.deps = deps;
|
|
7600
7615
|
}
|
|
7601
7616
|
/**
|
|
7602
|
-
* Start admitting
|
|
7603
|
-
* {@link markInterrupted}, so a new
|
|
7617
|
+
* Start admitting launches. Called at channel start after
|
|
7618
|
+
* {@link markInterrupted}, so a new launch is never marked interrupted.
|
|
7604
7619
|
*/
|
|
7605
7620
|
open() {
|
|
7606
7621
|
this.admitting = true;
|
|
7607
7622
|
}
|
|
7608
|
-
/**
|
|
7623
|
+
/** Refuse launches (`not-connected`) ahead of shutdown. */
|
|
7609
7624
|
close() {
|
|
7610
7625
|
this.admitting = false;
|
|
7611
7626
|
}
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
}
|
|
7618
|
-
async analyze(request, idempotencyKey) {
|
|
7619
|
-
const bodyHash = analyzeRequestHash(request);
|
|
7620
|
-
const pending = this.accepting.get(idempotencyKey);
|
|
7627
|
+
async launch(spec) {
|
|
7628
|
+
const parsed = topicLaunchSpecSchema.safeParse(spec);
|
|
7629
|
+
if (!parsed.success) return refused("invalid", `话题参数不合法:${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(spec)"}: ${issue.message}`).join("; ")}`);
|
|
7630
|
+
const valid = parsed.data;
|
|
7631
|
+
const pending = this.accepting.get(valid.key);
|
|
7621
7632
|
if (pending !== void 0) {
|
|
7622
|
-
if (pending.
|
|
7623
|
-
return await pending.
|
|
7633
|
+
if (pending.fingerprint !== valid.fingerprint) return reusedKey();
|
|
7634
|
+
return await pending.result;
|
|
7624
7635
|
}
|
|
7625
|
-
const
|
|
7626
|
-
this.accepting.set(
|
|
7627
|
-
|
|
7628
|
-
|
|
7636
|
+
const result = this.accept(valid);
|
|
7637
|
+
this.accepting.set(valid.key, {
|
|
7638
|
+
fingerprint: valid.fingerprint,
|
|
7639
|
+
result
|
|
7629
7640
|
});
|
|
7630
7641
|
try {
|
|
7631
|
-
return await
|
|
7642
|
+
return await result;
|
|
7632
7643
|
} finally {
|
|
7633
|
-
this.accepting.delete(
|
|
7644
|
+
this.accepting.delete(valid.key);
|
|
7634
7645
|
}
|
|
7635
7646
|
}
|
|
7636
7647
|
/**
|
|
7637
|
-
* Wait for every acceptance in flight, then every background
|
|
7638
|
-
*
|
|
7648
|
+
* Wait for every acceptance in flight, then every background topic that is
|
|
7649
|
+
* still starting. Called after `started` turns false, so each one ends on
|
|
7639
7650
|
* its failure card or, when its first prompt was already sent, as an
|
|
7640
7651
|
* ordinary topic.
|
|
7641
7652
|
*/
|
|
7642
7653
|
async drain() {
|
|
7643
|
-
await Promise.allSettled([...this.accepting.values()].map((pending) => pending.
|
|
7654
|
+
await Promise.allSettled([...this.accepting.values()].map((pending) => pending.result));
|
|
7644
7655
|
await Promise.allSettled([...this.running]);
|
|
7645
7656
|
}
|
|
7646
7657
|
/**
|
|
7647
|
-
* Mark every
|
|
7648
|
-
*
|
|
7649
|
-
*
|
|
7650
|
-
*
|
|
7651
|
-
*
|
|
7658
|
+
* Mark every launch still `running` as interrupted, on its card and in its
|
|
7659
|
+
* record. Called at channel start before admission opens, so each such
|
|
7660
|
+
* record belongs to a previous instance whose first turn never settled. A
|
|
7661
|
+
* card that cannot be updated (deleted, recalled) is logged and still
|
|
7662
|
+
* recorded as interrupted, so it is not retried on every start.
|
|
7652
7663
|
*/
|
|
7653
7664
|
async markInterrupted() {
|
|
7654
7665
|
const channel = this.deps.channel();
|
|
7655
7666
|
const domain = this.deps.domain();
|
|
7656
7667
|
if (channel === void 0 || domain === void 0) return;
|
|
7657
|
-
for (const [key, record] of [...domain.
|
|
7668
|
+
for (const [key, record] of [...domain.topicLaunches.entries()]) {
|
|
7658
7669
|
if (record.status !== "running") continue;
|
|
7659
|
-
|
|
7660
|
-
|
|
7670
|
+
const root = rootCardOf(record);
|
|
7671
|
+
if (root !== void 0) await channel.updateCard(record.messageId, buildTopicRootCard(root.requesterUserId, root.card, { kind: "interrupted" })).catch((error) => {
|
|
7672
|
+
this.deps.ctx.logger?.warn?.("dsh-channels: marking topic card %s interrupted failed: %s", key, String(error));
|
|
7661
7673
|
});
|
|
7662
7674
|
await this.recordStatus(key, "interrupted");
|
|
7663
7675
|
}
|
|
7664
7676
|
}
|
|
7665
|
-
async accept(
|
|
7677
|
+
async accept(spec) {
|
|
7666
7678
|
const channel = this.deps.channel();
|
|
7667
7679
|
const domain = this.deps.domain();
|
|
7668
|
-
if (!this.admitting || !this.deps.started() || channel === void 0 || domain === void 0) return refused(
|
|
7669
|
-
const stored = domain.
|
|
7680
|
+
if (!this.admitting || !this.deps.started() || channel === void 0 || domain === void 0) return refused("not-connected", "飞书渠道尚未连接,请稍后重试");
|
|
7681
|
+
const stored = domain.topicLaunches.get(spec.key);
|
|
7670
7682
|
if (stored !== void 0) {
|
|
7671
|
-
if (stored.bodyHash !==
|
|
7683
|
+
if (stored.bodyHash !== spec.fingerprint) return reusedKey();
|
|
7672
7684
|
return {
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
messageId: stored.messageId
|
|
7678
|
-
}
|
|
7685
|
+
ok: true,
|
|
7686
|
+
replayed: true,
|
|
7687
|
+
chatId: stored.chatId,
|
|
7688
|
+
messageId: stored.messageId
|
|
7679
7689
|
};
|
|
7680
7690
|
}
|
|
7681
|
-
if (!this.deps.
|
|
7682
|
-
const
|
|
7691
|
+
if (!this.deps.mayLaunchIn(spec.chatId)) return refused("forbidden-chat", "目标群未授权:请管理员在该群发送 /auth group add,或在设置面板「访问与授权」中添加");
|
|
7692
|
+
const card = copyCard(spec.card);
|
|
7683
7693
|
let rootId;
|
|
7684
7694
|
try {
|
|
7685
|
-
rootId = (await channel.send(
|
|
7695
|
+
rootId = (await channel.send(spec.chatId, { card: buildTopicRootCard(spec.requesterUserId, card, { kind: "running" }) })).messageId;
|
|
7686
7696
|
} catch (error) {
|
|
7687
|
-
return refused(
|
|
7697
|
+
return refused("send-failed", `${spec.card.taskName}卡片发送失败(机器人需在目标群内):${messageOf$4(error)}`);
|
|
7688
7698
|
}
|
|
7689
|
-
const body = {
|
|
7690
|
-
ok: true,
|
|
7691
|
-
chatId: request.chatId,
|
|
7692
|
-
messageId: rootId
|
|
7693
|
-
};
|
|
7694
7699
|
try {
|
|
7695
|
-
await domain.
|
|
7696
|
-
key,
|
|
7697
|
-
bodyHash,
|
|
7698
|
-
chatId:
|
|
7700
|
+
await domain.topicLaunches.put(spec.key, {
|
|
7701
|
+
key: spec.key,
|
|
7702
|
+
bodyHash: spec.fingerprint,
|
|
7703
|
+
chatId: spec.chatId,
|
|
7699
7704
|
messageId: rootId,
|
|
7700
7705
|
acceptedAt: Date.now(),
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
logDetails: [...subject.logDetails]
|
|
7704
|
-
},
|
|
7706
|
+
requesterUserId: spec.requesterUserId,
|
|
7707
|
+
card,
|
|
7705
7708
|
status: "running"
|
|
7706
7709
|
});
|
|
7707
7710
|
} catch (error) {
|
|
7708
|
-
this.deps.ctx.logger?.warn?.("dsh-channels: recording
|
|
7711
|
+
this.deps.ctx.logger?.warn?.("dsh-channels: recording topic launch %s failed: %s", spec.key, String(error));
|
|
7709
7712
|
}
|
|
7710
|
-
const run = this.
|
|
7713
|
+
const run = this.start(channel, spec, card, rootId);
|
|
7711
7714
|
this.running.add(run);
|
|
7712
7715
|
run.finally(() => {
|
|
7713
7716
|
this.running.delete(run);
|
|
7714
7717
|
});
|
|
7715
7718
|
return {
|
|
7716
|
-
|
|
7717
|
-
|
|
7719
|
+
ok: true,
|
|
7720
|
+
replayed: false,
|
|
7721
|
+
chatId: spec.chatId,
|
|
7722
|
+
messageId: rootId
|
|
7718
7723
|
};
|
|
7719
7724
|
}
|
|
7720
|
-
/** Start the
|
|
7721
|
-
async
|
|
7725
|
+
/** Start the topic below an accepted root card; every failure ends on the cards. */
|
|
7726
|
+
async start(channel, spec, card, rootId) {
|
|
7722
7727
|
const warn = (message, ...args) => {
|
|
7723
7728
|
this.deps.ctx.logger?.warn?.(message, ...args);
|
|
7724
7729
|
};
|
|
7725
7730
|
const showStatus = async (status) => {
|
|
7726
|
-
await channel.updateCard(rootId,
|
|
7727
|
-
warn("dsh-channels: updating
|
|
7731
|
+
await channel.updateCard(rootId, buildTopicRootCard(spec.requesterUserId, card, status)).catch((error) => {
|
|
7732
|
+
warn("dsh-channels: updating topic card %s failed: %s", rootId, String(error));
|
|
7728
7733
|
});
|
|
7729
|
-
await this.recordStatus(key, status.kind);
|
|
7734
|
+
await this.recordStatus(spec.key, status.kind);
|
|
7730
7735
|
};
|
|
7731
7736
|
const sessionId = randomUUID();
|
|
7732
7737
|
let handle;
|
|
@@ -7735,19 +7740,19 @@ var AnalyzeLauncher = class {
|
|
|
7735
7740
|
let releaseTopic;
|
|
7736
7741
|
let published = false;
|
|
7737
7742
|
try {
|
|
7738
|
-
const topic = await this.openTopic(channel,
|
|
7743
|
+
const topic = await this.openTopic(channel, spec.chatId, rootId, sessionId);
|
|
7739
7744
|
cardId = topic.cardId;
|
|
7740
7745
|
threadId = topic.threadId;
|
|
7741
7746
|
releaseTopic = this.deps.reserveTopic(threadId);
|
|
7742
7747
|
const services = placementServices(this.deps.ctx);
|
|
7743
|
-
const workspace = await
|
|
7748
|
+
const workspace = await ensureLaunchWorkspace(this.deps.ctx, this.deps.config.baseDir, spec.workspace.segments, spec.workspace.title);
|
|
7744
7749
|
const topicToken = topicTokenFor(threadId, "thread", sessionId);
|
|
7745
7750
|
const plan = await planTopic(services, this.deps.config, workspace.path, topicToken, warn);
|
|
7746
7751
|
this.assertStarted();
|
|
7747
7752
|
handle = await this.deps.sessions.createSession(sessionId, plan.cwd);
|
|
7748
7753
|
const placement = await enterTopic(services, handle.agent, plan, workspace.path, topicToken, warn);
|
|
7749
7754
|
if (plan.cwd === workspace.path) Promise.resolve(workspace.attachSession(sessionId)).catch((error) => {
|
|
7750
|
-
warn("dsh-channels: attaching
|
|
7755
|
+
warn("dsh-channels: attaching topic session %s to workspace failed: %s", sessionId, String(error));
|
|
7751
7756
|
});
|
|
7752
7757
|
await this.deps.sessions.mountGuidance(handle.agent, placement.sessionDir);
|
|
7753
7758
|
this.assertStarted();
|
|
@@ -7756,9 +7761,9 @@ var AnalyzeLauncher = class {
|
|
|
7756
7761
|
key: threadId,
|
|
7757
7762
|
kind: "thread",
|
|
7758
7763
|
sessionId,
|
|
7759
|
-
chatId:
|
|
7764
|
+
chatId: spec.chatId,
|
|
7760
7765
|
chatType: "group",
|
|
7761
|
-
senderUserId:
|
|
7766
|
+
senderUserId: spec.requesterUserId,
|
|
7762
7767
|
workspacePath: workspace.path,
|
|
7763
7768
|
sessionDir: placement.sessionDir,
|
|
7764
7769
|
...placement.worktreeBranch === void 0 ? {} : { worktreeBranch: placement.worktreeBranch },
|
|
@@ -7774,7 +7779,7 @@ var AnalyzeLauncher = class {
|
|
|
7774
7779
|
await this.deps.repair.write({
|
|
7775
7780
|
key: cardId,
|
|
7776
7781
|
sessionId,
|
|
7777
|
-
chatId:
|
|
7782
|
+
chatId: spec.chatId,
|
|
7778
7783
|
triggerMessageId: rootId,
|
|
7779
7784
|
cards: [{
|
|
7780
7785
|
messageId: cardId,
|
|
@@ -7793,13 +7798,13 @@ var AnalyzeLauncher = class {
|
|
|
7793
7798
|
this.deps.streamer()?.attach(agent.session, streamCardId, rootId, void 0, { onSettled: (outcome) => {
|
|
7794
7799
|
showStatus({ kind: outcome });
|
|
7795
7800
|
} });
|
|
7796
|
-
agent.followup(feishuUserMessage(
|
|
7801
|
+
agent.followup(feishuUserMessage(spec.prompt));
|
|
7797
7802
|
});
|
|
7798
7803
|
} catch (error) {
|
|
7799
7804
|
const reason = messageOf$4(error);
|
|
7800
|
-
warn("dsh-channels:
|
|
7805
|
+
warn("dsh-channels: topic %s failed to start: %s", spec.key, reason);
|
|
7801
7806
|
if (published && threadId !== void 0) await this.deps.routes.remove(threadId).catch((removeError) => {
|
|
7802
|
-
warn("dsh-channels: removing
|
|
7807
|
+
warn("dsh-channels: removing topic route %s failed: %s", threadId, String(removeError));
|
|
7803
7808
|
});
|
|
7804
7809
|
await handle?.dispose().catch(() => {});
|
|
7805
7810
|
if (cardId !== void 0) {
|
|
@@ -7807,7 +7812,7 @@ var AnalyzeLauncher = class {
|
|
|
7807
7812
|
await channel.updateCard(cardId, buildCard({
|
|
7808
7813
|
phase: "error",
|
|
7809
7814
|
reasoning: "",
|
|
7810
|
-
body: `❌
|
|
7815
|
+
body: `❌ ${card.taskName}未能开始:${reason}`,
|
|
7811
7816
|
stats: {
|
|
7812
7817
|
sessionId,
|
|
7813
7818
|
inputTokens: 0,
|
|
@@ -7824,8 +7829,8 @@ var AnalyzeLauncher = class {
|
|
|
7824
7829
|
}
|
|
7825
7830
|
}
|
|
7826
7831
|
/**
|
|
7827
|
-
* Open the
|
|
7828
|
-
*
|
|
7832
|
+
* Open the topic: the first streaming card, replied in thread below the
|
|
7833
|
+
* root card. The topic id is read from that card (or the root card, in
|
|
7829
7834
|
* topic groups).
|
|
7830
7835
|
*/
|
|
7831
7836
|
async openTopic(channel, chatId, rootId, sessionId) {
|
|
@@ -7852,44 +7857,52 @@ var AnalyzeLauncher = class {
|
|
|
7852
7857
|
threadId
|
|
7853
7858
|
};
|
|
7854
7859
|
}
|
|
7855
|
-
/** Store a root card status in the
|
|
7860
|
+
/** Store a root card status in the launch record (best-effort; the card already shows it). */
|
|
7856
7861
|
async recordStatus(key, status) {
|
|
7857
7862
|
const domain = this.deps.domain();
|
|
7858
|
-
const record = domain?.
|
|
7863
|
+
const record = domain?.topicLaunches.get(key);
|
|
7859
7864
|
if (domain === void 0 || record === void 0) return;
|
|
7860
7865
|
try {
|
|
7861
|
-
await domain.
|
|
7866
|
+
await domain.topicLaunches.put(key, {
|
|
7862
7867
|
...record,
|
|
7863
7868
|
status
|
|
7864
7869
|
});
|
|
7865
7870
|
} catch (error) {
|
|
7866
|
-
this.deps.ctx.logger?.warn?.("dsh-channels: recording
|
|
7871
|
+
this.deps.ctx.logger?.warn?.("dsh-channels: recording topic %s status failed: %s", key, String(error));
|
|
7867
7872
|
}
|
|
7868
7873
|
}
|
|
7869
7874
|
assertStarted() {
|
|
7870
|
-
if (!this.deps.started()) throw new Error("
|
|
7875
|
+
if (!this.deps.started()) throw new Error("飞书渠道已断开,已取消");
|
|
7871
7876
|
}
|
|
7872
7877
|
};
|
|
7873
|
-
|
|
7878
|
+
/** A detached copy of the card, so the caller cannot change what is recorded. */
|
|
7879
|
+
function copyCard(card) {
|
|
7874
7880
|
return {
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7881
|
+
title: card.title,
|
|
7882
|
+
taskName: card.taskName,
|
|
7883
|
+
lead: card.lead.map((run) => run.bold === void 0 ? { text: run.text } : {
|
|
7884
|
+
text: run.text,
|
|
7885
|
+
bold: run.bold
|
|
7886
|
+
}),
|
|
7887
|
+
...card.links === void 0 ? {} : { links: {
|
|
7888
|
+
nameHeader: card.links.nameHeader,
|
|
7889
|
+
linkHeader: card.links.linkHeader,
|
|
7890
|
+
rows: card.links.rows.map((row) => ({
|
|
7891
|
+
name: row.name,
|
|
7892
|
+
url: row.url
|
|
7893
|
+
}))
|
|
7894
|
+
} }
|
|
7880
7895
|
};
|
|
7881
7896
|
}
|
|
7882
|
-
function refused(
|
|
7897
|
+
function refused(code, message) {
|
|
7883
7898
|
return {
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
error
|
|
7888
|
-
}
|
|
7899
|
+
ok: false,
|
|
7900
|
+
code,
|
|
7901
|
+
message
|
|
7889
7902
|
};
|
|
7890
7903
|
}
|
|
7891
7904
|
function reusedKey() {
|
|
7892
|
-
return refused(
|
|
7905
|
+
return refused("key-conflict", "幂等键已被内容不同的请求使用");
|
|
7893
7906
|
}
|
|
7894
7907
|
//#endregion
|
|
7895
7908
|
//#region src/session/repair.ts
|
|
@@ -14982,6 +14995,8 @@ var TurnStreamer = class {
|
|
|
14982
14995
|
toolKind: "",
|
|
14983
14996
|
heartbeatTimer: void 0,
|
|
14984
14997
|
attempts: /* @__PURE__ */ new Map(),
|
|
14998
|
+
retry: void 0,
|
|
14999
|
+
retries: 0,
|
|
14985
15000
|
stopRequested: false,
|
|
14986
15001
|
stopDenied: false,
|
|
14987
15002
|
queuedDenied: false
|
|
@@ -15170,6 +15185,10 @@ var TurnStreamer = class {
|
|
|
15170
15185
|
break;
|
|
15171
15186
|
case "assistant/message":
|
|
15172
15187
|
if (state.turn === void 0) state.turn = event.data.turn;
|
|
15188
|
+
if (state.retry !== void 0) {
|
|
15189
|
+
state.retry = void 0;
|
|
15190
|
+
this.markDirty(session.id, state);
|
|
15191
|
+
}
|
|
15173
15192
|
if (event.data.usage !== void 0) {
|
|
15174
15193
|
state.lastUsage = {
|
|
15175
15194
|
usage: event.data.usage,
|
|
@@ -15251,6 +15270,24 @@ var TurnStreamer = class {
|
|
|
15251
15270
|
}
|
|
15252
15271
|
break;
|
|
15253
15272
|
}
|
|
15273
|
+
case "llm/retry":
|
|
15274
|
+
if (state.turn === void 0) state.turn = event.data.turn;
|
|
15275
|
+
state.retry = {
|
|
15276
|
+
retry: event.data.retry,
|
|
15277
|
+
maxRetries: event.data.mode === "normal" ? event.data.maxRetries : void 0,
|
|
15278
|
+
code: event.data.failure.code,
|
|
15279
|
+
until: Date.now() + Math.max(0, event.data.delayMs),
|
|
15280
|
+
started: false
|
|
15281
|
+
};
|
|
15282
|
+
state.retries++;
|
|
15283
|
+
this.markDirty(session.id, state);
|
|
15284
|
+
break;
|
|
15285
|
+
case "llm/retry-started":
|
|
15286
|
+
if (state.retry !== void 0 && state.retry.retry === event.data.retry) {
|
|
15287
|
+
state.retry.started = true;
|
|
15288
|
+
this.markDirty(session.id, state);
|
|
15289
|
+
}
|
|
15290
|
+
break;
|
|
15254
15291
|
case "turn/end":
|
|
15255
15292
|
if (state.turn === void 0) state.turn = event.data.turn;
|
|
15256
15293
|
this.finalize(session.id, state, turnFailure(event.data.reason), turnAborted(event.data.reason));
|
|
@@ -15279,6 +15316,10 @@ var TurnStreamer = class {
|
|
|
15279
15316
|
break;
|
|
15280
15317
|
case "chunk": {
|
|
15281
15318
|
const chunk = frame.chunk;
|
|
15319
|
+
if ((chunk.type === "text-delta" || chunk.type === "reasoning-delta") && chunk.text !== "" && state.retry !== void 0) {
|
|
15320
|
+
state.retry = void 0;
|
|
15321
|
+
this.markDirty(session.id, state);
|
|
15322
|
+
}
|
|
15282
15323
|
if (chunk.type === "text-delta") {
|
|
15283
15324
|
if (chunk.text !== "") this.closeReasoning(state);
|
|
15284
15325
|
const last = state.cards[state.cards.length - 1];
|
|
@@ -15298,7 +15339,7 @@ var TurnStreamer = class {
|
|
|
15298
15339
|
this.closeReasoning(state);
|
|
15299
15340
|
const mark = state.attempts.get(frame.attemptId);
|
|
15300
15341
|
state.attempts.delete(frame.attemptId);
|
|
15301
|
-
if (frame.outcome.kind === "abandoned" && mark !== void 0) {
|
|
15342
|
+
if ((frame.outcome.kind === "abandoned" || frame.outcome.eventType === "assistant/attempt") && mark !== void 0) {
|
|
15302
15343
|
this.rollbackAttempt(state, mark);
|
|
15303
15344
|
this.markDirty(session.id, state);
|
|
15304
15345
|
}
|
|
@@ -15323,11 +15364,13 @@ var TurnStreamer = class {
|
|
|
15323
15364
|
state.reasoningSince = void 0;
|
|
15324
15365
|
}
|
|
15325
15366
|
/**
|
|
15326
|
-
* Drop one
|
|
15367
|
+
* Drop one discarded attempt's deltas. The attempt's text is a contiguous
|
|
15327
15368
|
* suffix of the logical body: segments created during the attempt are
|
|
15328
15369
|
* removed, the segment that was last at `start` is truncated, and the
|
|
15329
15370
|
* reasoning tail is cut. Already-delivered plain-text segments cannot be
|
|
15330
|
-
* retracted and stop the removal
|
|
15371
|
+
* retracted and stop the removal; segment cards already sent (or being
|
|
15372
|
+
* sent) cannot be withdrawn, so they are emptied and kept. The newest
|
|
15373
|
+
* remaining card is reopened so a retry streams into it again.
|
|
15331
15374
|
*/
|
|
15332
15375
|
rollbackAttempt(state, mark) {
|
|
15333
15376
|
const lastIndex = mark.cardCount - 1;
|
|
@@ -15335,6 +15378,11 @@ var TurnStreamer = class {
|
|
|
15335
15378
|
const card = state.cards[index];
|
|
15336
15379
|
if (card === void 0) continue;
|
|
15337
15380
|
if (card.mode === "text" && card.messageId !== void 0) break;
|
|
15381
|
+
if (card.messageId !== void 0 || card.sending) {
|
|
15382
|
+
card.body = "";
|
|
15383
|
+
card.dirty = true;
|
|
15384
|
+
continue;
|
|
15385
|
+
}
|
|
15338
15386
|
state.cards.splice(index, 1);
|
|
15339
15387
|
}
|
|
15340
15388
|
const tail = state.cards[lastIndex];
|
|
@@ -15343,6 +15391,9 @@ var TurnStreamer = class {
|
|
|
15343
15391
|
tail.body = tail.body.slice(0, length);
|
|
15344
15392
|
tail.dirty = true;
|
|
15345
15393
|
}
|
|
15394
|
+
const last = state.cards[state.cards.length - 1];
|
|
15395
|
+
if (last !== void 0) if (last.mode === "text" && last.messageId !== void 0 || last.failed) state.cards.push(this.freshSegment(""));
|
|
15396
|
+
else last.frozen = false;
|
|
15346
15397
|
state.reasoning = state.reasoning.slice(0, mark.reasoningLength);
|
|
15347
15398
|
state.reasoningMs = mark.reasoningMs;
|
|
15348
15399
|
state.reasoningSince = mark.reasoningSince;
|
|
@@ -15509,6 +15560,13 @@ var TurnStreamer = class {
|
|
|
15509
15560
|
toolLine(state, now) {
|
|
15510
15561
|
if (state.finalized) return "";
|
|
15511
15562
|
if (state.awaitingApproval.length > 0) return clipActivity(`⏳ 等待你审批:${[...new Set(state.awaitingApproval)].join("、")}`);
|
|
15563
|
+
const retry = state.retry;
|
|
15564
|
+
if (retry !== void 0) {
|
|
15565
|
+
const count = retry.maxRetries === void 0 ? `${retry.retry}` : `${retry.retry}/${retry.maxRetries}`;
|
|
15566
|
+
if (retry.started) return clipActivity(`🔁 模型请求重试中(第 ${count} 次)`);
|
|
15567
|
+
const wait = Math.max(0, Math.ceil((retry.until - now) / 1e3));
|
|
15568
|
+
return clipActivity(`⚠️ 模型请求失败(${retry.code}),${wait} 秒后第 ${count} 次重试`);
|
|
15569
|
+
}
|
|
15512
15570
|
const tool = state.toolActivity;
|
|
15513
15571
|
if (tool === void 0) return "";
|
|
15514
15572
|
return clipActivity(`${tool.name === "subagent" ? "🤖 调用子代理:" : tool.name === "workflow" ? "🧵 调用工作流:" : "🔧 调用工具:"}${tool.label} · ${fmtElapsed(now - tool.since)}`);
|
|
@@ -15699,7 +15757,8 @@ var TurnStreamer = class {
|
|
|
15699
15757
|
this.closeReasoning(state);
|
|
15700
15758
|
if (failure !== void 0) {
|
|
15701
15759
|
const code = failure.code === void 0 ? "" : `\n\n错误码:\`${failure.code}\``;
|
|
15702
|
-
const
|
|
15760
|
+
const retried = state.retries === 0 ? "" : `\n\n已自动重试 ${state.retries} 次`;
|
|
15761
|
+
const notice = `❌ **本轮运行失败**\n\n${failure.message}${code}${retried}`;
|
|
15703
15762
|
const tail = state.cards[state.cards.length - 1];
|
|
15704
15763
|
if (tail !== void 0) if (tail.mode === "text" || tail.failed) state.cards.push(this.freshSegment(notice));
|
|
15705
15764
|
else {
|
|
@@ -15716,6 +15775,7 @@ var TurnStreamer = class {
|
|
|
15716
15775
|
state.heartbeatTimer = void 0;
|
|
15717
15776
|
}
|
|
15718
15777
|
state.tool = "";
|
|
15778
|
+
state.retry = void 0;
|
|
15719
15779
|
this.splitOverflow(sessionId, state);
|
|
15720
15780
|
for (const [index] of state.cards.entries()) this.patchCard(sessionId, state, index, true, false);
|
|
15721
15781
|
for (const trigger of [state, ...state.mergedTriggers]) this.swapReaction(trigger, failure !== void 0);
|
|
@@ -15896,8 +15956,24 @@ const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 2e4;
|
|
|
15896
15956
|
* optional context block.
|
|
15897
15957
|
*/
|
|
15898
15958
|
const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3e3;
|
|
15899
|
-
/**
|
|
15900
|
-
|
|
15959
|
+
/**
|
|
15960
|
+
* Keys of the log-platform analyze API, which moved to 观象
|
|
15961
|
+
* (`@modesto_ma/dsh-guanxiang`). A value still set in `cordis.yml` or the Web
|
|
15962
|
+
* overlay is ignored; the loader schema passes them through and
|
|
15963
|
+
* {@link movedAnalyzeKeysOf} finds them so the deployment is told.
|
|
15964
|
+
*/
|
|
15965
|
+
const MOVED_ANALYZE_KEYS = ["analyzeEnabled", "analyzeToken"];
|
|
15966
|
+
/** Log line naming where the moved analyze API settings went. */
|
|
15967
|
+
const MOVED_ANALYZE_NOTICE = "dsh-channels: 日志分析接口已迁到观象(@modesto_ma/dsh-guanxiang),忽略 %s:请在观象面板「分析接口」中开启并保存令牌(GUANXIANG_ANALYZE_TOKEN)";
|
|
15968
|
+
/**
|
|
15969
|
+
* The moved analyze API keys one raw config or overlay object still sets.
|
|
15970
|
+
* @param raw - undecoded `config` or overlay JSON.
|
|
15971
|
+
* @returns the keys present, in {@link MOVED_ANALYZE_KEYS} order.
|
|
15972
|
+
*/
|
|
15973
|
+
function movedAnalyzeKeysOf(raw) {
|
|
15974
|
+
if (typeof raw !== "object" || raw === null) return [];
|
|
15975
|
+
return MOVED_ANALYZE_KEYS.filter((key) => Object.hasOwn(raw, key));
|
|
15976
|
+
}
|
|
15901
15977
|
/** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
|
|
15902
15978
|
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");
|
|
15903
15979
|
const configSchema = z.object({
|
|
@@ -16227,28 +16303,20 @@ const configSchema = z.object({
|
|
|
16227
16303
|
* this window fails closed with an explicit error; a host *timed* wait owns
|
|
16228
16304
|
* its own deadline and is left to the host signal instead.
|
|
16229
16305
|
*/
|
|
16230
|
-
questionTimeoutMs: z.number().int().min(1e4).max(36e5).default(DEFAULT_QUESTION_TIMEOUT_MS)
|
|
16231
|
-
/**
|
|
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.
|
|
16235
|
-
*/
|
|
16236
|
-
analyzeEnabled: z.boolean().default(false),
|
|
16237
|
-
/**
|
|
16238
|
-
* Credential reference holding the analyze API bearer token (defaults to
|
|
16239
|
-
* `DSH_CHANNELS_ANALYZE_TOKEN`). Resolved per request, so rotating the value
|
|
16240
|
-
* applies to the next request; an unresolvable token refuses every request.
|
|
16241
|
-
*/
|
|
16242
|
-
analyzeToken: credentialRefSchema.default(DEFAULT_ANALYZE_TOKEN_REF)
|
|
16306
|
+
questionTimeoutMs: z.number().int().min(1e4).max(36e5).default(DEFAULT_QUESTION_TIMEOUT_MS)
|
|
16243
16307
|
});
|
|
16244
16308
|
/**
|
|
16245
16309
|
* DSH loader-facing schema. The bundle patch inserts this plugin without a
|
|
16246
16310
|
* `config` object; the loader validates that row with `undefined` before the
|
|
16247
16311
|
* user's patch layer supplies values. Accept that absence and fill the same
|
|
16248
16312
|
* defaults as {@link resolveConfig}, so enabling the plugin from the Web UI
|
|
16249
|
-
* cannot fail with "expected object, received undefined".
|
|
16313
|
+
* cannot fail with "expected object, received undefined". The moved analyze
|
|
16314
|
+
* keys pass through unchanged so `apply()` can warn about them.
|
|
16250
16315
|
*/
|
|
16251
|
-
const pluginConfigSchema = configSchema.
|
|
16316
|
+
const pluginConfigSchema = configSchema.extend({
|
|
16317
|
+
analyzeEnabled: z.unknown().optional(),
|
|
16318
|
+
analyzeToken: z.unknown().optional()
|
|
16319
|
+
}).prefault({});
|
|
16252
16320
|
/**
|
|
16253
16321
|
* Validate the raw loader config. Values stay credential *references*; the
|
|
16254
16322
|
* secrets themselves resolve through `ctx.credentials` at startup.
|
|
@@ -16385,7 +16453,6 @@ const EDITABLE_KEYS = [
|
|
|
16385
16453
|
"feishuTools.wikiRead",
|
|
16386
16454
|
"feishuTools.wikiWrite",
|
|
16387
16455
|
"wikiRoots",
|
|
16388
|
-
"analyzeEnabled",
|
|
16389
16456
|
"loggerLevel"
|
|
16390
16457
|
];
|
|
16391
16458
|
/**
|
|
@@ -16513,11 +16580,6 @@ const PANEL_COPY = {
|
|
|
16513
16580
|
reconnectDone: "已重启飞书连接",
|
|
16514
16581
|
loading: "加载中…",
|
|
16515
16582
|
loadFailed: "设置面板加载失败",
|
|
16516
|
-
analyzeTokenLabel: "访问令牌",
|
|
16517
|
-
analyzeTokenSummary: "请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求",
|
|
16518
|
-
analyzeTokenPlaceholder: "填写新令牌(建议 32 位以上随机串)",
|
|
16519
|
-
analyzeTokenSaved: "令牌已保存,下一次请求生效",
|
|
16520
|
-
analyzeTokenRemoved: "令牌已移除,接口将拒绝所有请求",
|
|
16521
16583
|
overlayTitle: "Web 覆盖",
|
|
16522
16584
|
overlayPath: "覆盖文件",
|
|
16523
16585
|
overlayCount: "{n} 项配置由面板覆盖",
|
|
@@ -16786,6 +16848,8 @@ var OverlayStore = class {
|
|
|
16786
16848
|
this.logger?.warn?.("dsh-channels: Web 覆盖配置不是合法 JSON(%s),按未覆盖处理:%s", this.file, String(error));
|
|
16787
16849
|
return {};
|
|
16788
16850
|
}
|
|
16851
|
+
const moved = movedAnalyzeKeysOf(raw);
|
|
16852
|
+
if (moved.length > 0) this.logger?.warn?.(MOVED_ANALYZE_NOTICE, `${this.file} 中的 ${moved.join("、")}`);
|
|
16789
16853
|
const parsed = parseOverlay(raw);
|
|
16790
16854
|
if (!parsed.ok) {
|
|
16791
16855
|
this.logger?.warn?.("dsh-channels: Web 覆盖配置含非法字段(%s),按未覆盖处理:%s", this.file, parsed.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
|
|
@@ -16864,7 +16928,7 @@ async function openFeishuDomain(ctx) {
|
|
|
16864
16928
|
const accessUsersRaw = domain.table("access_users");
|
|
16865
16929
|
const accessRequestsRaw = domain.table("access_requests");
|
|
16866
16930
|
const accessWikiUsersRaw = domain.table("access_wiki_users");
|
|
16867
|
-
const
|
|
16931
|
+
const topicLaunchesRaw = domain.table("analyze_requests");
|
|
16868
16932
|
const sessionOriginsRaw = domain.table("session_origins");
|
|
16869
16933
|
return {
|
|
16870
16934
|
routes: {
|
|
@@ -16915,10 +16979,10 @@ async function openFeishuDomain(ctx) {
|
|
|
16915
16979
|
delete: (key) => accessWikiUsersRaw.delete(key),
|
|
16916
16980
|
entries: () => accessWikiUsersRaw.entries()
|
|
16917
16981
|
},
|
|
16918
|
-
|
|
16919
|
-
get: (key) =>
|
|
16920
|
-
put: (key, value) =>
|
|
16921
|
-
entries: () =>
|
|
16982
|
+
topicLaunches: {
|
|
16983
|
+
get: (key) => topicLaunchesRaw.get(key),
|
|
16984
|
+
put: (key, value) => topicLaunchesRaw.put(key, value),
|
|
16985
|
+
entries: () => topicLaunchesRaw.entries()
|
|
16922
16986
|
},
|
|
16923
16987
|
sessionOrigins: {
|
|
16924
16988
|
get: (key) => sessionOriginsRaw.get(key),
|
|
@@ -16998,8 +17062,8 @@ var FeishuService = class {
|
|
|
16998
17062
|
messageReader = new MessageReader({ channel: () => this.channel });
|
|
16999
17063
|
/** Inbound routing: dedup, admission, decision tree, session establishment. */
|
|
17000
17064
|
inbound;
|
|
17001
|
-
/**
|
|
17002
|
-
|
|
17065
|
+
/** The `feishuTopics` service (other plugins' launches → shared topics). */
|
|
17066
|
+
topicLauncher;
|
|
17003
17067
|
/** Agent/session lifecycle: live registry, resume, presets, renew. */
|
|
17004
17068
|
sessions;
|
|
17005
17069
|
/**
|
|
@@ -17254,7 +17318,7 @@ var FeishuService = class {
|
|
|
17254
17318
|
handleSlash: (msg, line) => this.handleSlashMessage(msg, line)
|
|
17255
17319
|
}
|
|
17256
17320
|
});
|
|
17257
|
-
this.
|
|
17321
|
+
this.topicLauncher = new TopicLauncher({
|
|
17258
17322
|
ctx: this.ctx,
|
|
17259
17323
|
config: this.config,
|
|
17260
17324
|
started: () => this.started,
|
|
@@ -17266,8 +17330,7 @@ var FeishuService = class {
|
|
|
17266
17330
|
streamer: () => this.streamer,
|
|
17267
17331
|
queue: this.routeOperations,
|
|
17268
17332
|
reserveTopic: (threadId) => this.inbound.reserveTopic(threadId),
|
|
17269
|
-
|
|
17270
|
-
resolveToken: () => resolveCredentialRef(this.ctx, this.config.analyzeToken).catch(() => void 0)
|
|
17333
|
+
mayLaunchIn: (chatId) => this.accessController.mayLaunchIn(chatId)
|
|
17271
17334
|
});
|
|
17272
17335
|
}
|
|
17273
17336
|
/** Effective inbound attachment backend (matches the inbound router). */
|
|
@@ -17548,9 +17611,9 @@ var FeishuService = class {
|
|
|
17548
17611
|
this.restarting = false;
|
|
17549
17612
|
}
|
|
17550
17613
|
}
|
|
17551
|
-
/** The
|
|
17552
|
-
|
|
17553
|
-
return this.
|
|
17614
|
+
/** The `feishuTopics` service, provided by `apply()`. */
|
|
17615
|
+
topics() {
|
|
17616
|
+
return this.topicLauncher;
|
|
17554
17617
|
}
|
|
17555
17618
|
isRunning() {
|
|
17556
17619
|
return this.started;
|
|
@@ -18096,15 +18159,10 @@ var FeishuService = class {
|
|
|
18096
18159
|
};
|
|
18097
18160
|
}
|
|
18098
18161
|
};
|
|
18099
|
-
const [appId, appSecret
|
|
18100
|
-
read(this.config.appId),
|
|
18101
|
-
read(this.config.appSecret),
|
|
18102
|
-
read(this.config.analyzeToken)
|
|
18103
|
-
]);
|
|
18162
|
+
const [appId, appSecret] = await Promise.all([read(this.config.appId), read(this.config.appSecret)]);
|
|
18104
18163
|
return {
|
|
18105
18164
|
appId,
|
|
18106
|
-
appSecret
|
|
18107
|
-
analyzeToken
|
|
18165
|
+
appSecret
|
|
18108
18166
|
};
|
|
18109
18167
|
}
|
|
18110
18168
|
/** The host credentials seam, when mounted. */
|
|
@@ -18140,7 +18198,6 @@ var FeishuService = class {
|
|
|
18140
18198
|
switch (field) {
|
|
18141
18199
|
case "appId": return this.config.appId;
|
|
18142
18200
|
case "appSecret": return this.config.appSecret;
|
|
18143
|
-
case "analyzeToken": return this.config.analyzeToken;
|
|
18144
18201
|
}
|
|
18145
18202
|
}
|
|
18146
18203
|
/** Wire events and open the long connection. */
|
|
@@ -18239,8 +18296,8 @@ var FeishuService = class {
|
|
|
18239
18296
|
await channel.connect();
|
|
18240
18297
|
this.runScopeCheck(channel, appId, ownerLookup);
|
|
18241
18298
|
if (this.config.slashCommandPanel) this.runSlashCommandSync(channel);
|
|
18242
|
-
await this.
|
|
18243
|
-
this.
|
|
18299
|
+
await this.topicLauncher.markInterrupted();
|
|
18300
|
+
this.topicLauncher.open();
|
|
18244
18301
|
this.repair.repairAll().catch((error) => {
|
|
18245
18302
|
this.ctx.logger?.warn?.("dsh-channels: inflight repair failed: %s", String(error));
|
|
18246
18303
|
});
|
|
@@ -18275,8 +18332,8 @@ var FeishuService = class {
|
|
|
18275
18332
|
/** Tear down: close the connection and dispose all live agents. */
|
|
18276
18333
|
async shutdown() {
|
|
18277
18334
|
this.started = false;
|
|
18278
|
-
this.
|
|
18279
|
-
await this.
|
|
18335
|
+
this.topicLauncher.close();
|
|
18336
|
+
await this.topicLauncher.drain();
|
|
18280
18337
|
for (const controller of this.commandControllers) controller.abort();
|
|
18281
18338
|
await Promise.allSettled([...this.commandOperations]);
|
|
18282
18339
|
this.commandControllers.clear();
|
|
@@ -19490,7 +19547,7 @@ const MAX_BODY_BYTES = 64 * 1024;
|
|
|
19490
19547
|
function messageOf$2(error) {
|
|
19491
19548
|
return error instanceof Error ? error.message : String(error);
|
|
19492
19549
|
}
|
|
19493
|
-
var HttpError
|
|
19550
|
+
var HttpError = class extends Error {
|
|
19494
19551
|
status;
|
|
19495
19552
|
constructor(status, message) {
|
|
19496
19553
|
super(message);
|
|
@@ -19508,17 +19565,17 @@ function json(status, payload) {
|
|
|
19508
19565
|
}
|
|
19509
19566
|
/** Parse a JSON-object body; malformed bodies throw {@link HttpError} (415/413/400). */
|
|
19510
19567
|
async function readJsonObject(request) {
|
|
19511
|
-
if ((request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") throw new HttpError
|
|
19568
|
+
if ((request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") throw new HttpError(415, "请求体必须是 application/json");
|
|
19512
19569
|
const text = await request.text();
|
|
19513
|
-
if (Buffer.byteLength(text) > MAX_BODY_BYTES) throw new HttpError
|
|
19514
|
-
if (text === "") throw new HttpError
|
|
19570
|
+
if (Buffer.byteLength(text) > MAX_BODY_BYTES) throw new HttpError(413, "请求体过大");
|
|
19571
|
+
if (text === "") throw new HttpError(400, "请求体为空");
|
|
19515
19572
|
let body;
|
|
19516
19573
|
try {
|
|
19517
19574
|
body = JSON.parse(text);
|
|
19518
19575
|
} catch {
|
|
19519
|
-
throw new HttpError
|
|
19576
|
+
throw new HttpError(400, "请求体不是合法 JSON");
|
|
19520
19577
|
}
|
|
19521
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new HttpError
|
|
19578
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new HttpError(400, "请求体必须是 JSON 对象");
|
|
19522
19579
|
return body;
|
|
19523
19580
|
}
|
|
19524
19581
|
/** Distinct non-empty entries of one comma-separated id query parameter. */
|
|
@@ -19536,7 +19593,7 @@ function answered(handler) {
|
|
|
19536
19593
|
try {
|
|
19537
19594
|
return await handler(request);
|
|
19538
19595
|
} catch (error) {
|
|
19539
|
-
if (error instanceof HttpError
|
|
19596
|
+
if (error instanceof HttpError) return json(error.status, {
|
|
19540
19597
|
ok: false,
|
|
19541
19598
|
error: error.message
|
|
19542
19599
|
});
|
|
@@ -19602,14 +19659,13 @@ function registerSettingsRoutes(connection, surface) {
|
|
|
19602
19659
|
const values = {};
|
|
19603
19660
|
if (payload.appId !== void 0) values.appId = payload.appId;
|
|
19604
19661
|
if (payload.appSecret !== void 0) values.appSecret = payload.appSecret;
|
|
19605
|
-
if (payload.analyzeToken !== void 0) values.analyzeToken = payload.analyzeToken;
|
|
19606
19662
|
return result(await surface.saveCredentials(values));
|
|
19607
19663
|
}),
|
|
19608
19664
|
post(CLEAR_CREDENTIAL_ROUTE, async (request) => {
|
|
19609
19665
|
const field = (await readJsonObject(request)).field;
|
|
19610
|
-
if (field !== "appId" && field !== "appSecret"
|
|
19666
|
+
if (field !== "appId" && field !== "appSecret") return json(400, {
|
|
19611
19667
|
ok: false,
|
|
19612
|
-
error: "field 必须是 appId
|
|
19668
|
+
error: "field 必须是 appId 或 appSecret"
|
|
19613
19669
|
});
|
|
19614
19670
|
return result(await surface.clearCredential(field));
|
|
19615
19671
|
}),
|
|
@@ -19673,160 +19729,6 @@ function mountSettingsRoutes(ctx, surface) {
|
|
|
19673
19729
|
attach(ctx);
|
|
19674
19730
|
}
|
|
19675
19731
|
//#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) {
|
|
19805
|
-
const attach = (webCtx) => {
|
|
19806
|
-
const server = webCtx.get?.("webServer");
|
|
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");
|
|
19821
|
-
};
|
|
19822
|
-
const inject = ctx.inject;
|
|
19823
|
-
if (typeof inject === "function") {
|
|
19824
|
-
inject.call(ctx, ["webServer"], attach);
|
|
19825
|
-
return;
|
|
19826
|
-
}
|
|
19827
|
-
attach(ctx);
|
|
19828
|
-
}
|
|
19829
|
-
//#endregion
|
|
19830
19732
|
//#region src/settings/surface.ts
|
|
19831
19733
|
/**
|
|
19832
19734
|
* Web 设置面板的 Host 视图模型:把 `FeishuService` 的运行时事实、覆盖层与
|
|
@@ -19841,11 +19743,7 @@ function mountAnalyzeRoute(ctx, handler, warn) {
|
|
|
19841
19743
|
*/
|
|
19842
19744
|
const RESTART_SET = new Set(RESTART_KEYS);
|
|
19843
19745
|
/** Every credential slot the panel writes, in form order. */
|
|
19844
|
-
const CREDENTIAL_FIELDS = [
|
|
19845
|
-
"appId",
|
|
19846
|
-
"appSecret",
|
|
19847
|
-
"analyzeToken"
|
|
19848
|
-
];
|
|
19746
|
+
const CREDENTIAL_FIELDS = ["appId", "appSecret"];
|
|
19849
19747
|
/** Optional fields: an empty string in the panel clears the override. */
|
|
19850
19748
|
const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
|
|
19851
19749
|
"provider",
|
|
@@ -19867,8 +19765,7 @@ const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
|
|
|
19867
19765
|
"feishuTools.topicMessages",
|
|
19868
19766
|
"feishuTools.sendFile",
|
|
19869
19767
|
"feishuTools.wikiRead",
|
|
19870
|
-
"feishuTools.wikiWrite"
|
|
19871
|
-
"analyzeEnabled"
|
|
19768
|
+
"feishuTools.wikiWrite"
|
|
19872
19769
|
]);
|
|
19873
19770
|
/** Editable keys rendered as `string-list` (one input per entry). */
|
|
19874
19771
|
const STRING_LIST_FIELDS = { wikiRoots: {
|
|
@@ -20309,12 +20206,6 @@ const FIELD_META = {
|
|
|
20309
20206
|
placeholder: "https://<租户>.feishu.cn/wiki/space/<space_id>",
|
|
20310
20207
|
wide: true
|
|
20311
20208
|
},
|
|
20312
|
-
analyzeEnabled: {
|
|
20313
|
-
label: "日志分析接口",
|
|
20314
|
-
group: "analyze",
|
|
20315
|
-
summary: "关闭时接口返回 404",
|
|
20316
|
-
help: "接口挂在 DSH Web 端口上:POST /open-api/logalyze,请求需带 Authorization: Bearer <访问令牌>。只有带 Web 的 profile 才有这个接口。改后立即生效,不需要重连。"
|
|
20317
|
-
},
|
|
20318
20209
|
loggerLevel: {
|
|
20319
20210
|
label: "SDK 日志级别",
|
|
20320
20211
|
group: "logging"
|
|
@@ -20665,10 +20556,6 @@ var SettingsSurface = class {
|
|
|
20665
20556
|
appSecret: {
|
|
20666
20557
|
configured: false,
|
|
20667
20558
|
writable: false
|
|
20668
|
-
},
|
|
20669
|
-
analyzeToken: {
|
|
20670
|
-
configured: false,
|
|
20671
|
-
writable: false
|
|
20672
20559
|
}
|
|
20673
20560
|
};
|
|
20674
20561
|
try {
|
|
@@ -20684,8 +20571,7 @@ var SettingsSurface = class {
|
|
|
20684
20571
|
});
|
|
20685
20572
|
const credentials = {
|
|
20686
20573
|
appId: toWire(effective.appId, credentialStates.appId),
|
|
20687
|
-
appSecret: toWire(effective.appSecret, credentialStates.appSecret)
|
|
20688
|
-
analyzeToken: toWire(effective.analyzeToken, credentialStates.analyzeToken)
|
|
20574
|
+
appSecret: toWire(effective.appSecret, credentialStates.appSecret)
|
|
20689
20575
|
};
|
|
20690
20576
|
let access;
|
|
20691
20577
|
try {
|
|
@@ -20861,11 +20747,6 @@ var SettingsSurface = class {
|
|
|
20861
20747
|
error: messageOf$1(error)
|
|
20862
20748
|
};
|
|
20863
20749
|
}
|
|
20864
|
-
if (writes.every((write) => write.field === "analyzeToken")) return {
|
|
20865
|
-
ok: true,
|
|
20866
|
-
panel: await this.panel(),
|
|
20867
|
-
reconnect: "not-needed"
|
|
20868
|
-
};
|
|
20869
20750
|
return this.finishCredentialChange("save");
|
|
20870
20751
|
}
|
|
20871
20752
|
/** Remove the stored value behind one credential reference. */
|
|
@@ -20884,11 +20765,6 @@ var SettingsSurface = class {
|
|
|
20884
20765
|
error: messageOf$1(error)
|
|
20885
20766
|
};
|
|
20886
20767
|
}
|
|
20887
|
-
if (field === "analyzeToken") return {
|
|
20888
|
-
ok: true,
|
|
20889
|
-
panel: await this.panel(),
|
|
20890
|
-
reconnect: "not-needed"
|
|
20891
|
-
};
|
|
20892
20768
|
return this.finishCredentialChange("clear");
|
|
20893
20769
|
}
|
|
20894
20770
|
/**
|
|
@@ -21415,7 +21291,9 @@ function apply(ctx, config) {
|
|
|
21415
21291
|
ctx.logger?.warn?.(message, ...args);
|
|
21416
21292
|
};
|
|
21417
21293
|
mountSettingsRoutes(ctx, new SettingsSurface(service, overlay, { warn }, createHostCatalog(ctx, warn), createSettingsDirectoryPicker(ctx)));
|
|
21418
|
-
|
|
21294
|
+
ctx.provide?.(FEISHU_TOPICS_SERVICE, service.topics());
|
|
21295
|
+
const moved = movedAnalyzeKeysOf(config);
|
|
21296
|
+
if (moved.length > 0) warn(MOVED_ANALYZE_NOTICE, `cordis.yml 中的 ${moved.join("、")}`);
|
|
21419
21297
|
ctx.effect(() => {
|
|
21420
21298
|
service.boot().catch((error) => {
|
|
21421
21299
|
ctx.logger?.error?.("dsh-channels failed to boot: %s", String(error));
|
|
@@ -21425,6 +21303,6 @@ function apply(ctx, config) {
|
|
|
21425
21303
|
}, "dsh-channels");
|
|
21426
21304
|
}
|
|
21427
21305
|
//#endregion
|
|
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 };
|
|
21306
|
+
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, FEISHU_TOPICS_SERVICE, FEISHU_TOPICS_VERSION, 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, TopicLauncher, 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, buildTopicRootCard, 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 };
|
|
21429
21307
|
|
|
21430
21308
|
//# sourceMappingURL=index.js.map
|