@modesto_ma/dsh-channels 0.6.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 -557
- 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 -125
- 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,116 +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 /api/dsh-channels.logalyze` on the DSH Web server, with
|
|
7469
|
-
* `Authorization: Bearer <token>`, an `Idempotency-Key` header, and a JSON
|
|
7470
|
-
* body naming the requester, the trigger time, the subject (application +
|
|
7471
|
-
* error code), the log-platform detail pages, the target group, and the
|
|
7472
|
-
* prompt. A valid request posts the analysis root card into the group and
|
|
7473
|
-
* answers `202`; the analysis then streams in a topic below that card. Log
|
|
7474
|
-
* packages are the prompt's business: the analysis skill fetches whatever the
|
|
7475
|
-
* prompt names, and the detail pages are only listed on the card.
|
|
7476
|
-
*
|
|
7477
|
-
* @module dsh-channels/analyze/contract
|
|
7478
|
-
*/
|
|
7479
|
-
/**
|
|
7480
|
-
* Exact DSH Web route of the analyze API. It follows the host's plain-HTTP
|
|
7481
|
-
* `/api/<name>.<method>` naming; an exact route wins over the `/api` RPC prefix
|
|
7482
|
-
* route, so the request carries this API's bearer token, not a DSH login.
|
|
7483
|
-
*/
|
|
7484
|
-
const ANALYZE_ROUTE = "/api/dsh-channels.logalyze";
|
|
7485
|
-
/** Largest accepted request body (the prompt dominates it). */
|
|
7486
|
-
const ANALYZE_MAX_BODY_BYTES = 1024 * 1024;
|
|
7487
|
-
/** Largest accepted prompt, in characters. */
|
|
7488
|
-
const ANALYZE_MAX_PROMPT_CHARS = 1e5;
|
|
7489
|
-
/**
|
|
7490
|
-
* `Idempotency-Key` format: 1–200 visible ASCII characters without spaces, so
|
|
7491
|
-
* a platform can use a UUID or its own `type:id` token.
|
|
7492
|
-
*/
|
|
7493
|
-
const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,200}$/u;
|
|
7494
|
-
/**
|
|
7495
|
-
* Application / error-code segment: it names a workspace directory
|
|
7496
|
-
* (`<baseDir>/analyze/<app>/<errorCode>`), so only path-safe characters pass.
|
|
7497
|
-
*/
|
|
7498
|
-
const SUBJECT_SEGMENT = /^(?!\.{1,2}$)[A-Za-z0-9._-]{1,128}$/u;
|
|
7499
|
-
/** JSON body of one analyze request. */
|
|
7500
|
-
const analyzeRequestSchema = z.object({
|
|
7501
|
-
/** Feishu user_id of the person who started the analysis on the platform. */
|
|
7502
|
-
requesterUserId: z.string().regex(FEISHU_USER_ID_PATTERN, "must be a Feishu user_id").max(128),
|
|
7503
|
-
/** When the analysis was started on the platform, shown verbatim on the root card. */
|
|
7504
|
-
triggeredAt: z.string().min(1).max(64).refine((value) => value.trim() !== "", "must not be blank"),
|
|
7505
|
-
/** Application the logs belong to (workspace directory segment). */
|
|
7506
|
-
app: z.string().regex(SUBJECT_SEGMENT, "must be 1-128 of A-Z a-z 0-9 . _ -"),
|
|
7507
|
-
/** Error code of the logs (workspace directory segment). */
|
|
7508
|
-
errorCode: z.string().regex(SUBJECT_SEGMENT, "must be 1-128 of A-Z a-z 0-9 . _ -"),
|
|
7509
|
-
/** Log-platform detail pages of the logs that triggered the analysis (listed on the root card). */
|
|
7510
|
-
logDetails: z.array(z.object({
|
|
7511
|
-
/** Row label. */
|
|
7512
|
-
name: z.string().min(1).max(200).refine((value) => value.trim() !== "", "must not be blank"),
|
|
7513
|
-
/** Detail page address. */
|
|
7514
|
-
url: z.url({
|
|
7515
|
-
protocol: /^https?$/u,
|
|
7516
|
-
error: "must be an http(s) URL"
|
|
7517
|
-
}).max(2048)
|
|
7518
|
-
}).strict()).min(1).max(50),
|
|
7519
|
-
/** Target group chat (`oc_…`); must be an authorized group. */
|
|
7520
|
-
chatId: z.string().regex(/^oc_[A-Za-z0-9_-]{1,128}$/u, "must be a Feishu group chat_id (oc_...)"),
|
|
7521
|
-
/** Prompt forwarded verbatim as the analysis Session's first user message. */
|
|
7522
|
-
prompt: z.string().min(1).max(ANALYZE_MAX_PROMPT_CHARS).refine((value) => value.trim() !== "", "must not be blank")
|
|
7523
|
-
}).strict();
|
|
7524
|
-
/**
|
|
7525
|
-
* Fingerprint of one validated request, used to tell a retry of the same
|
|
7526
|
-
* request from a reused `Idempotency-Key`. Field order is fixed.
|
|
7527
|
-
* @param request - the validated request.
|
|
7528
|
-
* @returns SHA-256 hex digest.
|
|
7529
|
-
*/
|
|
7530
|
-
function analyzeRequestHash(request) {
|
|
7531
|
-
const canonical = JSON.stringify([
|
|
7532
|
-
request.requesterUserId,
|
|
7533
|
-
request.triggeredAt,
|
|
7534
|
-
request.app,
|
|
7535
|
-
request.errorCode,
|
|
7536
|
-
request.logDetails.map((detail) => [detail.name, detail.url]),
|
|
7537
|
-
request.chatId,
|
|
7538
|
-
request.prompt
|
|
7539
|
-
]);
|
|
7540
|
-
return createHash("sha256").update(canonical).digest("hex");
|
|
7541
|
-
}
|
|
7542
|
-
/**
|
|
7543
|
-
* Validate one decoded JSON body.
|
|
7544
|
-
* @param raw - the decoded body.
|
|
7545
|
-
* @returns the request, or a readable list of field errors.
|
|
7546
|
-
*/
|
|
7547
|
-
function parseAnalyzeRequest(raw) {
|
|
7548
|
-
const parsed = analyzeRequestSchema.safeParse(raw);
|
|
7549
|
-
if (parsed.success) return {
|
|
7550
|
-
ok: true,
|
|
7551
|
-
value: parsed.data
|
|
7552
|
-
};
|
|
7553
|
-
return {
|
|
7554
|
-
ok: false,
|
|
7555
|
-
error: `请求参数不合法:${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(body)"}: ${issue.message}`).join("; ")}`
|
|
7556
|
-
};
|
|
7557
|
-
}
|
|
7558
|
-
//#endregion
|
|
7559
|
-
//#region src/analyze/launcher.ts
|
|
7537
|
+
//#region src/topics/launcher.ts
|
|
7560
7538
|
/**
|
|
7561
|
-
*
|
|
7539
|
+
* The `feishuTopics` service: other plugins start group-shared topics (see
|
|
7540
|
+
* `./contract.ts`).
|
|
7562
7541
|
*
|
|
7563
|
-
* Synchronously (before
|
|
7564
|
-
*
|
|
7565
|
-
*
|
|
7566
|
-
* card
|
|
7567
|
-
* directory, the Session, the group-shared route, and the
|
|
7568
|
-
*
|
|
7569
|
-
* the first turn settles, the root card's status follows it
|
|
7570
|
-
* 失败), and the
|
|
7571
|
-
* records still `running` belong to
|
|
7572
|
-
* finished; {@link
|
|
7573
|
-
*
|
|
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
|
+
* 已中断.
|
|
7574
7553
|
*
|
|
7575
7554
|
* The topic is reserved in the inbound router from its creation until the
|
|
7576
7555
|
* route and the first prompt are published, so replies posted meanwhile queue
|
|
@@ -7580,152 +7559,179 @@ function parseAnalyzeRequest(raw) {
|
|
|
7580
7559
|
* root card failed with the reason, closes the streaming card, and leaves no
|
|
7581
7560
|
* route.
|
|
7582
7561
|
*
|
|
7583
|
-
* @module dsh-channels/
|
|
7584
|
-
*/
|
|
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();
|
|
7585
7600
|
/** Readable reason of an error value. */
|
|
7586
7601
|
function messageOf$4(error) {
|
|
7587
7602
|
return error instanceof Error ? error.message : String(error);
|
|
7588
7603
|
}
|
|
7589
|
-
var
|
|
7604
|
+
var TopicLauncher = class {
|
|
7590
7605
|
deps;
|
|
7591
|
-
|
|
7606
|
+
version = 1;
|
|
7607
|
+
/** Idempotency key → launch still being accepted (concurrent retries share it). */
|
|
7592
7608
|
accepting = /* @__PURE__ */ new Map();
|
|
7593
|
-
/** Background
|
|
7609
|
+
/** Background topics still starting (drained on shutdown). */
|
|
7594
7610
|
running = /* @__PURE__ */ new Set();
|
|
7595
|
-
/** Whether
|
|
7611
|
+
/** Whether launches are admitted: from {@link open} after start until {@link close}. */
|
|
7596
7612
|
admitting = false;
|
|
7597
7613
|
constructor(deps) {
|
|
7598
7614
|
this.deps = deps;
|
|
7599
7615
|
}
|
|
7600
7616
|
/**
|
|
7601
|
-
* Start admitting
|
|
7602
|
-
* {@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.
|
|
7603
7619
|
*/
|
|
7604
7620
|
open() {
|
|
7605
7621
|
this.admitting = true;
|
|
7606
7622
|
}
|
|
7607
|
-
/**
|
|
7623
|
+
/** Refuse launches (`not-connected`) ahead of shutdown. */
|
|
7608
7624
|
close() {
|
|
7609
7625
|
this.admitting = false;
|
|
7610
7626
|
}
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
}
|
|
7617
|
-
async analyze(request, idempotencyKey) {
|
|
7618
|
-
const bodyHash = analyzeRequestHash(request);
|
|
7619
|
-
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);
|
|
7620
7632
|
if (pending !== void 0) {
|
|
7621
|
-
if (pending.
|
|
7622
|
-
return await pending.
|
|
7633
|
+
if (pending.fingerprint !== valid.fingerprint) return reusedKey();
|
|
7634
|
+
return await pending.result;
|
|
7623
7635
|
}
|
|
7624
|
-
const
|
|
7625
|
-
this.accepting.set(
|
|
7626
|
-
|
|
7627
|
-
|
|
7636
|
+
const result = this.accept(valid);
|
|
7637
|
+
this.accepting.set(valid.key, {
|
|
7638
|
+
fingerprint: valid.fingerprint,
|
|
7639
|
+
result
|
|
7628
7640
|
});
|
|
7629
7641
|
try {
|
|
7630
|
-
return await
|
|
7642
|
+
return await result;
|
|
7631
7643
|
} finally {
|
|
7632
|
-
this.accepting.delete(
|
|
7644
|
+
this.accepting.delete(valid.key);
|
|
7633
7645
|
}
|
|
7634
7646
|
}
|
|
7635
7647
|
/**
|
|
7636
|
-
* Wait for every acceptance in flight, then every background
|
|
7637
|
-
*
|
|
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
|
|
7638
7650
|
* its failure card or, when its first prompt was already sent, as an
|
|
7639
7651
|
* ordinary topic.
|
|
7640
7652
|
*/
|
|
7641
7653
|
async drain() {
|
|
7642
|
-
await Promise.allSettled([...this.accepting.values()].map((pending) => pending.
|
|
7654
|
+
await Promise.allSettled([...this.accepting.values()].map((pending) => pending.result));
|
|
7643
7655
|
await Promise.allSettled([...this.running]);
|
|
7644
7656
|
}
|
|
7645
7657
|
/**
|
|
7646
|
-
* Mark every
|
|
7647
|
-
*
|
|
7648
|
-
*
|
|
7649
|
-
*
|
|
7650
|
-
*
|
|
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.
|
|
7651
7663
|
*/
|
|
7652
7664
|
async markInterrupted() {
|
|
7653
7665
|
const channel = this.deps.channel();
|
|
7654
7666
|
const domain = this.deps.domain();
|
|
7655
7667
|
if (channel === void 0 || domain === void 0) return;
|
|
7656
|
-
for (const [key, record] of [...domain.
|
|
7668
|
+
for (const [key, record] of [...domain.topicLaunches.entries()]) {
|
|
7657
7669
|
if (record.status !== "running") continue;
|
|
7658
|
-
|
|
7659
|
-
|
|
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));
|
|
7660
7673
|
});
|
|
7661
7674
|
await this.recordStatus(key, "interrupted");
|
|
7662
7675
|
}
|
|
7663
7676
|
}
|
|
7664
|
-
async accept(
|
|
7677
|
+
async accept(spec) {
|
|
7665
7678
|
const channel = this.deps.channel();
|
|
7666
7679
|
const domain = this.deps.domain();
|
|
7667
|
-
if (!this.admitting || !this.deps.started() || channel === void 0 || domain === void 0) return refused(
|
|
7668
|
-
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);
|
|
7669
7682
|
if (stored !== void 0) {
|
|
7670
|
-
if (stored.bodyHash !==
|
|
7683
|
+
if (stored.bodyHash !== spec.fingerprint) return reusedKey();
|
|
7671
7684
|
return {
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
messageId: stored.messageId
|
|
7677
|
-
}
|
|
7685
|
+
ok: true,
|
|
7686
|
+
replayed: true,
|
|
7687
|
+
chatId: stored.chatId,
|
|
7688
|
+
messageId: stored.messageId
|
|
7678
7689
|
};
|
|
7679
7690
|
}
|
|
7680
|
-
if (!this.deps.
|
|
7681
|
-
const
|
|
7691
|
+
if (!this.deps.mayLaunchIn(spec.chatId)) return refused("forbidden-chat", "目标群未授权:请管理员在该群发送 /auth group add,或在设置面板「访问与授权」中添加");
|
|
7692
|
+
const card = copyCard(spec.card);
|
|
7682
7693
|
let rootId;
|
|
7683
7694
|
try {
|
|
7684
|
-
rootId = (await channel.send(
|
|
7695
|
+
rootId = (await channel.send(spec.chatId, { card: buildTopicRootCard(spec.requesterUserId, card, { kind: "running" }) })).messageId;
|
|
7685
7696
|
} catch (error) {
|
|
7686
|
-
return refused(
|
|
7697
|
+
return refused("send-failed", `${spec.card.taskName}卡片发送失败(机器人需在目标群内):${messageOf$4(error)}`);
|
|
7687
7698
|
}
|
|
7688
|
-
const body = {
|
|
7689
|
-
ok: true,
|
|
7690
|
-
chatId: request.chatId,
|
|
7691
|
-
messageId: rootId
|
|
7692
|
-
};
|
|
7693
7699
|
try {
|
|
7694
|
-
await domain.
|
|
7695
|
-
key,
|
|
7696
|
-
bodyHash,
|
|
7697
|
-
chatId:
|
|
7700
|
+
await domain.topicLaunches.put(spec.key, {
|
|
7701
|
+
key: spec.key,
|
|
7702
|
+
bodyHash: spec.fingerprint,
|
|
7703
|
+
chatId: spec.chatId,
|
|
7698
7704
|
messageId: rootId,
|
|
7699
7705
|
acceptedAt: Date.now(),
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
logDetails: [...subject.logDetails]
|
|
7703
|
-
},
|
|
7706
|
+
requesterUserId: spec.requesterUserId,
|
|
7707
|
+
card,
|
|
7704
7708
|
status: "running"
|
|
7705
7709
|
});
|
|
7706
7710
|
} catch (error) {
|
|
7707
|
-
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));
|
|
7708
7712
|
}
|
|
7709
|
-
const run = this.
|
|
7713
|
+
const run = this.start(channel, spec, card, rootId);
|
|
7710
7714
|
this.running.add(run);
|
|
7711
7715
|
run.finally(() => {
|
|
7712
7716
|
this.running.delete(run);
|
|
7713
7717
|
});
|
|
7714
7718
|
return {
|
|
7715
|
-
|
|
7716
|
-
|
|
7719
|
+
ok: true,
|
|
7720
|
+
replayed: false,
|
|
7721
|
+
chatId: spec.chatId,
|
|
7722
|
+
messageId: rootId
|
|
7717
7723
|
};
|
|
7718
7724
|
}
|
|
7719
|
-
/** Start the
|
|
7720
|
-
async
|
|
7725
|
+
/** Start the topic below an accepted root card; every failure ends on the cards. */
|
|
7726
|
+
async start(channel, spec, card, rootId) {
|
|
7721
7727
|
const warn = (message, ...args) => {
|
|
7722
7728
|
this.deps.ctx.logger?.warn?.(message, ...args);
|
|
7723
7729
|
};
|
|
7724
7730
|
const showStatus = async (status) => {
|
|
7725
|
-
await channel.updateCard(rootId,
|
|
7726
|
-
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));
|
|
7727
7733
|
});
|
|
7728
|
-
await this.recordStatus(key, status.kind);
|
|
7734
|
+
await this.recordStatus(spec.key, status.kind);
|
|
7729
7735
|
};
|
|
7730
7736
|
const sessionId = randomUUID();
|
|
7731
7737
|
let handle;
|
|
@@ -7734,19 +7740,19 @@ var AnalyzeLauncher = class {
|
|
|
7734
7740
|
let releaseTopic;
|
|
7735
7741
|
let published = false;
|
|
7736
7742
|
try {
|
|
7737
|
-
const topic = await this.openTopic(channel,
|
|
7743
|
+
const topic = await this.openTopic(channel, spec.chatId, rootId, sessionId);
|
|
7738
7744
|
cardId = topic.cardId;
|
|
7739
7745
|
threadId = topic.threadId;
|
|
7740
7746
|
releaseTopic = this.deps.reserveTopic(threadId);
|
|
7741
7747
|
const services = placementServices(this.deps.ctx);
|
|
7742
|
-
const workspace = await
|
|
7748
|
+
const workspace = await ensureLaunchWorkspace(this.deps.ctx, this.deps.config.baseDir, spec.workspace.segments, spec.workspace.title);
|
|
7743
7749
|
const topicToken = topicTokenFor(threadId, "thread", sessionId);
|
|
7744
7750
|
const plan = await planTopic(services, this.deps.config, workspace.path, topicToken, warn);
|
|
7745
7751
|
this.assertStarted();
|
|
7746
7752
|
handle = await this.deps.sessions.createSession(sessionId, plan.cwd);
|
|
7747
7753
|
const placement = await enterTopic(services, handle.agent, plan, workspace.path, topicToken, warn);
|
|
7748
7754
|
if (plan.cwd === workspace.path) Promise.resolve(workspace.attachSession(sessionId)).catch((error) => {
|
|
7749
|
-
warn("dsh-channels: attaching
|
|
7755
|
+
warn("dsh-channels: attaching topic session %s to workspace failed: %s", sessionId, String(error));
|
|
7750
7756
|
});
|
|
7751
7757
|
await this.deps.sessions.mountGuidance(handle.agent, placement.sessionDir);
|
|
7752
7758
|
this.assertStarted();
|
|
@@ -7755,9 +7761,9 @@ var AnalyzeLauncher = class {
|
|
|
7755
7761
|
key: threadId,
|
|
7756
7762
|
kind: "thread",
|
|
7757
7763
|
sessionId,
|
|
7758
|
-
chatId:
|
|
7764
|
+
chatId: spec.chatId,
|
|
7759
7765
|
chatType: "group",
|
|
7760
|
-
senderUserId:
|
|
7766
|
+
senderUserId: spec.requesterUserId,
|
|
7761
7767
|
workspacePath: workspace.path,
|
|
7762
7768
|
sessionDir: placement.sessionDir,
|
|
7763
7769
|
...placement.worktreeBranch === void 0 ? {} : { worktreeBranch: placement.worktreeBranch },
|
|
@@ -7773,7 +7779,7 @@ var AnalyzeLauncher = class {
|
|
|
7773
7779
|
await this.deps.repair.write({
|
|
7774
7780
|
key: cardId,
|
|
7775
7781
|
sessionId,
|
|
7776
|
-
chatId:
|
|
7782
|
+
chatId: spec.chatId,
|
|
7777
7783
|
triggerMessageId: rootId,
|
|
7778
7784
|
cards: [{
|
|
7779
7785
|
messageId: cardId,
|
|
@@ -7792,13 +7798,13 @@ var AnalyzeLauncher = class {
|
|
|
7792
7798
|
this.deps.streamer()?.attach(agent.session, streamCardId, rootId, void 0, { onSettled: (outcome) => {
|
|
7793
7799
|
showStatus({ kind: outcome });
|
|
7794
7800
|
} });
|
|
7795
|
-
agent.followup(feishuUserMessage(
|
|
7801
|
+
agent.followup(feishuUserMessage(spec.prompt));
|
|
7796
7802
|
});
|
|
7797
7803
|
} catch (error) {
|
|
7798
7804
|
const reason = messageOf$4(error);
|
|
7799
|
-
warn("dsh-channels:
|
|
7805
|
+
warn("dsh-channels: topic %s failed to start: %s", spec.key, reason);
|
|
7800
7806
|
if (published && threadId !== void 0) await this.deps.routes.remove(threadId).catch((removeError) => {
|
|
7801
|
-
warn("dsh-channels: removing
|
|
7807
|
+
warn("dsh-channels: removing topic route %s failed: %s", threadId, String(removeError));
|
|
7802
7808
|
});
|
|
7803
7809
|
await handle?.dispose().catch(() => {});
|
|
7804
7810
|
if (cardId !== void 0) {
|
|
@@ -7806,7 +7812,7 @@ var AnalyzeLauncher = class {
|
|
|
7806
7812
|
await channel.updateCard(cardId, buildCard({
|
|
7807
7813
|
phase: "error",
|
|
7808
7814
|
reasoning: "",
|
|
7809
|
-
body: `❌
|
|
7815
|
+
body: `❌ ${card.taskName}未能开始:${reason}`,
|
|
7810
7816
|
stats: {
|
|
7811
7817
|
sessionId,
|
|
7812
7818
|
inputTokens: 0,
|
|
@@ -7823,8 +7829,8 @@ var AnalyzeLauncher = class {
|
|
|
7823
7829
|
}
|
|
7824
7830
|
}
|
|
7825
7831
|
/**
|
|
7826
|
-
* Open the
|
|
7827
|
-
*
|
|
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
|
|
7828
7834
|
* topic groups).
|
|
7829
7835
|
*/
|
|
7830
7836
|
async openTopic(channel, chatId, rootId, sessionId) {
|
|
@@ -7851,44 +7857,52 @@ var AnalyzeLauncher = class {
|
|
|
7851
7857
|
threadId
|
|
7852
7858
|
};
|
|
7853
7859
|
}
|
|
7854
|
-
/** Store a root card status in the
|
|
7860
|
+
/** Store a root card status in the launch record (best-effort; the card already shows it). */
|
|
7855
7861
|
async recordStatus(key, status) {
|
|
7856
7862
|
const domain = this.deps.domain();
|
|
7857
|
-
const record = domain?.
|
|
7863
|
+
const record = domain?.topicLaunches.get(key);
|
|
7858
7864
|
if (domain === void 0 || record === void 0) return;
|
|
7859
7865
|
try {
|
|
7860
|
-
await domain.
|
|
7866
|
+
await domain.topicLaunches.put(key, {
|
|
7861
7867
|
...record,
|
|
7862
7868
|
status
|
|
7863
7869
|
});
|
|
7864
7870
|
} catch (error) {
|
|
7865
|
-
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));
|
|
7866
7872
|
}
|
|
7867
7873
|
}
|
|
7868
7874
|
assertStarted() {
|
|
7869
|
-
if (!this.deps.started()) throw new Error("
|
|
7875
|
+
if (!this.deps.started()) throw new Error("飞书渠道已断开,已取消");
|
|
7870
7876
|
}
|
|
7871
7877
|
};
|
|
7872
|
-
|
|
7878
|
+
/** A detached copy of the card, so the caller cannot change what is recorded. */
|
|
7879
|
+
function copyCard(card) {
|
|
7873
7880
|
return {
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
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
|
+
} }
|
|
7879
7895
|
};
|
|
7880
7896
|
}
|
|
7881
|
-
function refused(
|
|
7897
|
+
function refused(code, message) {
|
|
7882
7898
|
return {
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
error
|
|
7887
|
-
}
|
|
7899
|
+
ok: false,
|
|
7900
|
+
code,
|
|
7901
|
+
message
|
|
7888
7902
|
};
|
|
7889
7903
|
}
|
|
7890
7904
|
function reusedKey() {
|
|
7891
|
-
return refused(
|
|
7905
|
+
return refused("key-conflict", "幂等键已被内容不同的请求使用");
|
|
7892
7906
|
}
|
|
7893
7907
|
//#endregion
|
|
7894
7908
|
//#region src/session/repair.ts
|
|
@@ -14981,6 +14995,8 @@ var TurnStreamer = class {
|
|
|
14981
14995
|
toolKind: "",
|
|
14982
14996
|
heartbeatTimer: void 0,
|
|
14983
14997
|
attempts: /* @__PURE__ */ new Map(),
|
|
14998
|
+
retry: void 0,
|
|
14999
|
+
retries: 0,
|
|
14984
15000
|
stopRequested: false,
|
|
14985
15001
|
stopDenied: false,
|
|
14986
15002
|
queuedDenied: false
|
|
@@ -15169,6 +15185,10 @@ var TurnStreamer = class {
|
|
|
15169
15185
|
break;
|
|
15170
15186
|
case "assistant/message":
|
|
15171
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
|
+
}
|
|
15172
15192
|
if (event.data.usage !== void 0) {
|
|
15173
15193
|
state.lastUsage = {
|
|
15174
15194
|
usage: event.data.usage,
|
|
@@ -15250,6 +15270,24 @@ var TurnStreamer = class {
|
|
|
15250
15270
|
}
|
|
15251
15271
|
break;
|
|
15252
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;
|
|
15253
15291
|
case "turn/end":
|
|
15254
15292
|
if (state.turn === void 0) state.turn = event.data.turn;
|
|
15255
15293
|
this.finalize(session.id, state, turnFailure(event.data.reason), turnAborted(event.data.reason));
|
|
@@ -15278,6 +15316,10 @@ var TurnStreamer = class {
|
|
|
15278
15316
|
break;
|
|
15279
15317
|
case "chunk": {
|
|
15280
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
|
+
}
|
|
15281
15323
|
if (chunk.type === "text-delta") {
|
|
15282
15324
|
if (chunk.text !== "") this.closeReasoning(state);
|
|
15283
15325
|
const last = state.cards[state.cards.length - 1];
|
|
@@ -15297,7 +15339,7 @@ var TurnStreamer = class {
|
|
|
15297
15339
|
this.closeReasoning(state);
|
|
15298
15340
|
const mark = state.attempts.get(frame.attemptId);
|
|
15299
15341
|
state.attempts.delete(frame.attemptId);
|
|
15300
|
-
if (frame.outcome.kind === "abandoned" && mark !== void 0) {
|
|
15342
|
+
if ((frame.outcome.kind === "abandoned" || frame.outcome.eventType === "assistant/attempt") && mark !== void 0) {
|
|
15301
15343
|
this.rollbackAttempt(state, mark);
|
|
15302
15344
|
this.markDirty(session.id, state);
|
|
15303
15345
|
}
|
|
@@ -15322,11 +15364,13 @@ var TurnStreamer = class {
|
|
|
15322
15364
|
state.reasoningSince = void 0;
|
|
15323
15365
|
}
|
|
15324
15366
|
/**
|
|
15325
|
-
* Drop one
|
|
15367
|
+
* Drop one discarded attempt's deltas. The attempt's text is a contiguous
|
|
15326
15368
|
* suffix of the logical body: segments created during the attempt are
|
|
15327
15369
|
* removed, the segment that was last at `start` is truncated, and the
|
|
15328
15370
|
* reasoning tail is cut. Already-delivered plain-text segments cannot be
|
|
15329
|
-
* 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.
|
|
15330
15374
|
*/
|
|
15331
15375
|
rollbackAttempt(state, mark) {
|
|
15332
15376
|
const lastIndex = mark.cardCount - 1;
|
|
@@ -15334,6 +15378,11 @@ var TurnStreamer = class {
|
|
|
15334
15378
|
const card = state.cards[index];
|
|
15335
15379
|
if (card === void 0) continue;
|
|
15336
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
|
+
}
|
|
15337
15386
|
state.cards.splice(index, 1);
|
|
15338
15387
|
}
|
|
15339
15388
|
const tail = state.cards[lastIndex];
|
|
@@ -15342,6 +15391,9 @@ var TurnStreamer = class {
|
|
|
15342
15391
|
tail.body = tail.body.slice(0, length);
|
|
15343
15392
|
tail.dirty = true;
|
|
15344
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;
|
|
15345
15397
|
state.reasoning = state.reasoning.slice(0, mark.reasoningLength);
|
|
15346
15398
|
state.reasoningMs = mark.reasoningMs;
|
|
15347
15399
|
state.reasoningSince = mark.reasoningSince;
|
|
@@ -15508,6 +15560,13 @@ var TurnStreamer = class {
|
|
|
15508
15560
|
toolLine(state, now) {
|
|
15509
15561
|
if (state.finalized) return "";
|
|
15510
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
|
+
}
|
|
15511
15570
|
const tool = state.toolActivity;
|
|
15512
15571
|
if (tool === void 0) return "";
|
|
15513
15572
|
return clipActivity(`${tool.name === "subagent" ? "🤖 调用子代理:" : tool.name === "workflow" ? "🧵 调用工作流:" : "🔧 调用工具:"}${tool.label} · ${fmtElapsed(now - tool.since)}`);
|
|
@@ -15698,7 +15757,8 @@ var TurnStreamer = class {
|
|
|
15698
15757
|
this.closeReasoning(state);
|
|
15699
15758
|
if (failure !== void 0) {
|
|
15700
15759
|
const code = failure.code === void 0 ? "" : `\n\n错误码:\`${failure.code}\``;
|
|
15701
|
-
const
|
|
15760
|
+
const retried = state.retries === 0 ? "" : `\n\n已自动重试 ${state.retries} 次`;
|
|
15761
|
+
const notice = `❌ **本轮运行失败**\n\n${failure.message}${code}${retried}`;
|
|
15702
15762
|
const tail = state.cards[state.cards.length - 1];
|
|
15703
15763
|
if (tail !== void 0) if (tail.mode === "text" || tail.failed) state.cards.push(this.freshSegment(notice));
|
|
15704
15764
|
else {
|
|
@@ -15715,6 +15775,7 @@ var TurnStreamer = class {
|
|
|
15715
15775
|
state.heartbeatTimer = void 0;
|
|
15716
15776
|
}
|
|
15717
15777
|
state.tool = "";
|
|
15778
|
+
state.retry = void 0;
|
|
15718
15779
|
this.splitOverflow(sessionId, state);
|
|
15719
15780
|
for (const [index] of state.cards.entries()) this.patchCard(sessionId, state, index, true, false);
|
|
15720
15781
|
for (const trigger of [state, ...state.mergedTriggers]) this.swapReaction(trigger, failure !== void 0);
|
|
@@ -15895,8 +15956,24 @@ const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 2e4;
|
|
|
15895
15956
|
* optional context block.
|
|
15896
15957
|
*/
|
|
15897
15958
|
const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3e3;
|
|
15898
|
-
/**
|
|
15899
|
-
|
|
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
|
+
}
|
|
15900
15977
|
/** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
|
|
15901
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");
|
|
15902
15979
|
const configSchema = z.object({
|
|
@@ -16226,28 +16303,20 @@ const configSchema = z.object({
|
|
|
16226
16303
|
* this window fails closed with an explicit error; a host *timed* wait owns
|
|
16227
16304
|
* its own deadline and is left to the host signal instead.
|
|
16228
16305
|
*/
|
|
16229
|
-
questionTimeoutMs: z.number().int().min(1e4).max(36e5).default(DEFAULT_QUESTION_TIMEOUT_MS)
|
|
16230
|
-
/**
|
|
16231
|
-
* Switch for the log-platform analyze API (`POST /api/dsh-channels.logalyze`
|
|
16232
|
-
* on the DSH Web server). Off (default) answers 404; read per request, so
|
|
16233
|
-
* toggling it needs no reconnect. Needs a host with a Web server.
|
|
16234
|
-
*/
|
|
16235
|
-
analyzeEnabled: z.boolean().default(false),
|
|
16236
|
-
/**
|
|
16237
|
-
* Credential reference holding the analyze API bearer token (defaults to
|
|
16238
|
-
* `DSH_CHANNELS_ANALYZE_TOKEN`). Resolved per request, so rotating the value
|
|
16239
|
-
* applies to the next request; an unresolvable token refuses every request.
|
|
16240
|
-
*/
|
|
16241
|
-
analyzeToken: credentialRefSchema.default(DEFAULT_ANALYZE_TOKEN_REF)
|
|
16306
|
+
questionTimeoutMs: z.number().int().min(1e4).max(36e5).default(DEFAULT_QUESTION_TIMEOUT_MS)
|
|
16242
16307
|
});
|
|
16243
16308
|
/**
|
|
16244
16309
|
* DSH loader-facing schema. The bundle patch inserts this plugin without a
|
|
16245
16310
|
* `config` object; the loader validates that row with `undefined` before the
|
|
16246
16311
|
* user's patch layer supplies values. Accept that absence and fill the same
|
|
16247
16312
|
* defaults as {@link resolveConfig}, so enabling the plugin from the Web UI
|
|
16248
|
-
* 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.
|
|
16249
16315
|
*/
|
|
16250
|
-
const pluginConfigSchema = configSchema.
|
|
16316
|
+
const pluginConfigSchema = configSchema.extend({
|
|
16317
|
+
analyzeEnabled: z.unknown().optional(),
|
|
16318
|
+
analyzeToken: z.unknown().optional()
|
|
16319
|
+
}).prefault({});
|
|
16251
16320
|
/**
|
|
16252
16321
|
* Validate the raw loader config. Values stay credential *references*; the
|
|
16253
16322
|
* secrets themselves resolve through `ctx.credentials` at startup.
|
|
@@ -16384,7 +16453,6 @@ const EDITABLE_KEYS = [
|
|
|
16384
16453
|
"feishuTools.wikiRead",
|
|
16385
16454
|
"feishuTools.wikiWrite",
|
|
16386
16455
|
"wikiRoots",
|
|
16387
|
-
"analyzeEnabled",
|
|
16388
16456
|
"loggerLevel"
|
|
16389
16457
|
];
|
|
16390
16458
|
/**
|
|
@@ -16512,11 +16580,6 @@ const PANEL_COPY = {
|
|
|
16512
16580
|
reconnectDone: "已重启飞书连接",
|
|
16513
16581
|
loading: "加载中…",
|
|
16514
16582
|
loadFailed: "设置面板加载失败",
|
|
16515
|
-
analyzeTokenLabel: "访问令牌",
|
|
16516
|
-
analyzeTokenSummary: "请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求",
|
|
16517
|
-
analyzeTokenPlaceholder: "填写新令牌(建议 32 位以上随机串)",
|
|
16518
|
-
analyzeTokenSaved: "令牌已保存,下一次请求生效",
|
|
16519
|
-
analyzeTokenRemoved: "令牌已移除,接口将拒绝所有请求",
|
|
16520
16583
|
overlayTitle: "Web 覆盖",
|
|
16521
16584
|
overlayPath: "覆盖文件",
|
|
16522
16585
|
overlayCount: "{n} 项配置由面板覆盖",
|
|
@@ -16785,6 +16848,8 @@ var OverlayStore = class {
|
|
|
16785
16848
|
this.logger?.warn?.("dsh-channels: Web 覆盖配置不是合法 JSON(%s),按未覆盖处理:%s", this.file, String(error));
|
|
16786
16849
|
return {};
|
|
16787
16850
|
}
|
|
16851
|
+
const moved = movedAnalyzeKeysOf(raw);
|
|
16852
|
+
if (moved.length > 0) this.logger?.warn?.(MOVED_ANALYZE_NOTICE, `${this.file} 中的 ${moved.join("、")}`);
|
|
16788
16853
|
const parsed = parseOverlay(raw);
|
|
16789
16854
|
if (!parsed.ok) {
|
|
16790
16855
|
this.logger?.warn?.("dsh-channels: Web 覆盖配置含非法字段(%s),按未覆盖处理:%s", this.file, parsed.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
|
|
@@ -16863,7 +16928,7 @@ async function openFeishuDomain(ctx) {
|
|
|
16863
16928
|
const accessUsersRaw = domain.table("access_users");
|
|
16864
16929
|
const accessRequestsRaw = domain.table("access_requests");
|
|
16865
16930
|
const accessWikiUsersRaw = domain.table("access_wiki_users");
|
|
16866
|
-
const
|
|
16931
|
+
const topicLaunchesRaw = domain.table("analyze_requests");
|
|
16867
16932
|
const sessionOriginsRaw = domain.table("session_origins");
|
|
16868
16933
|
return {
|
|
16869
16934
|
routes: {
|
|
@@ -16914,10 +16979,10 @@ async function openFeishuDomain(ctx) {
|
|
|
16914
16979
|
delete: (key) => accessWikiUsersRaw.delete(key),
|
|
16915
16980
|
entries: () => accessWikiUsersRaw.entries()
|
|
16916
16981
|
},
|
|
16917
|
-
|
|
16918
|
-
get: (key) =>
|
|
16919
|
-
put: (key, value) =>
|
|
16920
|
-
entries: () =>
|
|
16982
|
+
topicLaunches: {
|
|
16983
|
+
get: (key) => topicLaunchesRaw.get(key),
|
|
16984
|
+
put: (key, value) => topicLaunchesRaw.put(key, value),
|
|
16985
|
+
entries: () => topicLaunchesRaw.entries()
|
|
16921
16986
|
},
|
|
16922
16987
|
sessionOrigins: {
|
|
16923
16988
|
get: (key) => sessionOriginsRaw.get(key),
|
|
@@ -16997,8 +17062,8 @@ var FeishuService = class {
|
|
|
16997
17062
|
messageReader = new MessageReader({ channel: () => this.channel });
|
|
16998
17063
|
/** Inbound routing: dedup, admission, decision tree, session establishment. */
|
|
16999
17064
|
inbound;
|
|
17000
|
-
/**
|
|
17001
|
-
|
|
17065
|
+
/** The `feishuTopics` service (other plugins' launches → shared topics). */
|
|
17066
|
+
topicLauncher;
|
|
17002
17067
|
/** Agent/session lifecycle: live registry, resume, presets, renew. */
|
|
17003
17068
|
sessions;
|
|
17004
17069
|
/**
|
|
@@ -17253,7 +17318,7 @@ var FeishuService = class {
|
|
|
17253
17318
|
handleSlash: (msg, line) => this.handleSlashMessage(msg, line)
|
|
17254
17319
|
}
|
|
17255
17320
|
});
|
|
17256
|
-
this.
|
|
17321
|
+
this.topicLauncher = new TopicLauncher({
|
|
17257
17322
|
ctx: this.ctx,
|
|
17258
17323
|
config: this.config,
|
|
17259
17324
|
started: () => this.started,
|
|
@@ -17265,8 +17330,7 @@ var FeishuService = class {
|
|
|
17265
17330
|
streamer: () => this.streamer,
|
|
17266
17331
|
queue: this.routeOperations,
|
|
17267
17332
|
reserveTopic: (threadId) => this.inbound.reserveTopic(threadId),
|
|
17268
|
-
|
|
17269
|
-
resolveToken: () => resolveCredentialRef(this.ctx, this.config.analyzeToken).catch(() => void 0)
|
|
17333
|
+
mayLaunchIn: (chatId) => this.accessController.mayLaunchIn(chatId)
|
|
17270
17334
|
});
|
|
17271
17335
|
}
|
|
17272
17336
|
/** Effective inbound attachment backend (matches the inbound router). */
|
|
@@ -17547,9 +17611,9 @@ var FeishuService = class {
|
|
|
17547
17611
|
this.restarting = false;
|
|
17548
17612
|
}
|
|
17549
17613
|
}
|
|
17550
|
-
/** The
|
|
17551
|
-
|
|
17552
|
-
return this.
|
|
17614
|
+
/** The `feishuTopics` service, provided by `apply()`. */
|
|
17615
|
+
topics() {
|
|
17616
|
+
return this.topicLauncher;
|
|
17553
17617
|
}
|
|
17554
17618
|
isRunning() {
|
|
17555
17619
|
return this.started;
|
|
@@ -18095,15 +18159,10 @@ var FeishuService = class {
|
|
|
18095
18159
|
};
|
|
18096
18160
|
}
|
|
18097
18161
|
};
|
|
18098
|
-
const [appId, appSecret
|
|
18099
|
-
read(this.config.appId),
|
|
18100
|
-
read(this.config.appSecret),
|
|
18101
|
-
read(this.config.analyzeToken)
|
|
18102
|
-
]);
|
|
18162
|
+
const [appId, appSecret] = await Promise.all([read(this.config.appId), read(this.config.appSecret)]);
|
|
18103
18163
|
return {
|
|
18104
18164
|
appId,
|
|
18105
|
-
appSecret
|
|
18106
|
-
analyzeToken
|
|
18165
|
+
appSecret
|
|
18107
18166
|
};
|
|
18108
18167
|
}
|
|
18109
18168
|
/** The host credentials seam, when mounted. */
|
|
@@ -18139,7 +18198,6 @@ var FeishuService = class {
|
|
|
18139
18198
|
switch (field) {
|
|
18140
18199
|
case "appId": return this.config.appId;
|
|
18141
18200
|
case "appSecret": return this.config.appSecret;
|
|
18142
|
-
case "analyzeToken": return this.config.analyzeToken;
|
|
18143
18201
|
}
|
|
18144
18202
|
}
|
|
18145
18203
|
/** Wire events and open the long connection. */
|
|
@@ -18238,8 +18296,8 @@ var FeishuService = class {
|
|
|
18238
18296
|
await channel.connect();
|
|
18239
18297
|
this.runScopeCheck(channel, appId, ownerLookup);
|
|
18240
18298
|
if (this.config.slashCommandPanel) this.runSlashCommandSync(channel);
|
|
18241
|
-
await this.
|
|
18242
|
-
this.
|
|
18299
|
+
await this.topicLauncher.markInterrupted();
|
|
18300
|
+
this.topicLauncher.open();
|
|
18243
18301
|
this.repair.repairAll().catch((error) => {
|
|
18244
18302
|
this.ctx.logger?.warn?.("dsh-channels: inflight repair failed: %s", String(error));
|
|
18245
18303
|
});
|
|
@@ -18274,8 +18332,8 @@ var FeishuService = class {
|
|
|
18274
18332
|
/** Tear down: close the connection and dispose all live agents. */
|
|
18275
18333
|
async shutdown() {
|
|
18276
18334
|
this.started = false;
|
|
18277
|
-
this.
|
|
18278
|
-
await this.
|
|
18335
|
+
this.topicLauncher.close();
|
|
18336
|
+
await this.topicLauncher.drain();
|
|
18279
18337
|
for (const controller of this.commandControllers) controller.abort();
|
|
18280
18338
|
await Promise.allSettled([...this.commandOperations]);
|
|
18281
18339
|
this.commandControllers.clear();
|
|
@@ -19489,7 +19547,7 @@ const MAX_BODY_BYTES = 64 * 1024;
|
|
|
19489
19547
|
function messageOf$2(error) {
|
|
19490
19548
|
return error instanceof Error ? error.message : String(error);
|
|
19491
19549
|
}
|
|
19492
|
-
var HttpError
|
|
19550
|
+
var HttpError = class extends Error {
|
|
19493
19551
|
status;
|
|
19494
19552
|
constructor(status, message) {
|
|
19495
19553
|
super(message);
|
|
@@ -19507,17 +19565,17 @@ function json(status, payload) {
|
|
|
19507
19565
|
}
|
|
19508
19566
|
/** Parse a JSON-object body; malformed bodies throw {@link HttpError} (415/413/400). */
|
|
19509
19567
|
async function readJsonObject(request) {
|
|
19510
|
-
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");
|
|
19511
19569
|
const text = await request.text();
|
|
19512
|
-
if (Buffer.byteLength(text) > MAX_BODY_BYTES) throw new HttpError
|
|
19513
|
-
if (text === "") throw new HttpError
|
|
19570
|
+
if (Buffer.byteLength(text) > MAX_BODY_BYTES) throw new HttpError(413, "请求体过大");
|
|
19571
|
+
if (text === "") throw new HttpError(400, "请求体为空");
|
|
19514
19572
|
let body;
|
|
19515
19573
|
try {
|
|
19516
19574
|
body = JSON.parse(text);
|
|
19517
19575
|
} catch {
|
|
19518
|
-
throw new HttpError
|
|
19576
|
+
throw new HttpError(400, "请求体不是合法 JSON");
|
|
19519
19577
|
}
|
|
19520
|
-
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 对象");
|
|
19521
19579
|
return body;
|
|
19522
19580
|
}
|
|
19523
19581
|
/** Distinct non-empty entries of one comma-separated id query parameter. */
|
|
@@ -19535,7 +19593,7 @@ function answered(handler) {
|
|
|
19535
19593
|
try {
|
|
19536
19594
|
return await handler(request);
|
|
19537
19595
|
} catch (error) {
|
|
19538
|
-
if (error instanceof HttpError
|
|
19596
|
+
if (error instanceof HttpError) return json(error.status, {
|
|
19539
19597
|
ok: false,
|
|
19540
19598
|
error: error.message
|
|
19541
19599
|
});
|
|
@@ -19601,14 +19659,13 @@ function registerSettingsRoutes(connection, surface) {
|
|
|
19601
19659
|
const values = {};
|
|
19602
19660
|
if (payload.appId !== void 0) values.appId = payload.appId;
|
|
19603
19661
|
if (payload.appSecret !== void 0) values.appSecret = payload.appSecret;
|
|
19604
|
-
if (payload.analyzeToken !== void 0) values.analyzeToken = payload.analyzeToken;
|
|
19605
19662
|
return result(await surface.saveCredentials(values));
|
|
19606
19663
|
}),
|
|
19607
19664
|
post(CLEAR_CREDENTIAL_ROUTE, async (request) => {
|
|
19608
19665
|
const field = (await readJsonObject(request)).field;
|
|
19609
|
-
if (field !== "appId" && field !== "appSecret"
|
|
19666
|
+
if (field !== "appId" && field !== "appSecret") return json(400, {
|
|
19610
19667
|
ok: false,
|
|
19611
|
-
error: "field 必须是 appId
|
|
19668
|
+
error: "field 必须是 appId 或 appSecret"
|
|
19612
19669
|
});
|
|
19613
19670
|
return result(await surface.clearCredential(field));
|
|
19614
19671
|
}),
|
|
@@ -19672,160 +19729,6 @@ function mountSettingsRoutes(ctx, surface) {
|
|
|
19672
19729
|
attach(ctx);
|
|
19673
19730
|
}
|
|
19674
19731
|
//#endregion
|
|
19675
|
-
//#region src/analyze/server.ts
|
|
19676
|
-
/**
|
|
19677
|
-
* The analyze API's HTTP route on the DSH Web server.
|
|
19678
|
-
*
|
|
19679
|
-
* Registers the exact route `POST /api/dsh-channels.logalyze` (see
|
|
19680
|
-
* `./contract.ts`) on the host `webServer`; hosts without one (headless) have
|
|
19681
|
-
* no analyze API. This module owns the transport-level checks, in order:
|
|
19682
|
-
* path, the `analyzeEnabled` switch, method, bearer token, `Idempotency-Key`,
|
|
19683
|
-
* media type, body size, and JSON validation; everything after validation
|
|
19684
|
-
* belongs to the {@link AnalyzeHandler}. Bodies, tokens, and prompts are never
|
|
19685
|
-
* logged.
|
|
19686
|
-
*
|
|
19687
|
-
* @module dsh-channels/analyze/server
|
|
19688
|
-
*/
|
|
19689
|
-
var HttpError = class extends Error {
|
|
19690
|
-
status;
|
|
19691
|
-
constructor(status, message) {
|
|
19692
|
-
super(message);
|
|
19693
|
-
this.status = status;
|
|
19694
|
-
}
|
|
19695
|
-
};
|
|
19696
|
-
function send(res, response) {
|
|
19697
|
-
const body = JSON.stringify(response.body);
|
|
19698
|
-
res.writeHead(response.status, {
|
|
19699
|
-
"content-type": "application/json; charset=utf-8",
|
|
19700
|
-
"cache-control": "no-store",
|
|
19701
|
-
"content-length": Buffer.byteLength(body)
|
|
19702
|
-
});
|
|
19703
|
-
res.end(body);
|
|
19704
|
-
}
|
|
19705
|
-
function refuse(res, status, error, headers = {}) {
|
|
19706
|
-
for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
|
|
19707
|
-
send(res, {
|
|
19708
|
-
status,
|
|
19709
|
-
body: {
|
|
19710
|
-
ok: false,
|
|
19711
|
-
error
|
|
19712
|
-
}
|
|
19713
|
-
});
|
|
19714
|
-
}
|
|
19715
|
-
/** Constant-time token comparison over fixed-length digests. */
|
|
19716
|
-
function tokenMatches(presented, expected) {
|
|
19717
|
-
return timingSafeEqual(createHash("sha256").update(presented).digest(), createHash("sha256").update(expected).digest());
|
|
19718
|
-
}
|
|
19719
|
-
/** The bearer credential of one request, or undefined. */
|
|
19720
|
-
function bearerOf(req) {
|
|
19721
|
-
const header = req.headers.authorization;
|
|
19722
|
-
if (header === void 0) return void 0;
|
|
19723
|
-
return /^Bearer[ \t]+(\S+)[ \t]*$/iu.exec(header)?.[1];
|
|
19724
|
-
}
|
|
19725
|
-
/** Read the body up to {@link ANALYZE_MAX_BODY_BYTES}; larger bodies fail with 413. */
|
|
19726
|
-
async function readBody(req) {
|
|
19727
|
-
const declared = Number(req.headers["content-length"]);
|
|
19728
|
-
if (Number.isFinite(declared) && declared > 1048576) throw new HttpError(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
|
|
19729
|
-
const chunks = [];
|
|
19730
|
-
let size = 0;
|
|
19731
|
-
for await (const chunk of req) {
|
|
19732
|
-
const buffer = chunk;
|
|
19733
|
-
size += buffer.length;
|
|
19734
|
-
if (size > 1048576) throw new HttpError(413, `请求体超过 ${ANALYZE_MAX_BODY_BYTES} 字节`);
|
|
19735
|
-
chunks.push(buffer);
|
|
19736
|
-
}
|
|
19737
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
19738
|
-
}
|
|
19739
|
-
/**
|
|
19740
|
-
* Handle one HTTP request against the analyze contract.
|
|
19741
|
-
* @param req - the incoming request.
|
|
19742
|
-
* @param res - its response.
|
|
19743
|
-
* @param handler - the business side.
|
|
19744
|
-
*/
|
|
19745
|
-
async function handleAnalyzeHttp(req, res, handler) {
|
|
19746
|
-
if (new URL(req.url ?? "/", "http://analyze.invalid").pathname !== "/api/dsh-channels.logalyze") {
|
|
19747
|
-
refuse(res, 404, "未知路径");
|
|
19748
|
-
return;
|
|
19749
|
-
}
|
|
19750
|
-
if (!handler.enabled()) {
|
|
19751
|
-
refuse(res, 404, "日志分析接口未开启,请在设置面板「高级 → 日志分析接口」打开开关");
|
|
19752
|
-
return;
|
|
19753
|
-
}
|
|
19754
|
-
if ((req.method ?? "GET").toUpperCase() !== "POST") {
|
|
19755
|
-
refuse(res, 405, "只接受 POST", { allow: "POST" });
|
|
19756
|
-
return;
|
|
19757
|
-
}
|
|
19758
|
-
const expected = await handler.token();
|
|
19759
|
-
if (expected === void 0 || expected === "") {
|
|
19760
|
-
refuse(res, 503, "分析接口未配置访问令牌,请联系部署者配置 analyzeToken 引用的凭据");
|
|
19761
|
-
return;
|
|
19762
|
-
}
|
|
19763
|
-
const presented = bearerOf(req);
|
|
19764
|
-
if (presented === void 0 || !tokenMatches(presented, expected)) {
|
|
19765
|
-
refuse(res, 401, "访问令牌无效", { "www-authenticate": "Bearer" });
|
|
19766
|
-
return;
|
|
19767
|
-
}
|
|
19768
|
-
const idempotencyKey = req.headers["idempotency-key"];
|
|
19769
|
-
if (typeof idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)) {
|
|
19770
|
-
refuse(res, 400, "缺少或不合法的 Idempotency-Key 请求头(1-200 个可见 ASCII 字符)");
|
|
19771
|
-
return;
|
|
19772
|
-
}
|
|
19773
|
-
if ((req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase() !== "application/json") {
|
|
19774
|
-
refuse(res, 415, "Content-Type 必须是 application/json");
|
|
19775
|
-
return;
|
|
19776
|
-
}
|
|
19777
|
-
let raw;
|
|
19778
|
-
try {
|
|
19779
|
-
raw = JSON.parse(await readBody(req));
|
|
19780
|
-
} catch (error) {
|
|
19781
|
-
if (error instanceof HttpError) {
|
|
19782
|
-
refuse(res, error.status, error.message);
|
|
19783
|
-
return;
|
|
19784
|
-
}
|
|
19785
|
-
refuse(res, 400, "请求体不是合法的 JSON");
|
|
19786
|
-
return;
|
|
19787
|
-
}
|
|
19788
|
-
const parsed = parseAnalyzeRequest(raw);
|
|
19789
|
-
if (!parsed.ok) {
|
|
19790
|
-
refuse(res, 400, parsed.error);
|
|
19791
|
-
return;
|
|
19792
|
-
}
|
|
19793
|
-
send(res, await handler.analyze(parsed.value, idempotencyKey));
|
|
19794
|
-
}
|
|
19795
|
-
/**
|
|
19796
|
-
* Register the analyze route on the host Web server once it is available.
|
|
19797
|
-
* Requests skip the DSH login gate on purpose: the bearer token is this API's
|
|
19798
|
-
* only credential.
|
|
19799
|
-
* @param ctx - the plugin context.
|
|
19800
|
-
* @param handler - the business side.
|
|
19801
|
-
* @param warn - diagnostics sink for unexpected handler failures.
|
|
19802
|
-
*/
|
|
19803
|
-
function mountAnalyzeRoute(ctx, handler, warn) {
|
|
19804
|
-
const attach = (webCtx) => {
|
|
19805
|
-
const server = webCtx.get?.("webServer");
|
|
19806
|
-
if (server === void 0) return;
|
|
19807
|
-
webCtx.effect(() => server.register({
|
|
19808
|
-
kind: "exact",
|
|
19809
|
-
path: ANALYZE_ROUTE,
|
|
19810
|
-
handler: async (req, res) => {
|
|
19811
|
-
try {
|
|
19812
|
-
await handleAnalyzeHttp(req, res, handler);
|
|
19813
|
-
} catch (error) {
|
|
19814
|
-
warn("dsh-channels: analyze request failed: %s", String(error));
|
|
19815
|
-
if (!res.headersSent) refuse(res, 500, `分析请求处理失败:${String(error)}`);
|
|
19816
|
-
else res.destroy();
|
|
19817
|
-
}
|
|
19818
|
-
}
|
|
19819
|
-
}), "dsh-channels: analyze route");
|
|
19820
|
-
};
|
|
19821
|
-
const inject = ctx.inject;
|
|
19822
|
-
if (typeof inject === "function") {
|
|
19823
|
-
inject.call(ctx, ["webServer"], attach);
|
|
19824
|
-
return;
|
|
19825
|
-
}
|
|
19826
|
-
attach(ctx);
|
|
19827
|
-
}
|
|
19828
|
-
//#endregion
|
|
19829
19732
|
//#region src/settings/surface.ts
|
|
19830
19733
|
/**
|
|
19831
19734
|
* Web 设置面板的 Host 视图模型:把 `FeishuService` 的运行时事实、覆盖层与
|
|
@@ -19840,11 +19743,7 @@ function mountAnalyzeRoute(ctx, handler, warn) {
|
|
|
19840
19743
|
*/
|
|
19841
19744
|
const RESTART_SET = new Set(RESTART_KEYS);
|
|
19842
19745
|
/** Every credential slot the panel writes, in form order. */
|
|
19843
|
-
const CREDENTIAL_FIELDS = [
|
|
19844
|
-
"appId",
|
|
19845
|
-
"appSecret",
|
|
19846
|
-
"analyzeToken"
|
|
19847
|
-
];
|
|
19746
|
+
const CREDENTIAL_FIELDS = ["appId", "appSecret"];
|
|
19848
19747
|
/** Optional fields: an empty string in the panel clears the override. */
|
|
19849
19748
|
const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
|
|
19850
19749
|
"provider",
|
|
@@ -19866,8 +19765,7 @@ const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
|
|
|
19866
19765
|
"feishuTools.topicMessages",
|
|
19867
19766
|
"feishuTools.sendFile",
|
|
19868
19767
|
"feishuTools.wikiRead",
|
|
19869
|
-
"feishuTools.wikiWrite"
|
|
19870
|
-
"analyzeEnabled"
|
|
19768
|
+
"feishuTools.wikiWrite"
|
|
19871
19769
|
]);
|
|
19872
19770
|
/** Editable keys rendered as `string-list` (one input per entry). */
|
|
19873
19771
|
const STRING_LIST_FIELDS = { wikiRoots: {
|
|
@@ -20308,12 +20206,6 @@ const FIELD_META = {
|
|
|
20308
20206
|
placeholder: "https://<租户>.feishu.cn/wiki/space/<space_id>",
|
|
20309
20207
|
wide: true
|
|
20310
20208
|
},
|
|
20311
|
-
analyzeEnabled: {
|
|
20312
|
-
label: "日志分析接口",
|
|
20313
|
-
group: "analyze",
|
|
20314
|
-
summary: "关闭时接口返回 404",
|
|
20315
|
-
help: "接口挂在 DSH Web 端口上:POST /api/dsh-channels.logalyze,请求需带 Authorization: Bearer <访问令牌>。只有带 Web 的 profile 才有这个接口。改后立即生效,不需要重连。"
|
|
20316
|
-
},
|
|
20317
20209
|
loggerLevel: {
|
|
20318
20210
|
label: "SDK 日志级别",
|
|
20319
20211
|
group: "logging"
|
|
@@ -20664,10 +20556,6 @@ var SettingsSurface = class {
|
|
|
20664
20556
|
appSecret: {
|
|
20665
20557
|
configured: false,
|
|
20666
20558
|
writable: false
|
|
20667
|
-
},
|
|
20668
|
-
analyzeToken: {
|
|
20669
|
-
configured: false,
|
|
20670
|
-
writable: false
|
|
20671
20559
|
}
|
|
20672
20560
|
};
|
|
20673
20561
|
try {
|
|
@@ -20683,8 +20571,7 @@ var SettingsSurface = class {
|
|
|
20683
20571
|
});
|
|
20684
20572
|
const credentials = {
|
|
20685
20573
|
appId: toWire(effective.appId, credentialStates.appId),
|
|
20686
|
-
appSecret: toWire(effective.appSecret, credentialStates.appSecret)
|
|
20687
|
-
analyzeToken: toWire(effective.analyzeToken, credentialStates.analyzeToken)
|
|
20574
|
+
appSecret: toWire(effective.appSecret, credentialStates.appSecret)
|
|
20688
20575
|
};
|
|
20689
20576
|
let access;
|
|
20690
20577
|
try {
|
|
@@ -20860,11 +20747,6 @@ var SettingsSurface = class {
|
|
|
20860
20747
|
error: messageOf$1(error)
|
|
20861
20748
|
};
|
|
20862
20749
|
}
|
|
20863
|
-
if (writes.every((write) => write.field === "analyzeToken")) return {
|
|
20864
|
-
ok: true,
|
|
20865
|
-
panel: await this.panel(),
|
|
20866
|
-
reconnect: "not-needed"
|
|
20867
|
-
};
|
|
20868
20750
|
return this.finishCredentialChange("save");
|
|
20869
20751
|
}
|
|
20870
20752
|
/** Remove the stored value behind one credential reference. */
|
|
@@ -20883,11 +20765,6 @@ var SettingsSurface = class {
|
|
|
20883
20765
|
error: messageOf$1(error)
|
|
20884
20766
|
};
|
|
20885
20767
|
}
|
|
20886
|
-
if (field === "analyzeToken") return {
|
|
20887
|
-
ok: true,
|
|
20888
|
-
panel: await this.panel(),
|
|
20889
|
-
reconnect: "not-needed"
|
|
20890
|
-
};
|
|
20891
20768
|
return this.finishCredentialChange("clear");
|
|
20892
20769
|
}
|
|
20893
20770
|
/**
|
|
@@ -21414,7 +21291,9 @@ function apply(ctx, config) {
|
|
|
21414
21291
|
ctx.logger?.warn?.(message, ...args);
|
|
21415
21292
|
};
|
|
21416
21293
|
mountSettingsRoutes(ctx, new SettingsSurface(service, overlay, { warn }, createHostCatalog(ctx, warn), createSettingsDirectoryPicker(ctx)));
|
|
21417
|
-
|
|
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("、")}`);
|
|
21418
21297
|
ctx.effect(() => {
|
|
21419
21298
|
service.boot().catch((error) => {
|
|
21420
21299
|
ctx.logger?.error?.("dsh-channels failed to boot: %s", String(error));
|
|
@@ -21424,6 +21303,6 @@ function apply(ctx, config) {
|
|
|
21424
21303
|
}, "dsh-channels");
|
|
21425
21304
|
}
|
|
21426
21305
|
//#endregion
|
|
21427
|
-
export { AUTH_USER_TARGET_ERRORS, AccessController, AccessRequests, AccessStore, ApprovalAnswerer, CARD_MAX_BYTES, CARD_TARGET_BYTES, CATALOG_ROUTE, CHANNEL_COMMAND_NAMES, CLEAR_CONFIG_ROUTE, CLEAR_CREDENTIAL_ROUTE, Config, DM_DENY_TEXT, DM_DENY_WHITELIST_TEXT, DM_UNCONFIGURED_TEXT, DSH_CHANNELS_VERSION, EDITABLE_KEYS, EMPTY_TOTALS, FIELD_DESCRIPTORS, FeishuService, HEALTH_DEPENDENCY_NAMES, INTERRUPTION_NOTICE, LOGICAL_EXPORT_FORMAT, LOGICAL_EXPORT_FORMAT_VERSION, MAX_QUESTION_OPTIONS, MAX_TABLES_PER_CARD, OverlayStore, PANEL_CHEVRON_TOKEN, PANEL_COPY, PANEL_ROUTE, PANEL_TITLES, PICK_DIRECTORY_ROUTE, QuestionAnswerer, REASONING_MAX_BYTES, RECONNECT_ROUTE, RESTART_KEYS, RouteOperationQueue, SAVE_CONFIG_ROUTE, SAVE_CREDENTIALS_ROUTE, SESSION_HISTORY_DISPLAY_LIMIT, STATUS_ROUTE, SUPPORTED_COMMAND_NAMES, SettingsSurface, TRUNCATION_SUFFIX, TurnStreamer, accessGroupRecordSchema, accessRequestRecordSchema, accessUserRecordSchema, addUsage, appendSessionHistory, apply, applyCatalog, applyProjectionViews, armModelCommand, averageDecodeTokensPerSecond, averageTtftMs, buildApprovalCard, buildApprovalSettledCard, buildCard, buildCommandResultCard, buildCompactComparisonCard, buildGroupInfoCard, buildHealthCard, buildInterruptionCard, buildQuestionCard, buildQuestionSettledCard, buildRenewCard, buildRequestSettledCard, buildSessionHistoryCard, buildSessionStatusCard, buildUserRequestCard, buildWorkDirCard, cacheHitPct, cardJsonBytes, chatRouteKey, commandResultCardKind, compactFailureContext, countFiberStates, createHostCatalog, createLogicalSessionExport, createSettingsDirectoryPicker, decideAccess, defaultOverlayPath, emptyCatalog, ensureWorkspace, expiredRouteRecordSchema, feishuDomain, foldAll, foldEvent, foldHealthSnapshot, foldSessionStats, inflightSnapshotSchema, inject, isAdminUserId, isSupportedCommand, maskOpenIdTail, measureSessionContext, mergeOverlay, mountSettingsRoutes, name, normalizeCredentialValue, normalizeOverlay, normalizeRouteRecord, normalizeSessionHistory, parseOverlay, parseSlashCommand, pickEditable, processedRecordSchema, querySessionHistory, readProjectionViews, registerChannelCommands, registerModelCommand, registerSettingsRoutes, resolveAuthUserTarget, resolveDshHome, resolveWorkDir, routeRecordSchema, segmentCardContent, serializeLogicalSession, sessionDirOf, sessionHistoryEntrySchema, splitBodyForCard, stagingPathFor, stagingRoot, stripTables, supportedCommandHelp, supportedCommandList, tableBlocks, topicDirFor, topicTokenFor, usageFromBuckets, withDirectoryBrowse, workspacePathFor, workspaceTitleFor, worktreeBranchFor };
|
|
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 };
|
|
21428
21307
|
|
|
21429
21308
|
//# sourceMappingURL=index.js.map
|